# A tour of Tempo

## Setup — required for every example

The cell below installs Tempo, pulls in Kino for rich output, and — critically — `import`s `Tempo.Sigils` so the `~o` sigil used throughout this notebook is in scope. Run it first; every subsequent cell depends on it. The import brings only `sigil_o/2` and `sigil_TEMPO/2` into the caller's namespace — no helper functions leak in.

```elixir
Mix.install([
  {:ex_tempo, "~> 1.6"},
  {:kino, "~> 0.14"}
])

import Tempo.Sigils
```

## The thesis — time is an interval, not an instant

Most languages split time into three scalar types — `Date`, `Time`, `DateTime` — and leave you to figure out which interval a scalar "really means." Is `~D[2026-06-15]` the instant of midnight, or the whole day? Your codebase probably has helpers like `end_of_day/1` because the type can't say.

Tempo takes the opposite stance. Every Tempo value *is* a bounded span on the time line. One type. Every operation — iteration, comparison, set algebra — is defined uniformly across every resolution and every calendar.

Start with the simplest example. A plain day:

```elixir
~o"2026-06-15"
```

Now see its bounds:

```elixir
{:ok, interval} = Tempo.to_interval(~o"2026-06-15")
Tempo.Interval.endpoints(interval)
```

The span is `[2026-06-15 00:00, 2026-06-16 00:00)` — half-open, so adjacent days concatenate cleanly (`[a, b) ++ [b, c) == [a, c)`). No off-by-one. No ambiguity.

The same rule applies at every resolution:

```elixir
for value <- [~o"2026", ~o"2026-06", ~o"2026-06-15", ~o"2026-06-15T10"] do
  {:ok, iv} = Tempo.to_interval(value)
  {from, to} = Tempo.Interval.endpoints(iv)
  %{input: inspect(value), from: inspect(from), to: inspect(to)}
end
|> Kino.DataTable.new()
```

A year implicitly spans one year. A month spans one month. An hour spans one hour. The next-finer unit sets the span — there's no guessing.

## Explaining a value

If you hand a Tempo value to a colleague who's never seen it, what does it *mean*? Tempo has a built-in explainer:

```elixir
Tempo.explain(~o"156X")
```

```elixir
Tempo.explain(~o"1984?/2004~")
```

For richer UI surfaces (HTML, coloured terminals) use `Tempo.Explain.explain/1` — it returns a `%Tempo.Explanation{}` with tagged parts that `to_string/1`, `to_ansi/1`, or `to_iodata/1` render differently:

```elixir
explanation = Tempo.Explain.explain(~o"156X")
explanation.parts
```

## Iteration

Every Tempo value is enumerable. The iteration unit is "the next-higher-resolution below what's stated" — a year yields months, a month yields days, etc.

```elixir
Enum.take(~o"2026Y", 12)
```

```elixir
Enum.take(~o"2026-06", 5)
```

Masks iterate over the values they represent. The 1560s:

```elixir
Enum.take(~o"156X", 10)
```

## Comparison and predicates

Tempo implements Allen's interval algebra: `Tempo.relation/2` names the precise relation (all thirteen, `:precedes` through `:preceded_by`), and the everyday questions have named predicates — `overlaps?`, `contains?`, `before?`, `after?`, `meets?`, `adjacent?`, `during?`, `within?`, `equal?`, `disjoint?`.

```elixir
a = ~o"2026-06-01/2026-06-15"
b = ~o"2026-06-10/2026-06-20"

%{
  "a contains 2026-06-10": Tempo.contains?(a, ~o"2026-06-10"),
  "a overlaps b": Tempo.overlaps?(a, b),
  "a before 2026-07": Tempo.before?(a, ~o"2026-07")
}
```

## Set operations

Once every value is a bounded interval, the set-theoretic operations follow naturally.

### Union — both windows kept distinct (member-preserving)

```elixir
{:ok, both} = Tempo.union(a, b)
Tempo.IntervalSet.to_list(both)
```

> The **union** of `a` and `b` keeps both intervals as distinct members — `union/2` is member-preserving so identity and metadata survive.

For the merged "one continuous window" form, coalesce explicitly:

```elixir
both
|> Tempo.IntervalSet.coalesce()
|> Tempo.IntervalSet.to_list()
```

> One continuous window — June 1 through June 20 — after coalescing.

### Intersection — the overlap between two windows

```elixir
{:ok, overlap} = Tempo.intersection(a, b)
Tempo.IntervalSet.to_list(overlap)
```

> The **intersection** of `a` and `b` is just the shared stretch — June 10 through June 15.

### Difference — free time around a lunch break

```elixir
work_day = ~o"2026-06-15T09/2026-06-15T17"
lunch    = ~o"2026-06-15T12/2026-06-15T13"

{:ok, free} = Tempo.difference(work_day, lunch)
Tempo.IntervalSet.to_list(free)
```

> The **workday minus lunch** is my **free** time — two intervals, 09:00-12:00 and 13:00-17:00.

Two free intervals: 09:00-12:00 and 13:00-17:00.

## Selecting sub-spans — `Tempo.select/2`

`Tempo.select/2` is a composition primitive: it narrows a base span by a **selector** and returns the matched sub-spans as an `IntervalSet`. The selector vocabulary covers territory-aware queries (via `Tempo.workdays/1` and `Tempo.weekend/1`), integer indices applied at the next-finer unit, projection of a Tempo or Interval onto a larger base, and arbitrary functions.

`Tempo.select/2` is a **pure function**. Locale-dependent constraints are constructed by `Tempo.workdays/1` and `Tempo.weekend/1` and composed in at the call site.

### Workdays of a month

```elixir
{:ok, workdays} = Tempo.select(~o"2026-06", Tempo.workdays(:US))
Tempo.IntervalSet.to_list(workdays) |> length()
```

> **Workdays** of **June 2026** in the **United States** are Monday through Friday — 22 day-resolution intervals.

### Pick specific days inside a month

```elixir
{:ok, paydays} = Tempo.select(~o"2026-06", [1, 15])
Tempo.IntervalSet.map(paydays, &Tempo.day/1)
```

> **Integer selectors** apply at the **next-finer unit** below the base's resolution — on a month base that's day.

### Project a date pattern onto a larger base

```elixir
{:ok, holidays} = Tempo.select(~o"2026", [~o"07-04", ~o"12-25"])
Tempo.IntervalSet.map(holidays, &{Tempo.month(&1), Tempo.day(&1)})
```

> **Project** the constraint months/days `07-04` and `12-25` onto the base year — July 4 and Dec 25 of 2026.

### Different territory, different weekend

```elixir
{:ok, us}    = Tempo.select(~o"2026-02", Tempo.weekend(:US))
{:ok, saudi} = Tempo.select(~o"2026-02", Tempo.weekend(:SA))

{
  Tempo.IntervalSet.map(us, &Tempo.day/1),
  Tempo.IntervalSet.map(saudi, &Tempo.day/1)
}
```

> The US weekend is Sat+Sun, the Saudi weekend is Fri+Sat. `Tempo.weekend/1` and `Tempo.workdays/1` accept a territory atom, a territory string, a locale string (`"ar-SA"`), or a `%Localize.LanguageTag{}`. With no argument they walk the ambient chain: `Application.get_env(:ex_tempo, :default_territory)` → `Localize.get_locale()`.

### Compose with set operations

```elixir
{: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)

Tempo.IntervalSet.to_list(available)
```

> **Workdays** of June **minus** my **vacation** yields the days I'm **available**. Because `select/3` returns an IntervalSet, it drops straight into `union/2`, `intersection/2`, `difference/2`, and `symmetric_difference/2`.

## Recurring events

Tempo implements the full RFC 5545 RRULE vocabulary. Everything from `FREQ=DAILY;COUNT=10` to `"4th Thursday of November, every year"` goes through the same interpreter.

### Every Monday for 10 weeks

```elixir
alias Tempo.RRule.{Rule, Expander}

rule = %Rule{freq: :week, interval: 1, byday: [{nil, 1}], count: 10}
{:ok, occurrences} = Expander.expand(rule, ~o"2026-06-01")

occurrences
|> Enum.map(fn iv ->
  start = Tempo.Interval.from(iv)
  {Tempo.year(start), Tempo.month(start), Tempo.day(start)}
end)
|> Kino.DataTable.new()
```

### Thanksgiving (4th Thursday of November)

```elixir
rule = %Rule{
  freq: :year,
  interval: 1,
  bymonth: [11],
  byday: [{4, 4}],
  count: 5
}

{:ok, thanksgivings} = Expander.expand(rule, ~o"2022-11-24")

thanksgivings
|> Enum.map(fn iv ->
  start = Tempo.Interval.from(iv)
  {Tempo.year(start), Tempo.month(start), Tempo.day(start)}
end)
```

### US Presidential Election Day

"Every four years, the first Tuesday after a Monday in November":

```elixir
rule = %Rule{
  freq: :year,
  interval: 4,
  bymonth: [11],
  byday: [{nil, 2}],
  bymonthday: [2, 3, 4, 5, 6, 7, 8],
  count: 5
}

{:ok, elections} = Expander.expand(rule, ~o"1996-11-05")

elections
|> Enum.map(fn iv ->
  start = Tempo.Interval.from(iv)
  {Tempo.year(start), Tempo.month(start), Tempo.day(start)}
end)
```

### Every Friday the 13th

```elixir
rule = %Rule{
  freq: :month,
  interval: 1,
  byday: [{nil, 5}],
  bymonthday: [13],
  count: 10
}

{:ok, f13s} = Expander.expand(rule, ~o"1998-02-13")

f13s
|> Enum.map(fn iv ->
  start = Tempo.Interval.from(iv)
  {Tempo.year(start), Tempo.month(start), Tempo.day(start)}
end)
```

### Last weekday of every month

```elixir
rule = %Rule{
  freq: :month,
  interval: 1,
  byday: [{nil, 1}, {nil, 2}, {nil, 3}, {nil, 4}, {nil, 5}],
  bysetpos: [-1],
  count: 6
}

{:ok, last_weekdays} = Expander.expand(rule, ~o"2026-06-01")

last_weekdays
|> Enum.map(fn iv ->
  start = Tempo.Interval.from(iv)
  {Tempo.year(start), Tempo.month(start), Tempo.day(start)}
end)
```

BYSETPOS applies last, picking the Nth element of each month's candidate set.

## Cross-calendar comparison

Tempo values are calendar-aware. The IXDTF `[u-ca=NAME]` suffix attaches a calendar to any value at parse time — Hebrew, Islamic, Persian, Buddhist, and the rest resolve to their `Calendrical.*` modules automatically:

```elixir
{:ok, hebrew} = Tempo.from_iso8601("5786-10-30[u-ca=hebrew]")
Tempo.overlaps?(hebrew, ~o"2026-06-15")
```

Tempo converts via `Date.convert!/2` internally, so the operation is calendar-neutral. See `Calendrical.supported_cldr_calendar_types/0` for the full list of recognised CLDR calendar identifiers.

## Cross-timezone comparison

```elixir
paris = Tempo.from_elixir(DateTime.new!(~D[2026-06-15], ~T[10:00:00], "Europe/Paris"))
utc_window = ~o"2026-06-15T07Z/2026-06-15T09Z"

Tempo.overlaps?(paris, utc_window)
```

Both values are *grounded* — the Paris time by its zone, the window by `Z`. Comparing a grounded value with a floating one (no zone or offset) raises `Tempo.FloatingTempoError` rather than silently assuming UTC; ground the floating side first.

Paris 10:00 CEST is UTC 08:00 — inside the UTC 07:00-09:00 window. Tempo projects to UTC via `Tzdata` per-call; the wall-clock representation is preserved on the struct.

## Archaeological and approximate dates

EDTF (ISO 8601-2) gives us qualifiers for vague historical references.

### "Sometime in the 1560s"

```elixir
Enum.take(~o"156X", 10)
```

### "Approximately 2022"

```elixir
value = ~o"2022~"
{:ok, iv} = Tempo.to_interval(value)

%{
  value: inspect(value),
  qualification: value.qualification,
  span: Tempo.Interval.endpoints(iv)
}
```

The qualifier decorates the value but doesn't shift its bounds — this is still the year 2022, just with a note that the author wasn't certain.

### Open-ended intervals

```elixir
[~o"1985/..", ~o"../2024", ~o"../.."]
|> Enum.map(fn iv ->
  {from, to} = Tempo.Interval.endpoints(iv)
  %{input: inspect(iv), from: inspect(from), to: inspect(to)}
end)
|> Kino.DataTable.new()
```

`:undefined` is the parsed shape for an open endpoint. Ranges and intervals with one open side are legal; both open (`../..`) is the "entire time line" identity.

## Non-contiguous mask expansion

The 15th of every month in 1985:

```elixir
{:ok, set} = Tempo.to_interval(~o"1985-XX-15")

set
|> Tempo.IntervalSet.map(&Tempo.Interval.endpoints/1)
|> Kino.DataTable.new()
```

Twelve distinct day-intervals. The masked `XX` month expands to each valid value (1..12); the concrete day-15 pins each resulting interval.

## Interval relations — Allen's algebra

`Tempo.relation/2` returns one of 13 Allen-algebra relations between two intervals:

```elixir
a = ~o"2026-06-01/2026-06-10"
b = ~o"2026-06-05/2026-06-15"

%{
  "a overlaps b":      Tempo.relation(a, b),
  "a meets next day":  Tempo.relation(~o"2026-06-15", ~o"2026-06-16"),
  "year contains day": Tempo.relation(~o"2026Y", ~o"2026-06-15")
}
```

Named predicates wrap the common checks — `within?`, `adjacent?`, `before?`, `after?`, `meets?`, `during?`:

```elixir
window = ~o"2026-06-15T09/2026-06-15T17"
candidate = ~o"2026-06-15T10/2026-06-15T11"

Tempo.within?(candidate, window)
```

## Duration predicates

Five predicates cover the interval-length comparison lattice:

```elixir
iv = ~o"2026-06-15T09/2026-06-15T10"

%{
  "duration":     Tempo.duration(iv),
  "≥ 1h":         Tempo.at_least?(iv, ~o"PT1H"),
  "≤ 1h":         Tempo.at_most?(iv, ~o"PT1H"),
  "exactly 1h":   Tempo.exactly?(iv, ~o"PT1H"),
  "> 30m":        Tempo.longer_than?(iv, ~o"PT30M"),
  "< 2h":         Tempo.shorter_than?(iv, ~o"PT2H")
}
```

## Putting it together — find mutual free time ≥ 1 hour

A scenario that used to be awkward: two colleagues' calendars plus my working hours. Which windows can host a 1-hour meeting?

```elixir
alice =
  Tempo.IntervalSet.new!([
    Tempo.Interval.new!(from: ~o"2026-06-15T10", to: ~o"2026-06-15T11", metadata: %{who: "Alice"}),
    Tempo.Interval.new!(from: ~o"2026-06-15T14", to: ~o"2026-06-15T15", metadata: %{who: "Alice"})
  ])

bob =
  Tempo.IntervalSet.new!([
    Tempo.Interval.new!(from: ~o"2026-06-15T11", to: ~o"2026-06-15T12", metadata: %{who: "Bob"}),
    Tempo.Interval.new!(from: ~o"2026-06-15T15:30", to: ~o"2026-06-15T16", metadata: %{who: "Bob"})
  ])

work_hours = ~o"2026-06-15T09/2026-06-15T17"

{:ok, alice_free} = Tempo.difference(work_hours, alice)
{:ok, bob_free}   = Tempo.difference(work_hours, bob)
{:ok, mutual}     = Tempo.intersection(alice_free, bob_free)

bookable = Tempo.IntervalSet.filter(mutual, &Tempo.at_least?(&1, ~o"PT1H"))

bookable
|> Tempo.IntervalSet.map(&Tempo.Interval.endpoints/1)
|> Kino.DataTable.new()
```

> Alice's free time is the **workday minus** her **busy** periods. Bob's is the same. **Mutual** free time is the **intersection** of theirs. **Bookable** windows are the mutual ones **at least an hour** long.

Three windows pass: 09:00-10:00, 12:00-14:00, and 16:00-17:00. The 15:00-15:30 gap (Alice free but Bob booked at 15:30) is correctly dropped.

### Alternative — "does this exact candidate fit somewhere?"

When you have preferred times in mind rather than discovering free windows, `Tempo.within?/2` reads the question naturally:

```elixir
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)

bookable |> Enum.map(&inspect/1)
```

> A **candidate** is **bookable** if **any** mutual-free window **contains** it.

Two of the three candidates fit inside a mutual-free window. The 11:00-12:00 one is rejected because Bob is busy then.

## What to read next

* [Tempo cookbook](https://hexdocs.pm/ex_tempo/cookbook.html) — 65+ task-oriented recipes
* [Set operations guide](https://hexdocs.pm/ex_tempo/set-operations.html)
* [iCalendar integration](https://hexdocs.pm/ex_tempo/ical-integration.html) — import `.ics` with full RRULE/RDATE/EXDATE
* [ISO 8601 conformance](https://hexdocs.pm/ex_tempo/iso8601-conformance.html) — what parts of the standard are covered

Happy iterating.
