Mix.install([
{:ex_tempo, "~> 2.0"},
{:tz, "~> 0.28"}
])
Calendar.put_time_zone_database(Tz.TimeZoneDatabase)Setup
Run this cell next: every example below depends on it. It imports Tempo.Sigils, which provides the ~o sigil the examples use to write a Tempo value as ISO 8601 text — ~o"2026-06-15" is the whole day of 15 June 2026. The import brings in only sigil_o/2, no other functions.
It needs a cell of its own because Livebook compiles a whole cell before running it, so the setup cell above can't import a module that its own Mix.install/2 has yet to fetch.
import Tempo.SigilsFree time is set algebra
Scheduling questions are set-theory questions in disguise. When is Alice free? is the workday minus her meetings. When are Alice and Bob both free? is the intersection of their free time. No cursors, no sorting by start time — just the algebra:
work = ~o"2026-06-15T09:00:00/2026-06-15T17:00:00"
alice_busy =
Tempo.IntervalSet.new!([
~o"2026-06-15T10:00:00/2026-06-15T11:00:00",
~o"2026-06-15T14:00:00/2026-06-15T15:00:00"
])
bob_busy =
Tempo.IntervalSet.new!([
~o"2026-06-15T09:30:00/2026-06-15T10:30:00",
~o"2026-06-15T16:00:00/2026-06-15T17:00:00"
])
{:ok, alice_free} = Tempo.difference(work, alice_busy)
{:ok, bob_free} = Tempo.difference(work, bob_busy)
{:ok, mutual} = Tempo.intersection(alice_free, bob_free)
mutual"Alice's free time is the workday minus her meetings; Bob's is the same. Mutual free time is the intersection of theirs."
From regions to bookable slots
Free time gives the regions where a meeting could go. A booking page offers discrete slots — cut the regions into hours with slots/3, which leaves out a window too short to hold one:
{:ok, slots} = Tempo.IntervalSet.slots(mutual, ~o"PT1H")
Tempo.IntervalSet.members(slots)When will it be done? — shifting through free time
The dual question — given a task and a busy calendar, when does it finish? — is Tempo.shift/3 with skipping:. The duration is consumed from free time only; busy spans cost nothing to cross:
meeting = ~o"2026-06-15T10:00/2026-06-15T11:00"
Tempo.shift(~o"2026-06-15T09:30", ~o"PT1H", skipping: meeting)"One hour of work starting 09:30, skipping the 10:00 meeting, finishes at 11:30 — thirty minutes before, thirty after."
The busy set can be unbounded. The weekend days of a span with no end are a lazy, territory-aware set, and the walk consumes only as much of it as the shift needs:
{:ok, weekends} = Tempo.select(~o"2026-06-18/..", Tempo.weekends(:US))
Tempo.shift(~o"2026-06-18T16:00", ~o"P3D", skipping: weekends)"Three days of processing starting Thursday 16:00, skipping every weekend, finishes Tuesday 16:00."
Workdays
Business-day arithmetic is the same idea with the calendar's own vocabulary. Which days count as the weekend is territory-aware CLDR data — Saudi Arabia weekends on Friday–Saturday:
{Tempo.add_workdays(~o"2026-06-18", 3), Tempo.count_workdays(~o"2026-06", :US)}June 19 2026 is a Friday — a workday in the United States, the weekend in Saudi Arabia:
{Tempo.weekend?(~o"2026-06-19"), Tempo.weekend?(~o"2026-06-19", :SA)}Recurrence — RFC 5545 RRULE
Recurring events parse from standard RRULE strings into the same interval model, so occurrences are just another set to do algebra on:
{:ok, standup} = Tempo.RRule.parse("FREQ=WEEKLY;BYDAY=TU;COUNT=4", from: ~o"2026-06-01")
{:ok, tuesdays} = Tempo.to_interval(standup)
tuesdaysAn unbounded recurrence (FREQ=WEEKLY with no COUNT/UNTIL) refuses to list its occurrences without a :within window — supply one and it expands exactly as far as you asked:
{:ok, weekly} = Tempo.RRule.parse("FREQ=WEEKLY;BYDAY=FR", from: ~o"2026-06-01")
{:ok, fridays} = Tempo.to_interval(weekly, within: ~o"2026-07")
Tempo.IntervalSet.count(fridays)Putting it together
Occurrences are just another set, so every predicate applies. Does June 9 fall on a standup day? Which standups land in the first half of the month?
{Tempo.overlaps?(~o"2026-06-09", tuesdays), Tempo.IntervalSet.covered?(tuesdays, ~o"2026-06-10")}{:ok, early_june} = Tempo.members_overlapping(tuesdays, ~o"2026-06-01/2026-06-15")
early_juneWhere next
- The scheduling guide adds floating-vs-zoned events, zone-rule-proof future dates, and bounded recurrence. Critical-path scheduling is in the companion package
tempo_network. - The workdays and weekends and holidays cookbooks cover observed-holiday rules.
- The iCal integration guide imports real calendars into this same algebra.