Tempo.Select (Tempo v1.4.0)

Copy Markdown View Source

Narrow a Tempo span by a selector — the composition primitive for "workdays of June", "the 15th of every month", "every Dec 25 in the next decade", and user-supplied holidays.

Tempo.select(~o"2026-06", Tempo.workdays(:US))  # workdays of June — locale-aware
Tempo.select(~o"2026-06", Tempo.weekend(:US))   # weekend days of June
Tempo.select(~o"2026", [1, 15])
Tempo.select(~o"2026", ~o"12-25")
Tempo.select(~o"2026", ~o"10O")     # ISO 8601-2 ordinal day — the 10th day of 2026
Tempo.select(~o"2026-06", ~o"5K")   # ISO 8601-2 day-of-week — every Friday in June 2026
Tempo.select(~o"2026", ~o"-1M")     # ISO 8601-2 negative — the last month of 2026
Tempo.select(~o"2026-06", ~o"-1D")  # ISO 8601-2 negative — the last day of June 2026
Tempo.select(~o"2026", &my_holidays/1)

Every call returns {:ok, %Tempo.IntervalSet{}} (or {:error, reason}), consistent with the other set-algebra operations — the result composes directly into Tempo.union/2, Tempo.intersection/2, Tempo.difference/2.

Tempo.select/2 is a pure function. It has no opts, no ambient locale read, no implicit territory resolution. Every input that can affect the result is a value on the selector. Locale-dependent constraints like "workdays" or "weekend" are constructed by Tempo.workdays/1 and Tempo.weekend/1 (which read the locale once at construction time) and composed in:

interval
|> Tempo.select(Tempo.workdays(:US))

That means the workdays(:US) call is where territory resolution happens — not inside select/2 — and the resulting value is safe to capture anywhere, including module attributes.

Selector shapes

ShapeExampleMeaning
[integer] / RangeTempo.select(m, [1, 15])Integer indices applied at base's next-finer unit
%Tempo{} or listTempo.select(y, ~o"12-25")Project the constraint's specified units onto the base
%Tempo{day_of_week: …}Tempo.select(m, ~o"5K")Day-of-week pattern — every matching weekday in the base (ISO 8601-2 K suffix)
%Tempo{day_of_week: [...]}Tempo.select(m, Tempo.workdays(:US))Day-of-week list — every matching weekday in the base
%Tempo{day: N} (ordinal)Tempo.select(y, ~o"10O")Ordinal day in the year — the Nth day (ISO 8601-2 O suffix)
Negative componentsTempo.select(y, ~o"-1M")ISO 8601-2 §4.4.1 — count from the end of the containing unit
%Tempo.Interval{} or listTempo.select(days, ~o"T09/T17")Project as a span — coarser units from the base, finer from each endpoint, half-open [from, to)
Interval duration formTempo.select(days, ~o"T09/PT7H36M")Span from the projected start plus the duration
FunctionTempo.select(y, &fn/1)The function returns any of the above; evaluated against the base

Base can be a Tempo.t/0, Tempo.Interval.t/0, or Tempo.IntervalSet.t/0. IntervalSet bases flat-map the selector across each member and collect the results.

Time-of-day windows — interval selectors project as spans

Narrowing every member of a day set to the same part of each day is one expression: an interval selector keeps its whole extent, so "nine to five" is a single eight-hour span per member — half-open, written as nine to five, with no off-by-one and no coalesce/1. A list gives several windows per member (the business-hours-with-lunch case), and the duration form expresses windows that are not hour-aligned:

{:ok, workdays} = Tempo.select(~o"2026-07", Tempo.workdays(:AU))
{:ok, open}     = Tempo.select(workdays, [~o"T09/T12", ~o"T13/T17"])
{:ok, shift}    = Tempo.select(workdays, ~o"T09/PT7H36M")

"The workdays of July, nine to five with an hour for lunch."

A window whose to is at or before its from crosses midnight and rolls forward: ~o"T21/T05" on the 15th is the span from the 15th 21:00 to the 16th 05:00 — a night shift reads the way a roster writes it. Use the duration form (~o"T21/PT8H") to say the same thing explicitly.

Negative components — "last N from the end"

ISO 8601-2 §4.4.1 allows any integer component to be negative, meaning "count from the end of the containing time-scale unit". Tempo.select/2 honours this: the resolution is context-aware and produces end-of-span selections without string munging or calendar arithmetic at the call site.

Tempo.select(~o"2026",    ~o"-1M")   #=> December 2026 (last month of year)
Tempo.select(~o"2026",    ~o"-1D")   #=> Dec 31 2026 (last day of year)
Tempo.select(~o"2026",    ~o"-1W")   #=> week 52 of 2026 (last ISO week)
Tempo.select(~o"2026-06", ~o"-1D")   #=> Jun 30 2026 (last day of month)
Tempo.select(~o"2026-02", ~o"-1D")   #=> Feb 28 2026 (leap-aware — Feb 29 in 2024)
Tempo.select(~o"2026-06-15", ~o"-1H") #=> 23:00 (last hour of day)
Tempo.select(~o"2026-06-15T14", ~o"T-1M") #=> 14:59 (last minute of hour)

The resolution is calendar-aware — Tempo.select(~o"2024-02", ~o"-1D") returns Feb 29 because 2024 is a leap year. It is also axis-aware: -1W on a year base uses ISO weeks-in-year (52 or 53); -1W on a month base uses weeks of that month (4 or 5). -1M always refers to the calendar month; -1K to the week's last day-of-week; -1O to the year's last ordinal day.

Time-of-day components (:hour, :minute, :second, :day_of_week) have fixed ranges and resolve at parse time~o"-1H" parses directly as hour: 23, ~o"T-1M" as minute: 59, ~o"T-1S" as second: 59, ~o"-1K" as day_of_week: 7. Calendar-dependent units (:month, :week, :day, :day_of_year) keep their negative value through parse and are resolved against the base context when Tempo.select/2 materialises them.

~o"-1M" is always "last month" (never "last minute") — use the T time designator (~o"T-1M") to select minute-of-hour.

Negative :year values are preserved (BC designator per ISO 8601-2 expanded year form) — they're not flipped to "last year" because a time line has no "end" to count from.

Summary

Functions

Narrow base by selector, returning the selected intervals as a Tempo.IntervalSet.t/0.

Types

base()

selector()

@type selector() ::
  [integer()]
  | Range.t()
  | Tempo.t()
  | Tempo.Interval.t()
  | [Tempo.t() | Tempo.Interval.t()]
  | (base() -> selector())

Functions

select(set, selector)

@spec select(base(), selector()) :: {:ok, Tempo.IntervalSet.t()} | {:error, term()}

Narrow base by selector, returning the selected intervals as a Tempo.IntervalSet.t/0.

See the module doc for the selector vocabulary and runtime- resolution caveats.

Supported base shapes

base can be any Tempo value that materialises to an Interval or IntervalSet. Grouped and masked forms have their endpoints resolved to concrete values before the selector runs, so every ISO 8601-2 shape composes with every selector:

Base shapeExampleMaterialises to
Scalar %Tempo{}~o"2026-06"single Interval
Explicit Interval~o"2026-07/2026-10"single Interval
IntervalSetoutput of Tempo.union/2 etc.IntervalSet (flat-mapped)
Quarter (NQ)~o"2026Y3Q"single Interval (group resolved)
Season (codes 25–32)~o"2026Y26M"Interval bounded by equinox/solstice
Month/day range in a slot~o"2026Y{6..8}M"IntervalSet of three members
Stepped range~o"2026Y{1..-1//3}M"IntervalSet of disjoint members
Set of intervals~o"{2026-01-05/2026-01-12,2026-02-02/2026-02-09}"IntervalSet (flat-mapped)
Archaeological mask~o"156X"decade-long Interval

Example with a quarter base:

Tempo.select(~o"2026Y3Q", Tempo.workdays(:US))
#=> {:ok, IntervalSet with 66 members — workdays of Q3 2026}

Examples

iex> {:ok, set} = Tempo.Select.select(~o"2026-02", [1, 15])
iex> Enum.map(Tempo.IntervalSet.to_list(set), & &1.from.time[:day])
[1, 15]

iex> {:ok, set} = Tempo.Select.select(~o"2026", ~o"12-25")
iex> [xmas] = Tempo.IntervalSet.to_list(set)
iex> xmas.from.time
[year: 2026, month: 12, day: 25]

iex> {:ok, set} = Tempo.Select.select(~o"2026", ~o"10O")
iex> [day10] = Tempo.IntervalSet.to_list(set)
iex> {day10.from.time[:month], day10.from.time[:day]}
{1, 10}

iex> {:ok, set} = Tempo.Select.select(~o"2026-06", ~o"5K")
iex> set |> Tempo.IntervalSet.to_list() |> Enum.map(& &1.from.time[:day])
[5, 12, 19, 26]

iex> {:ok, set} = Tempo.Select.select(~o"2026-02", Tempo.workdays(:US))
iex> Tempo.IntervalSet.count(set)
20