# `Tempo.Select`
[🔗](https://github.com/kipcole9/tempo/blob/v0.5.0/lib/tempo/select.ex#L1)

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.

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

| Shape | Example | Meaning |
| ----- | ------- | ------- |
| `[integer]` / `Range` | `Tempo.select(m, [1, 15])` | Integer indices applied at base's next-finer unit |
| `%Tempo{}` or list | `Tempo.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 components | `Tempo.select(y, ~o"-1M")` | ISO 8601-2 §4.4.1 — count from the end of the containing unit |
| `%Tempo.Interval{}` or list | `Tempo.select(y, vacation)` | Same, for explicit intervals |
| Function | `Tempo.select(y, &fn/1)` | The function returns any of the above; evaluated against the base |

Base can be a `t:Tempo.t/0`, `t:Tempo.Interval.t/0`, or
`t: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.

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

# `base`

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

# `selector`

```elixir
@type selector() ::
  [integer()]
  | Range.t()
  | Tempo.t()
  | Tempo.Interval.t()
  | [Tempo.t() | Tempo.Interval.t()]
  | (base() -&gt; selector())
```

# `select`

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

Narrow `base` by `selector`, returning the selected intervals
as a `t: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 shape | Example | Materialises to |
| ---------- | ------- | --------------- |
| Scalar `%Tempo{}` | `~o"2026-06"` | single Interval |
| Explicit Interval | `~o"2026-07/2026-10"` | single Interval |
| IntervalSet | output 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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
