Task-oriented recipes for common time-and-calendar problems. Each recipe states the problem, shows the solution, and explains why it works. Copy, adapt, iterate.
Setup — required for every example
Every code example in this guide uses the ~o sigil from Tempo.Sigils. Before running any of them — in iex, a script, or a module — you must bring the sigil into scope:
import Tempo.SigilsThe import adds only sigil_o/2 and sigil_TEMPO/2 to the caller's namespace; no helper functions leak in. In iex, paste it once and every subsequent snippet runs against it.
Contents
- Parsing and construction
- Exploring a value
- Iteration
- Comparison and predicates
- Set operations
- Selecting sub-spans with
Tempo.select/3 - Recurring events (RRULE)
- iCalendar import
- Cross-calendar and cross-timezone
- Archaeological / approximate dates
- Real-world scenarios
- Famous moments in time
1. Parsing and construction
How do I parse an ISO 8601 date?
iex> ~o"2026-06-15"
~o"2026Y6M15D"The sigil output uses ISO 8601-2's unit-suffix form (Y/M/D/H/…) which disambiguates months from minutes.
How do I convert an Elixir Date, Time, NaiveDateTime, or DateTime to a Tempo?
iex> Tempo.from_elixir(~D[2026-06-15])
~o"2026Y6M15D"
iex> Tempo.from_elixir(~U[2026-06-15 10:30:00Z])
# Hour-resolution datetime with zone_id "Etc/UTC"from_elixir/2 accepts a :resolution option when you want to coarsen or extend the inferred one.
How do I express a duration?
iex> ~o"P1Y6M"
~o"P1Y6M"
iex> ~o"P1Y6M".time
[year: 1, month: 6]How do I express an interval between two dates?
iex> ~o"2026-06-01/2026-06-30"
~o"2026Y6M1D/2026Y6M30D"Tempo uses the half-open [from, to) convention. Adjacent intervals concatenate cleanly.
How do I say "approximately 2022" or "uncertain 1984"?
iex> ~o"2022~"
# Approximate 2022
iex> ~o"1984?"
# Uncertain 1984
iex> ~o"1984%"
# Both uncertain and approximate (equivalent to ISO 8601-2 `%`)The qualification is stored on the value's :qualification field; the span stays the same calendar year.
2. Exploring a value
How do I see what a value represents?
iex> Tempo.explain(~o"156X")
"""
A masked year spanning the 1560s.
Span: [1560-01-01, 1570-01-01).
Iterates at :month granularity.
Materialise as an interval with `Tempo.to_interval/1`.
"""Tempo.explain/1 returns a plain string. For structured output (HTML visualiser, terminal colour) call Tempo.Explain.explain/1 which returns a %Tempo.Explanation{} with tagged parts — Tempo.Explain.to_string/1, to_ansi/1, and to_iodata/1 format it for different surfaces.
How do I see the concrete bounds of an implicit interval?
iex> {:ok, iv} = Tempo.to_interval(~o"2026-06")
iex> {from, to} = Tempo.Interval.endpoints(iv)
iex> {Tempo.year(from), Tempo.month(from), Tempo.day(from), Tempo.year(to), Tempo.month(to), Tempo.day(to)}
{2026, 6, 1, 2026, 7, 1}Every Tempo value is an interval. to_interval/1 materialises the implicit span into explicit from/to endpoints.
How do I check a value's resolution?
iex> Tempo.resolution(~o"2026-06-15")
{:day, 1}3. Iteration
How do I list every month of a year?
iex> Enum.take(~o"2026Y", 12)
[~o"2026Y1M", ~o"2026Y2M", ~o"2026Y3M", ~o"2026Y4M", ~o"2026Y5M",
~o"2026Y6M", ~o"2026Y7M", ~o"2026Y8M", ~o"2026Y9M", ~o"2026Y10M",
~o"2026Y11M", ~o"2026Y12M"]A year iterates at month granularity — the next-finer unit below what's specified.
How do I list every day of a month?
iex> Enum.take(~o"2026-06", 30) |> length()
30How do I list every year in the 1560s?
iex> Enum.take(~o"156X", 5)
[~o"1560Y", ~o"1561Y", ~o"1562Y", ~o"1563Y", ~o"1564Y"]The X mask is interpreted as "any digit." Enumeration walks the concrete years the mask represents.
How do I step by a non-default unit?
Wrap the value in an explicit interval with the resolution you want:
iex> interval = ~o"2026-06-01/2026-07-01"
# Iterates at day resolution (the boundaries' resolution).
iex> Enum.take(interval, 3)
# First three days of June4. Comparison and predicates
How do I check if a date is in an interval?
iex> Tempo.contains?(~o"2026Y", ~o"2026-06-15")
trueHow do I check if two intervals overlap?
iex> a = ~o"2026-06-01/2026-06-15"
iex> b = ~o"2026-06-10/2026-06-20"
iex> Tempo.overlaps?(a, b)
trueWhat's the full set of relationships between two intervals?
Tempo implements Allen's interval algebra — every pair of bounded intervals stands in exactly one of 13 relations. Tempo.relation/2 returns the atom:
iex> a = ~o"2026-06-01/2026-06-10"
iex> b = ~o"2026-06-05/2026-06-15"
iex> Tempo.relation(a, b)
:overlaps
iex> Tempo.relation(~o"2026-06-15", ~o"2026-06-16")
:meetsNamed predicates cover the common one-shot checks:
| Predicate | Maps to |
|---|---|
Tempo.before?(a, b) | :precedes — ends with a gap before b |
Tempo.after?(a, b) | :preceded_by |
Tempo.meets?(a, b) | :meets — ends exactly at b's start |
Tempo.adjacent?(a, b) | :meets | :met_by — touches, no gap |
Tempo.during?(a, b) | :during — strictly inside |
Tempo.within?(a, b) | :equals | :starts | :during | :finishes — fits inside, inclusive |
Tempo.within?/2 is the canonical "does this fit inside that window?" predicate:
iex> candidate = ~o"2026-06-15T10/2026-06-15T11"
iex> window = ~o"2026-06-15T09/2026-06-15T17"
iex> Tempo.within?(candidate, window)
trueFor set-level questions across two multi-member IntervalSets, use Tempo.IntervalSet.relation_matrix/2 which returns every pairwise relation.
How long is an interval?
iex> iv = ~o"2026-06-15T09/2026-06-15T11"
iex> Tempo.duration(iv)
~o"PT7200S"Returns :infinity when one or both endpoints are :undefined.
How do I check an interval's length against a duration?
Five predicates cover the comparison lattice:
iv = ~o"2026-06-15T09/2026-06-15T10"
Tempo.at_least?(iv, ~o"PT1H") # true — length ≥ 1h
Tempo.exactly?(iv, ~o"PT1H") # true — length == 1h
Tempo.at_most?(iv, ~o"PT1H") # true — length ≤ 1h
Tempo.longer_than?(iv, ~o"PT30M") # true — strict >
Tempo.shorter_than?(iv, ~o"PT2H") # true — strict <How do I compare two values across different calendars?
iex> {:ok, hebrew} = Tempo.from_iso8601("5786-10-30[u-ca=hebrew]")
iex> hebrew.calendar
Calendrical.Hebrew
iex> Tempo.overlaps?(hebrew, ~o"2026-06-15")
trueThe IXDTF [u-ca=NAME] suffix swaps the value's calendar to the corresponding Calendrical.* module — hebrew, islamic-umalqura, persian, buddhist, and the rest. See Calendrical.supported_cldr_calendar_types/0 for the full list. Cross-calendar comparisons then convert operands to a shared reference automatically.
5. Set operations
How do I merge two overlapping intervals into one?
iex> a = ~o"2026-06-01/2026-06-15"
iex> b = ~o"2026-06-10/2026-06-20"
iex> {:ok, merged} = Tempo.union(a, b)
iex> Tempo.IntervalSet.count(merged)
1
# The merged span is June 1 .. June 20.How do I find the overlap between two intervals?
iex> {:ok, overlap} = Tempo.intersection(a, b)
iex> [span] = Tempo.IntervalSet.to_list(overlap)
iex> {from, to} = Tempo.Interval.endpoints(span)
iex> {Tempo.day(from), Tempo.day(to)}
{10, 15}How do I subtract a busy period from a free window?
work_day = ~o"2026-06-15T09/2026-06-15T17"
lunch = ~o"2026-06-15T12/2026-06-15T13"
{:ok, free} = Tempo.difference(work_day, lunch)The workday minus lunch is my free time — two intervals, 09:00-12:00 and 13:00-17:00.
How do I compose free/busy across a real schedule?
{:ok, schedule} = Tempo.ICal.from_ical_file("~/work.ics")
work = ~o"2026-06-15T09/2026-06-15T17"
{:ok, free} = Tempo.difference(work, schedule)Work minus my schedule gives me free time that day.
Result intervals carry the event metadata from the subtracted schedule where relevant, so you can trace each "busy" segment back to the meeting that caused it.
How do I get the symmetric difference (everything in A or B but not both)?
iex> {:ok, set} = Tempo.symmetric_difference(a, b)6. Selecting sub-spans with Tempo.select/2
Tempo.select/2 narrows a base span (a Tempo, an Interval, or an IntervalSet) by a selector and returns the matched spans as a {:ok, %Tempo.IntervalSet{}} tuple. The same vocabulary covers territory-aware queries (via Tempo.workdays/1 and Tempo.weekend/1), integer indices at the next-finer unit, and projection of a Tempo or Interval onto a larger base.
Tempo.select/2 is a pure function — no ambient territory, no hidden options. Locale-dependent constraints are constructed by Tempo.workdays/1 and Tempo.weekend/1 (which resolve the territory once at construction time) and composed in at the call site.
How do I select the workdays of a month?
iex> {:ok, workdays} = Tempo.select(~o"2026-06", Tempo.workdays(:US))
iex> Tempo.IntervalSet.count(workdays)
22Workdays of June 2026 in the United States are Monday through Friday — 22 day-resolution intervals.
How do I pick specific days inside a month?
iex> {:ok, paydays} = Tempo.select(~o"2026-06", [1, 15])
iex> Tempo.IntervalSet.map(paydays, &Tempo.day/1)
[1, 15]Integer selectors apply at the next-finer unit below the base's resolution — on a month base that's day, on a year base it's month. A
Rangeworks too:Tempo.select(~o"2026-06", 10..15).
How do I project a date pattern onto a larger base?
iex> {:ok, set} = Tempo.select(~o"2026", ~o"12-25")
iex> [xmas] = Tempo.IntervalSet.to_list(set)
iex> {Tempo.year(xmas), Tempo.month(xmas), Tempo.day(xmas)}
{2026, 12, 25}Project the constraint
12-25onto the base year — Dec 25 of 2026. A list of constraints works the same:Tempo.select(~o"2026", [~o"07-04", ~o"12-25"])yields both US holidays.
How do I select a different territory's weekend?
iex> {:ok, sa_weekend} = Tempo.select(~o"2026-02", Tempo.weekend(:SA))
iex> Tempo.IntervalSet.map(sa_weekend, &Tempo.day/1)
[6, 7, 13, 14, 20, 21, 27, 28]Saudi Arabia's weekend is Friday + Saturday.
Tempo.weekend/1andTempo.workdays/1accept a territory atom (:SA), a territory string ("SA","sazzzz"), a locale string ("ar-SA"), or a%Localize.LanguageTag{}. With no argument they use the ambient resolution chain:Application.get_env(:ex_tempo, :default_territory)→Localize.get_locale().
Pass a full locale when you have one rather than the territory:
iex> {:ok, sa_weekend} = Tempo.select(~o"2026-02", Tempo.weekend("ar-SA"))
iex> Tempo.IntervalSet.map(sa_weekend, &Tempo.day/1)
[6, 7, 13, 14, 20, 21, 27, 28]How do I compose select with the set operations?
{:ok, june_workdays} = Tempo.select(~o"2026-06", Tempo.workdays(:US))
{:ok, vacation} = Tempo.to_interval_set(~o"2026-06-15/2026-06-20")
{:ok, available} = Tempo.difference(june_workdays, vacation)US workdays of June minus my vacation yields the workdays I'm available. Because
select/2returns an IntervalSet, it drops straight intounion/2,intersection/2,difference/2, andsymmetric_difference/2.
How do I use a function as a selector?
holidays = fn _base -> [~o"01-01", ~o"07-04", ~o"12-25"] end
{:ok, set} = Tempo.select(~o"2026", holidays)Function selectors receive the base and return any selector shape (list of Tempos here). This is the extension point for user-defined holiday calendars, business rules, or anything else you want to compute from the base.
How do I pick "the last X of Y"?
ISO 8601-2 §4.4.1 allows any component to be negative, meaning "count from the end of the containing unit". Negative components flow straight through Tempo.select/2 — no string arithmetic, no days_in_month/2 calls, no calendar branches:
iex> {:ok, last_month} = Tempo.select(~o"2026", ~o"-1M")
iex> Tempo.month(Tempo.IntervalSet.to_list(last_month) |> hd())
12
iex> {:ok, last_day_of_feb} = Tempo.select(~o"2024-02", ~o"-1D")
iex> Tempo.day(Tempo.IntervalSet.to_list(last_day_of_feb) |> hd())
29
iex> {:ok, last_day_of_feb} = Tempo.select(~o"2026-02", ~o"-1D")
iex> Tempo.day(Tempo.IntervalSet.to_list(last_day_of_feb) |> hd())
28
-1Mon a year base is the last month.-1Don a month base is the last day of that month — leap-aware (Feb 29 in 2024, Feb 28 in 2026).-1Won a year base is the last ISO week (52 or 53 depending on year).
The resolution is axis-aware: -1W on a month base gives the last week-of-month (4 or 5), while on a year base it gives the last ISO week-of-year (52 or 53). -1O (ordinal) on a year base is the year's last day; -1K is the week's last day-of-week.
Time-of-day units work the same way. ~o"-1H" is hour 23, ~o"T-1M" is minute 59, ~o"T-1S" is second 59:
iex> {:ok, last_hour} = Tempo.select(~o"2026-06-15", ~o"-1H")
iex> last_hour |> Tempo.IntervalSet.to_list() |> hd() |> Tempo.hour()
23
iex> {:ok, last_minute} = Tempo.select(~o"2026-06-15T14", ~o"T-1M")
iex> last_minute |> Tempo.IntervalSet.to_list() |> hd() |> Tempo.minute()
59
~o"-1M"is always month (last month of year). Use~o"T-1M"— with theTtime designator — to select minute-of-hour. The bare-formMbelongs to the date axis; theT-prefixed form belongs to time-of-day.
Negative components compose with the rest of the selector vocabulary — Tempo.select(~o"2026", [~o"-1D", ~o"12-25"]) projects both "last day of year" and "Christmas" onto 2026, yielding Dec 25 and Dec 31 as separate members.
See Tempo.Select for the full selector vocabulary.
7. Recurring events (RRULE)
How do I express "every Monday for 10 weeks"?
iex> rule = %Tempo.RRule.Rule{freq: :week, interval: 1, byday: [{nil, 1}], count: 10}
iex> {:ok, occurrences} = Tempo.RRule.Expander.expand(rule, ~o"2026-06-01")
iex> length(occurrences)
10How do I parse an RRULE string?
iex> {:ok, ast} = Tempo.RRule.parse("FREQ=WEEKLY;BYDAY=MO;COUNT=10", from: ~o"2026-06-01")
iex> {:ok, set} = Tempo.to_interval(ast)
iex> Tempo.IntervalSet.count(set)
10How do I express "the 4th Thursday of November" (Thanksgiving)?
iex> rule = %Tempo.RRule.Rule{
...> freq: :year, interval: 1,
...> bymonth: [11], byday: [{4, 4}],
...> count: 5
...> }
iex> {:ok, occurrences} = Tempo.RRule.Expander.expand(rule, ~o"2022-11-24")
iex> Enum.map(occurrences, &Tempo.day(Tempo.Interval.from(&1)))
[24, 23, 28, 27, 26]Positive ordinals count from the start of the period; negatives count from the end (-1FR = last Friday).
How do I express "every Friday the 13th"?
iex> rule = %Tempo.RRule.Rule{
...> freq: :month, interval: 1,
...> byday: [{nil, 5}], bymonthday: [13],
...> count: 10
...> }
iex> {:ok, occurrences} = Tempo.RRule.Expander.expand(rule, ~o"1998-02-13")When BYMONTHDAY is co-present, BYDAY becomes a filter (per RFC Note 1) — BYMONTHDAY=13 picks day 13 of each month, then BYDAY=FR keeps only the Fridays.
How do I express US Presidential Election Day?
"Every four years, the first Tuesday after a Monday in November":
iex> rule = %Tempo.RRule.Rule{
...> freq: :year, interval: 4,
...> bymonth: [11], byday: [{nil, 2}],
...> bymonthday: [2, 3, 4, 5, 6, 7, 8],
...> count: 3
...> }
iex> {:ok, occurrences} = Tempo.RRule.Expander.expand(rule, ~o"1996-11-05")
iex> Enum.map(occurrences, fn iv ->
...> start = Tempo.Interval.from(iv)
...> {Tempo.year(start), Tempo.day(start)}
...> end)
[{1996, 5}, {2000, 7}, {2004, 2}]How do I express "last weekday of every month"?
iex> rule = %Tempo.RRule.Rule{
...> freq: :month, interval: 1,
...> byday: [{nil, 1}, {nil, 2}, {nil, 3}, {nil, 4}, {nil, 5}],
...> bysetpos: [-1],
...> count: 3
...> }
iex> {:ok, occurrences} = Tempo.RRule.Expander.expand(rule, ~o"2026-06-01")BYDAY=MO..FR expands each month to all weekdays; BYSETPOS=-1 picks the last.
How do I handle an unbounded rule?
Supply :bound:
iex> rule = %Tempo.RRule.Rule{freq: :day, interval: 1} # No COUNT, no UNTIL
iex> {:ok, occurrences} = Tempo.RRule.Expander.expand(rule, ~o"2026-06-01", bound: ~o"2026-07-01")
iex> length(occurrences)
308. iCalendar import
How do I import an .ics file?
iex> {:ok, schedule} = Tempo.ICal.from_ical_file("~/work.ics")
iex> Tempo.IntervalSet.count(schedule)
# One interval per VEVENT (or per materialised recurrence occurrence).Each event becomes a %Tempo.Interval{} with full metadata (summary, location, attendees, …) attached to :metadata.
How do I import an .ics that contains recurring events?
Pass a :bound so unbounded recurrences terminate:
iex> {:ok, schedule} = Tempo.ICal.from_ical(ics, bound: ~o"2026-04-01/2026-07-01")Every RRULE part (including BY-rules, BYSETPOS, WKST, RDATE, EXDATE) materialises correctly — one %Tempo.Interval{} per occurrence carrying the event's metadata.
How do I find when a specific attendee is in a meeting?
{:ok, schedule} = Tempo.ICal.from_ical(ics)
ada_meetings =
schedule
|> Tempo.IntervalSet.to_list()
|> Enum.filter(fn meeting ->
"ada@example.com" in (meeting.metadata[:attendees] || [])
end)Ada's meetings are every event in the schedule whose attendees include her.
Metadata rides through any downstream set operation — after intersection/difference/union, you can still trace each result fragment to its originating event.
9. Cross-calendar and cross-timezone
How do I compare a Hebrew date to a Gregorian one?
hebrew = Tempo.new!(year: 5786, month: 10, day: 30, calendar: Calendrical.Hebrew)
gregorian = ~o"2026-06-15"
Tempo.overlaps?(hebrew, gregorian)
#=> trueThe Hebrew date 5786-10-30 overlaps the Gregorian date 2026-06-15 — they're the same day.
How do I compare across timezones?
paris = Tempo.from_elixir(DateTime.new!(~D[2026-06-15], ~T[10:00:00], "Europe/Paris"))
utc_window = ~o"2026-06-15T07/2026-06-15T09"
Tempo.overlaps?(paris, utc_window)
#=> trueParis 10:00 CEST overlaps the UTC 07:00-09:00 window — it projects to UTC 08:00, which is inside.
Tempo projects to UTC via Tzdata for cross-zone comparisons. The wall-clock representation on the struct is preserved; the projection happens per-call.
How do I convert a Tempo to a specific calendar?
iex> Tempo.to_calendar(~o"2026-06-15", Calendrical.Hebrew)
# Returns {:ok, %Tempo{...calendar: Calendrical.Hebrew}}10. Archaeological / approximate dates
How do I say "sometime in the 1560s"?
iex> ~o"156X"
# Decade mask — spans 1560-01-01 .. 1570-01-01.How do I say "the 15th of every month in 1985"?
iex> {:ok, set} = Tempo.to_interval(~o"1985-XX-15")
iex> Tempo.IntervalSet.count(set)
12A non-contiguous mask (masked month, concrete day) expands to one interval per valid month.
How do I express an open-ended interval?
iex> ~o"1985/.."
# From 1985 onward, no end.
iex> ~o"../2024"
# No start, ending 2024.
iex> ~o"../.."
# Fully open.How do I attach a qualifier to a single endpoint?
iex> ~o"1984?/2004~"
# Uncertain lower bound, approximate upper bound.Each endpoint carries its own :qualification in addition to any expression-level one.
11. Real-world scenarios
Find every Friday the 13th this century
friday_the_13th = %Tempo.RRule.Rule{
freq: :month,
interval: 1,
byday: [{nil, 5}],
bymonthday: [13]
}
century = ~o"2000-01-01/2100-01-01"
{:ok, occurrences} =
Tempo.RRule.Expander.expand(friday_the_13th, ~o"2000-01-01", bound: century)Friday the 13th is a monthly rule — Fridays whose day-of-month is 13. Expanding the rule across the century gives every occurrence.
Find when two people are both free for at least 1 hour
{:ok, ada} = Tempo.ICal.from_ical_file("~/ada.ics")
{:ok, grace} = Tempo.ICal.from_ical_file("~/grace.ics")
work = ~o"2026-06-15T09/2026-06-15T17"
{:ok, ada_free} = Tempo.difference(work, ada)
{:ok, grace_free} = Tempo.difference(work, grace)
{:ok, mutual} = Tempo.intersection(ada_free, grace_free)
slots =
mutual
|> Tempo.IntervalSet.to_list()
|> Enum.filter(&Tempo.at_least?(&1, ~o"PT1H"))Ada's free time is the workday minus her busy periods. Grace's is the same. Mutual free time is the intersection of theirs. Slots are the mutual windows at least an hour long.
Which of these candidate meeting times can I book?
candidates = [
~o"2026-06-15T09/2026-06-15T10",
~o"2026-06-15T11/2026-06-15T12",
~o"2026-06-15T16/2026-06-15T17"
]
bookable =
Enum.filter(candidates, fn candidate ->
Enum.any?(Tempo.IntervalSet.to_list(mutual), &Tempo.within?(candidate, &1))
end)A candidate is bookable if any mutual free window contains it.
How do I check if a dig layer overlaps a historical period?
dig_layer = ~o"1520/1590"
ming_period = ~o"1368/1644"
Tempo.overlaps?(dig_layer, ming_period)
#=> trueThe dig layer overlaps the Ming period — the site was in use during the dynasty.
How do I find free time across multiple schedules and timezones?
{:ok, ny} = Tempo.ICal.from_ical_file("~/cal_ny.ics")
{:ok, london} = Tempo.ICal.from_ical_file("~/cal_london.ics")
work = ~o"2026-06-15T09/2026-06-15T17"
{:ok, ny_free} = Tempo.difference(work, ny)
{:ok, free} = Tempo.difference(ny_free, london)Work minus New York's busy times gives one person's free window; that minus London's busy times gives the cross-timezone free slots. Each
difference/2call projects to UTC internally, so wall-clock mismatches across zones resolve correctly.
How do I round a datetime to the nearest hour?
iex> Tempo.at_resolution(~o"2026-06-15T10:37:42", :hour)
~o"2026Y6M15DT10H"at_resolution/2 is the single entry point for normalising to a target unit — coarser uses trunc/2, finer uses extend_resolution/2.
How do I generate a list of business days in a month?
iex> {:ok, workdays} = Tempo.select(~o"2026-06", Tempo.workdays(:US))
iex> Tempo.IntervalSet.count(workdays)
22Workdays of June 2026 are Monday through Friday — 22 day-resolution intervals, locale-aware via
Localize.Calendar. See §6 for the full selector vocabulary and territory-resolution chain.
An RRULE equivalent is available when you need the full rule machinery (byday counts, bymonth filters, intervals greater than 1):
weekdays = %Tempo.RRule.Rule{
freq: :day,
interval: 1,
byday: [{nil, 1}, {nil, 2}, {nil, 3}, {nil, 4}, {nil, 5}]
}
{:ok, days} = Tempo.RRule.Expander.expand(weekdays, ~o"2026-06-01", bound: ~o"2026-06")Every free minute in a month
{:ok, schedule} = Tempo.ICal.from_ical(ics, bound: ~o"2026-06")
month = ~o"2026-06"
{:ok, free} = Tempo.difference(month, schedule)The month of June minus my schedule is my free time that month.
12. Famous moments in time
A small collection of historically awkward dates — the kind that break naive date libraries. Each recipe demonstrates a specific Tempo capability against a real artefact of history.
The Ides of March, 44 BCE
iex> {:ok, ides} = Tempo.from_iso8601("-0043-03-15")
iex> {Tempo.year(ides), Tempo.month(ides), Tempo.day(ides)}
{-43, 3, 15}ISO 8601 uses astronomical year numbering — 1 BCE is year 0, 2 BCE is year -1, and so on. The Ides of March in 44 BCE is therefore year -43. Tempo parses this without fuss; negative years are first-class.
The 1560s as an iterable decade
iex> decade = ~o"156X"
iex> Enum.to_list(decade) |> Enum.map(&Tempo.year/1)
[1560, 1561, 1562, 1563, 1564, 1565, 1566, 1567, 1568, 1569]
~o"156X"is an ISO 8601-2 masked year — "some year in the 1560s." It's both a bounded span (the full decade) and an enumerable sequence of 10 year-values. Archaeological records and historical citations use this form routinely; Tempo gives it a first-class type.
A leap second — detected, never represented as a value
iex> iv = ~o"2016-12-31T23:59:00Z/2017-01-01T00:01:00Z"
iex> Tempo.Interval.spans_leap_second?(iv)
true
iex> Tempo.Interval.duration(iv)
~o"PT120S"
iex> Tempo.Interval.duration(iv, leap_seconds: true)
~o"PT121S"At the end of 2016 UTC, a leap second was inserted — the minute 23:59 had 61 seconds, numbered 00 through 60. Tempo rejects
23:59:60as a value (to stay compatible withTime,DateTime, andCalendar.ISOin Elixir/OTP — none of which represent leap seconds). Instead, Tempo exposes leap-second information as interval metadata viaTempo.Interval.spans_leap_second?/1,leap_seconds_spanned/1, and theleap_seconds: trueoption onduration/2. Scientific and financial pipelines that need exact elapsed time get a clean API; everyone else gets stdlib interop for free. SeeTempo.LeapSeconds.dates/0for the 27 IERS-announced insertions.
A daylight-saving gap — the hour that never was
iex> Tempo.from_iso8601("2024-03-10T02:30:00[America/New_York]")
{:error,
"Wall time 2024-03-10T02:30:00 does not exist in \"America/New_York\" — it falls inside a daylight-saving or zone-transition gap."}At 02:00 local time on the second Sunday of March, US clocks jump to 03:00 — the hour 02:00–03:00 never exists. Tempo consults Tzdata at parse time and rejects wall times inside the gap, so downstream operations never encounter a phantom instant. Fall-back ambiguity (the repeated hour in November) is accepted by default — callers can disambiguate with an explicit offset.
Samoa skipping the international date line, 2011
iex> Tempo.from_iso8601("2011-09-24T12:00:00[Pacific/Apia]")
{:error,
"Wall time 2011-09-24T12:00:00 does not exist in \"Pacific/Apia\" — it falls inside a daylight-saving or zone-transition gap."}In 2011, Samoa shifted from east of the international date line to west of it — their timeline skipped forward 25 hours. Tempo consults Tzdata for the exact gap boundaries. (Current IANA data has the gap spanning Sep 24 03:00 → Sep 25 04:00 local, 25 hours; the news coverage at the time described the shift as end-of-December 2011. Wherever IANA places the transition, Tempo uses it as authoritative.)
Julian vs Gregorian — the same nominal date, different calendars
iex> {:ok, julian} = Tempo.from_iso8601("1582-01-01", Calendrical.Julian)
iex> {:ok, gregorian} = Tempo.from_iso8601("1582-01-01[u-ca=gregory]")
iex> Tempo.overlaps?(julian, gregorian)
false1 January 1582 under the Julian calendar and 1 January 1582 under the Gregorian calendar are not the same real day — they're 10 days apart because of the Julian-to-Gregorian drift. Tempo comparisons are calendar-aware: same nominal components, different calendar, different underlying instant. The answer is
false.
Allen's interval algebra
iex> Tempo.relation(~o"2022-06", ~o"2022-07")
:meets
iex> Tempo.relation(~o"2022-06", ~o"2022-06-15")
:contains
iex> Tempo.relation(~o"2022-06", ~o"2023-06")
:precedesTwo intervals relate in one of 13 named ways — Allen's interval algebra. June meets July (touches at the boundary with no gap or overlap). June 2022 contains June 15 2022. June 2022 precedes June 2023. The relation is always exact; no equality-tolerance bikeshedding.
Related reading
- When to use Tempo — a short decision guide on choosing between Tempo and the Elixir standard library.
- Scheduling — bounded enumeration, wall-clock-vs-UTC authority, floating vs zoned events, and how future dates survive Tzdata rule changes.
- Working with workdays and weekends — business-day queries (N days from today, next workday, workdays between two dates) built from
Tempo.workdays/1and set algebra. - Holidays — planning with a real holiday calendar — fetch an ICS holiday feed, parse it with
Tempo.ICal.from_ical/1, and compose it withTempo.workdays/1for territory-aware scheduling. - Falsehoods programmers believe about time — the ten most impactful wrong assumptions, each with the Tempo idiom that makes the right behaviour automatic.
- ISO 8601 conformance — what's supported from the standard.
- Enumeration semantics — how iteration works across Tempo values.
- Set operations — union, intersection, complement, difference.
- iCalendar integration — full
.icsimport with RRULE/RDATE/EXDATE. - Shared AST for ISO 8601 and RRULE — the internal representation.