Tempo.Select (Tempo v0.5.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(y, vacation)Same, for explicit intervals
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.

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()

@type base() :: Tempo.t() | Tempo.Interval.t() | Tempo.IntervalSet.t()

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
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