Tempo treats a calendar as data, not a built-in assumption. Comparison, duration, iteration, and materialisation all route through the value's calendar — so a custom human calendar is a first-class citizen alongside Gregorian, Hebrew, or Persian. You build the calendar once with Calendrical, and every Tempo operation just works on values that carry it.
This is the time-granularity idea made concrete: a granularity is a calendar. A fiscal quarter, a retail week, an academic term are all granules of a calendar whose year happens not to start on the 1st of January.
A fiscal year
The US federal fiscal year runs October → September. Calendrical ships the fiscal calendars; Tempo consumes one directly:
{:ok, us_fiscal} = Calendrical.FiscalYear.calendar_for(:US)
{:ok, fiscal_2026} = Tempo.from_iso8601("2026", us_fiscal)
{:ok, fiscal_months} = Tempo.to_interval(fiscal_2026)
Enum.count(fiscal_months) #=> 12
fiscal_months |> Enum.at(0) |> Tempo.to_iso8601 #=> "2026Y1M""Fiscal year 2026 is twelve fiscal months; iterating it walks them in order."
Because every comparison projects through the calendar, a fiscal date and a Gregorian date sit on the same timeline — no manual conversion:
fiscal_q1_start = Tempo.from_iso8601!("2026-01-01", us_fiscal)
Tempo.relation(fiscal_q1_start, ~o"2025-10-01") #=> :equals
Tempo.quarter_of_year(Tempo.from_iso8601!("2026-11-15", us_fiscal)) #=> 4"US fiscal year 2026 begins on the 1st of October, 2025, and its eleventh fiscal month falls in the fourth fiscal quarter."
A retail 4-4-5 calendar
Retailers count time in weeks, grouped 4-4-5 into thirteen-week quarters. Build one with Calendrical.new/3 and hand it to Tempo:
{:ok, retail} = Calendrical.new(MyApp.Retail, :week, weeks_in_month: [4, 4, 5])
{:ok, retail_2026} = Tempo.from_iso8601("2026", retail)
{:ok, retail_weeks} = Tempo.to_interval(retail_2026)
Enum.count(retail_weeks) #=> 52
retail_weeks |> Enum.at(0) |> Tempo.to_iso8601 #=> "2026Y1W""The 2026 retail year is fifty-two retail weeks; iterating it walks week by week."
The weeks_in_month: [4, 4, 5] layout is the only thing that distinguishes this calendar from ISO weeks — everything else (iteration, comparison, duration) is inherited.
An academic year
An academic year is just a year that starts in a different month. A September-start calendar is a month-based calendar with a month_of_year offset:
{:ok, academic} = Calendrical.new(MyApp.Academic, :month, month_of_year: 9)
Tempo.relation(Tempo.from_iso8601!("2026-01-01", academic), ~o"2025-09-01") #=> :equals"Academic year 2026 begins on the 1st of September, 2025."
What ties them together
Each of these is a granularity lattice in miniature: a fiscal day groups into a fiscal month groups into a fiscal quarter groups into a fiscal year. Tempo reads that nesting off the calendar's own period structure, so to_interval/1 and iteration yield the calendar's natural granules rather than Gregorian ones — the finer-than / groups-into relations of time-granularity theory realised as calendar arithmetic.
What isn't here yet
Two families of human "calendar" are epoch-anchored cycles rather than year-structured, and Calendrical does not yet build them, so Tempo cannot consume them:
Sprints and pay periods — a fixed N-week cycle counted from an epoch (2-week sprints from a start date, biweekly pay periods) has no year boundary to hang on
Calendrical.Config's year-anchored fields.Irregular academic terms — Fall/Spring/Summer with bespoke, non-uniform boundaries need a labelled calendar, not a month offset.
Both are upstream Calendrical enhancements (an epoch-anchored periodic builder and a labelled builder). A small set of period-query conveniences on the Tempo side — "which fiscal quarter contains this date" as an interval, "the 3rd business day of Q2" — are also planned. Until then, the year-structured calendars above are fully supported.