Time as an interval, not an instant.
Every mainstream language gives you date, time, and datetime as scalar points, and then leaves you to invent the spans yourself. Is ~D[2026-06-15] the instant of midnight, or the whole day? The type cannot say — so every codebase grows its own end_of_day/1, its own month-end arithmetic, its own quiet off-by-one.
These libraries take the other road. A temporal value is a bounded span at a stated resolution: 2026 is the whole year, 2026-06 the whole month, 2026-06-15T10 that one hour. Once time is an interval, the operations that are awkward elsewhere become ordinary — iteration, comparison, overlap, free-busy, set algebra — and a class of bugs stops being possible rather than being caught.
tempo — the core
One type for every temporal value — a year, an afternoon, a meeting, an archaeological period, a recurring event, a whole free-busy calendar — with every operation defined uniformly over it.
-
ISO 8601 Parts 1 and 2, plus IXDTF and RFC 5545
RRULE. Partial dates, unspecified digits, uncertainty qualifiers, and±margins are first-class rather than parse errors. -
Set algebra over time — union, intersection, difference, and overlap depth, on a half-open
[from, to)convention that makes adjacent spans join cleanly. -
Allen's interval algebra — all thirteen relations, with three-valued certainty (
:certain/:possible/:impossible) when the inputs are uncertain. -
Recurrence in ISO 8601 or RFC 5545 form, expanded correctly across daylight-saving transitions.
-
iCalendar and JSCalendar import — RFC 5545
.icsthroughTempo.ICal.from_ical/2, RFC 7953VAVAILABILITYthroughTempo.ICal.available_from_ical/2(the time someone offers, as against the time they have taken), and RFC 8984 JSCalendar throughTempo.JSCalendar.from_jscalendar/2. Event metadata — summary, location, attendees, status — rides along on each interval, so intersecting a schedule with work hours tells you which meetings fall inside, not merely how much time. -
Calendar- and territory-aware arithmetic through Localize and Calendrical — non-Gregorian calendars, locale-aware formatting, and workday arithmetic that knows which days are the weekend where you are.
-
Constraint networks and dependency scheduling — critical paths, and consistency checking over relative or undated constraints.
work = ~o"2026-06-15T09:00:00/2026-06-15T17:00:00"
lunch = ~o"2026-06-15T12:00:00/2026-06-15T13:00:00"
{:ok, free} = Tempo.difference(work, lunch)
free |> Tempo.IntervalSet.slots(~o"PT1H") |> Tempo.IntervalSet.count()
#=> 7"The workday minus lunch, cut into bookable hours."
tempo_sql — persistence
Ecto types and migration helpers that store Tempo intervals as PostgreSQL range values.
PostgreSQL already models this shape natively: a tstzrange is a bounded span with the same half-open convention, and tstzmultirange is a set of disjoint spans. There is no impedance mismatch to paper over — an interval dumps straight to a range, loads straight back, and takes part in Postgres range queries (@>, &&, -|-) as a first-class value.
create table(:meetings) do
add :name, :string
add_interval :window, null: false
endRequires PostgreSQL 14+ for multirange support; tstzrange alone works on 9.2+.
jscalendar — interchange
RFC 8984 JSCalendar — events, tasks and groups — read and written as ordinary JSON.
JSCalendar carries what iCalendar carries, in a form that needs no parser of its own. Where iCalendar's line folding, backslash escaping and property parameters have produced two decades of interoperability bugs, this is a JSON object with named fields — so Erlang's :json and Elixir's Duration are the entire toolchain and the library has no dependencies at all. Values arrive as standard types: NaiveDateTime, DateTime, Duration, MapSet.
An event carries a start and a duration rather than a start and an end — the same commitment the rest of these libraries make. The end is start + duration in the event's own time zone, so an hour-long meeting stays an hour long across a daylight-saving boundary, where a stored end time would silently become two hours or none.
{:ok, event} = JSCalendar.decode(~s({
"@type": "Event",
"uid": "a8df6573-0474-496d-8496-033ad45d7fea",
"title": "Budget review",
"start": "2026-03-07T13:00:00",
"timeZone": "America/New_York",
"duration": "PT1H"
}))
JSCalendar.Event.ends_at(event)
#=> ~N[2026-03-07 14:00:00]Properties the library has never heard of are kept and written back untouched, and properties equal to their RFC default are omitted on encode, so a document neither loses information nor grows each time it is relayed. Expanding a recurrence rule into the dates it generates is calendar arithmetic rather than a format concern, and stays with tempo — Tempo.JSCalendar.from_jscalendar/2 reads a document straight onto a timeline as a Tempo.IntervalSet, recurrence and overrides resolved.
Requires OTP 27 or later — that is where Erlang's :json arrived.
New to the idea? The Tempo README opens with why intervals remove whole classes of bug, and the getting-started livebook is a tour you can run in Livebook without installing anything.
If you would rather see it applied, the cookbook works through the problems people actually bring — free-busy, recurring events, business days, uncertain historical dates, and scheduling across time zones.
Issues and pull requests are welcome on the individual repositories. All the libraries hold to the same standards: formatted, no compiler warnings, tests and doctests passing, Dialyzer clean, Credo strict, and documentation that builds without warnings. Every example in the documentation is executed rather than illustrated — if a snippet is in the docs, it runs.
Apache-2.0.