# `Tempo.Holidays`
[🔗](https://github.com/elixir-tempo/tempo_holidays/blob/v0.1.0/lib/tempo/holidays.ex#L1)

Holidays as Tempo recurrences, for any window.

A holiday is a *recurrence*, not a date: "Christmas is the 25th of
December", "Thanksgiving is the fourth Thursday of November". A territory's
holidays are a recurrence set (a `t:Tempo.t/0`) — one member per holiday, each
tagged with the holiday's metadata — and `Tempo.to_interval/2` converts
it to the set (a `t:Tempo.t/0`) of the holidays in a window, each
occurrence tagged the same way. Because both are Tempo values, holidays
compose with anything else through set algebra.

The data is the [date-holidays](https://github.com/commenthol/date-holidays)
dataset for every territory, carrying every `:type` (public holidays through
to observances). Fixed dates and weekday-in-month holidays are ISO 8601-2
selections (`FL12M25DN`, `FL6M1K2IN`); Easter-relative holidays are windows
off the computed `(easter)e` event; Islamic, Hebrew and other calendar
holidays are recurrences in their own calendar, which a set of occurrences
brings to the one calendar it is in; a holiday
observed on its own date and a substitute day is a nested set of both; and a
bridge day or a holiday moved off another is a conditional member
(`Tempo.RecurrenceSet.keep_when/2`, `move_when/2`), resolved against the
others when the set converts.

Each holiday's metadata is `:id` (its date-holidays rule, unique within the
set, where names repeat), `:name` and `:type`; a substitute day adds
`substitute: true`, and a holiday whose rule is in another calendar than the
Gregorian adds `:calendar`, that calendar's module. The set's own metadata
names its territory, its subdivision and the `:dates` it gives.

The primary API is `holidays/2`. `add/3` and `remove/2` change the holidays
it gives with holidays of your own, and the `:holidays` configuration
changes a territory's for every request; `compile/2` reads a holiday from a
rule in the dataset's grammar, and `day_start/2` projects the Islamic and
Hebrew holidays whose day begins at sunset onto the evening before.

## Example

    {:ok, holidays} = Tempo.Holidays.holidays(:AU)
    {:ok, this_year} = Tempo.to_interval(holidays, within: ~o"2026")

    {:ok, clashes} = Tempo.intersection(my_diary, holidays, metadata: :merge)

> *"Australia's **holidays** are a set of recurrences. **This year's** are the
> days they fall on in 2026; the diary's **clashes** are its entries that fall
> on one, each labelled with the holiday it hit."*

# `dates`

```elixir
@type dates() :: :substitute | :gazetted | :both
```

The dates a holiday observed on another day is given on: `:substitute` the
day it is observed on, `:gazetted` its own date, or `:both`.

# `holiday_type`

```elixir
@type holiday_type() :: :public | :bank | :school | :optional | :observance
```

The category a holiday falls under, following
[date-holidays](https://github.com/commenthol/date-holidays): a `:public`
holiday is a statutory day off, `:bank` a banking holiday, `:school` a school
holiday, `:optional` a day observed at the holder's discretion, and
`:observance` a marked but non-statutory day.

# `target`

```elixir
@type target() :: atom() | String.t() | Localize.LanguageTag.t() | keyword()
```

A holiday request's target: a positional CLDR territory code (atom or string,
always a territory — never a language), a `t:Localize.LanguageTag.t/0`, or a
keyword list carrying a `:territory` or `:locale` option.

# `add`

```elixir
@spec add(Tempo.t(), String.t(), keyword()) ::
  {:ok, Tempo.t()} | {:error, {atom(), term()} | Exception.t()}
```

Adds a holiday of your own to a set of holidays.

The holiday is read from its rule as `compile/2` reads it, with the
`:dates` the set was read with, and takes the place of a holiday of the
same id. It is added whatever its type: a selection by type in `holidays/2`
selects the territory's holidays, not yours.

A holiday that depends on others, a bridge day or a holiday moved off
another, is decided against the holidays in the set when the set converts:
an added holiday that depends on the set's, and a holiday of the set that
depends on yours. The exception is a holiday whose dependency `holidays/2`
left out by type, which carries the holidays it depends on as they were
read and does not see yours.

### Arguments

* `holidays` is a set of holidays as `holidays/2` returns it.

* `rule` is a holiday rule in the date-holidays grammar. See
  [the grammar](https://hexdocs.pm/ex_tempo/date-rule-grammar.html).

* `options` is a keyword list of options.

### Options

* `:name`, `:type`, `:id` and `:metadata` are as for `compile/2`. The
  holiday's dates are the set's, so `:dates` is not an option.

### Returns

* `{:ok, holidays}` — the set with the holiday added, or

* `{:error, exception}` — an exception `compile/2` returns for a rule it
  cannot read, or

* `{:error, reason}` — `{:invalid_holidays, holidays}` for a value that is
  not a set of holidays, `{:invalid_rule, rule}` for a rule that is not a
  string, `{:invalid_option, option}`, or a reason `compile/2` returns for
  an option.

### Examples

    iex> import Tempo.Sigils
    iex> {:ok, holidays} = Tempo.Holidays.holidays(:AU, include: :public)
    iex> {:ok, holidays} = Tempo.Holidays.add(holidays, "1st friday in December", name: "Company Day")
    iex> {:ok, december} = Tempo.to_interval(holidays, within: ~o"2026-12")
    iex> Tempo.IntervalSet.map(december, &Tempo.metadata(&1).name)
    ["Company Day", "Christmas Day", "Boxing Day"]

# `compile`

```elixir
@spec compile(String.t(), keyword()) ::
  {:ok, Tempo.t()} | {:error, {atom(), term()} | Exception.t()}
```

Reads a holiday rule into the holiday it describes.

The rule is written in the date-holidays grammar the dataset's holidays are
written in, which Tempo reads with `Tempo.DateRule`: `"12-25"`, `"4th
thursday in November"`, `"01-26 and if saturday, sunday then next monday"`,
`"15 Nisan"`. The holiday is tagged as a member of `holidays/2` is, so it
sits beside a territory's holidays as one of them.

A rule that depends on other holidays, a bridge day (`"08-14 if 08-15 is
holiday"`) or a holiday moved off another, is a conditional member: decided
against the other members of a set it is in, and against none when it is
converted alone, so alone it falls on no day.

### Arguments

* `rule` is a holiday rule in the date-holidays grammar. See
  [the grammar](https://hexdocs.pm/ex_tempo/date-rule-grammar.html).

* `options` is a keyword list of options.

### Options

* `:name` — the holiday's name. The default is the rule.

* `:type` — its `t:holiday_type/0`. The default is `:public`.

* `:id` — its id, unique within a set of holidays. The default is the rule,
  as a dataset holiday's id is its rule.

* `:metadata` — a map of metadata of your own, kept beside the id, name and
  type. The default is `%{}`.

* `:dates` — the dates a holiday observed on another day is given on,
  `:substitute`, `:gazetted` or `:both`, as for `holidays/2`. The default
  is `:substitute`.

### Returns

* `{:ok, holiday}` — the holiday, a `t:Tempo.t/0`: a yearly recurrence, a
  recurrence set of its date and its observed days, or a conditional
  member, tagged with `:id`, `:name`, `:type` and your own metadata, and
  with `:calendar` for a rule of another calendar than the Gregorian.

* `{:error, exception}` — a `Tempo.ParseError` for a rule that cannot be
  read, naming where reading stopped, or another exception for a date the
  rule names that its calendar does not have (`Tempo.DateRule.parse/2`), or

* `{:error, reason}` — `{:invalid_option, option}`, `{:invalid_id, value}`,
  `{:invalid_name, value}`, `{:invalid_holiday_type, value}`,
  `{:invalid_metadata, value}` or `{:invalid_dates, value}`.

### Examples

    iex> import Tempo.Sigils
    iex> {:ok, thanksgiving} = Tempo.Holidays.compile("4th thursday in November", name: "Thanksgiving Day")
    iex> Tempo.metadata(thanksgiving)
    %{id: "4th thursday in November", name: "Thanksgiving Day", type: :public}
    iex> {:ok, this_year} = Tempo.to_interval(thanksgiving, within: ~o"2026")
    iex> Tempo.IntervalSet.map(this_year, &Tempo.day/1)
    [26]

    iex> {:error, %Tempo.ParseError{}} = Tempo.Holidays.compile("12-25 on a full moon")

# `day_start`

```elixir
@spec day_start(Tempo.t(), Tempo.Holidays.DayStart.t()) ::
  {:ok, Tempo.t()} | {:error, term()}
```

Projects holiday occurrences onto the evening their day begins.

A day in Tempo is just a day: an Islamic or Hebrew holiday converts to a day
in its own calendar, which carries no day-start convention. That calendar's
day begins at sunset, so on the Gregorian timeline the holiday begins the
evening before. `day_start/2` projects each occurrence of a holiday of such a
calendar onto the datetime interval from the boundary that begins its first
day to the one that begins the day after its last. A set of occurrences is
in one calendar, so a holiday is told by the `:calendar` its metadata
carries, the calendar its rule is in, and an occurrence that carries none by
the calendar its own day is in.

A set is of one frame, so every other occurrence becomes the civil day it
is where the day start is read: in the zone it names, at its location, or,
for a bare `:evening` or `:sunset`, at the reference place of the set's
sunset holidays. Where no holiday is projected, as for a set of Gregorian
holidays alone, every occurrence is left as it is.

### Arguments

* `occurrences` is a set (a `t:Tempo.t/0`) of holiday occurrences, as
  `Tempo.to_interval/2` converts the set `holidays/2` returns.

* `day_start` is how the day begins, a `t:Tempo.Holidays.DayStart.t/0`:

  * `:midnight` — leaves the occurrences as they are.

  * `:evening` or `:sunset` — 18:00, or sunset, at the calendar's reference
    place: Mecca for an Islamic date, Jerusalem for a Hebrew one.

  * `{:evening, location}` or `{:sunset, location}` — the same at a location
    of your own, an IANA zone id (`"Asia/Kuala_Lumpur"`) or a `{longitude,
    latitude}` point.

### Returns

* `{:ok, interval_set}` — the occurrences, each projected one a datetime
  interval and each other one a day in the day start's zone, keeping their
  metadata. A lazy set stays lazy.

* `{:error, {:invalid_day_start, day_start}}`,
  `{:error, {:invalid_occurrences, occurrences}}`, or
  `{:error, {:location_required, day_start}}` for a lazy set and a bare
  `:evening` or `:sunset`, since a set read as it is walked cannot be looked
  through for the reference place of its sunset holidays.

### Examples

    iex> import Tempo.Sigils
    iex> {:ok, holidays} = Tempo.Holidays.holidays(:SA, include: :public)
    iex> {:ok, this_year} = Tempo.to_interval(holidays, within: ~o"2026")
    iex> {:ok, evenings} = Tempo.Holidays.day_start(this_year, {:evening, "Asia/Riyadh"})
    iex> eid = Enum.find(Tempo.IntervalSet.members(evenings), &(Tempo.metadata(&1).name =~ "Eid al-Fitr"))
    iex> {:ok, begins} = Tempo.to_elixir(Tempo.Interval.from(eid))
    iex> {DateTime.to_date(begins), DateTime.to_time(begins)}
    {~D[2026-03-18], ~T[18:00:00]}

# `holidays`

```elixir
@spec holidays(target(), keyword()) ::
  {:ok, Tempo.t()} | {:error, {atom(), term()} | Exception.t()}
```

Returns a territory's holidays as a recurrence set.

Each member is one holiday — its recurrence, a nested set of its own date and
its substitute days, or a conditional member for a bridge day or a holiday
moved off another — tagged with `:id`, `:name` and `:type` metadata. The
set's own metadata names the territory, and the subdivision when one is given.
`Tempo.to_interval/2` converts it to the holidays in a window.

A period date-holidays splits over the year end, because its data cannot run
one past it, is one member: Victoria's summer school holidays run from
December to the end of January, 31 December included.

A selection by type never changes a holiday's date: a kept holiday that moves
off another (Näfelser Fahrt off Maundy Thursday, an observance) carries the
holidays it depends on when their type is left out.

Holidays of your own change a territory's in two ways. `add/3` and
`remove/2` change the set this function returns. `config :tempo_holidays,
holidays:` changes a territory's holidays for every request, keyed by
territory, or by territory and subdivision, each with an `:add` (rules, each
a rule or `{rule, options}` with the options of `add/3`) and a `:remove`
(ids or names). A territory's apply to its subdivisions too, and a
subdivision's come after its territory's, each added holiday in place of
one of its id. The configured holidays are the territory's, so a selection
by type selects them as it does the dataset's. The configuration is read at
each request.

    config :tempo_holidays,
      holidays: [
        AU: [add: [{"1st friday in December", name: "Company Day", type: :optional}]],
        BR: [remove: ["Election Day"]],
        "AU-VIC": [add: [{"11-02", name: "Office Closure"}]]
      ]

### Arguments

* `target` names the territory — a positional CLDR territory code (`:AU`,
  `"AU"`, always a territory, never a language) or a `t:Localize.LanguageTag.t/0`.
  Omit it and pass the target by option instead. See `Tempo.Holidays.Locale`.

### Options

* `:territory` — an explicit CLDR territory code, validated (`territory: :SA`
  is Saudi Arabia).

* `:locale` — a BCP 47 locale identifier or `t:Localize.LanguageTag.t/0` whose
  territory is derived (`locale: "en-US-u-sd-usca"` selects California).

* `:subdivision` — a subdivision code, the ISO 3166-2 part after the
  territory's (`"ENG"`, `"CA"`), or the whole code (`"GB-ENG"`), in place of
  a locale's `u-sd` subdivision. It names a subdivision at whichever level
  the data holds it: a state, or a region by its own code or by its path from
  its state (`"CA-LA"`, Los Angeles, where `"LA"` alone is Louisiana). See
  `Tempo.Holidays.Data.subdivision/2`.

* `:include` — the `t:holiday_type/0` to keep, a type or a list of them. The
  default is every type.

* `:exclude` — the `t:holiday_type/0` to leave out, a type or a list of them,
  winning over `:include`. The default is none.

* `:dates` — the dates a holiday observed on another day is given on, when
  its own date falls on a weekend, say:

  * `:substitute` — the day it is observed on: its substitute day in place
    of its own date, so each holiday is counted once. The default.

  * `:gazetted` — its own date alone, the date its law names.

  * `:both` — its own date and its substitute day, as date-holidays gives
    them. A holiday the data moves to another day, rather than observing on
    both, is on the day it moves to.

### Returns

* `{:ok, recurrence_set}` — a recurrence set (a `t:Tempo.t/0`) of the
  holidays: the territory's, with a subdivision's own in place of the ones
  it redefines. Its metadata names the territory, the subdivision when one
  is given, and the `:dates` it gives, or

* `{:error, reason}` — `{:unknown_territory, territory}`,
  `{:invalid_locale, target}`, `{:unknown_subdivision, subdivision}`,
  `{:ambiguous_subdivision, subdivision}`, `{:invalid_holiday_type, value}`,
  `{:invalid_dates, value}`, `{:invalid_config, entry}` or
  `{:invalid_option, option}`, or

* an error `compile/2` returns for a rule the configuration adds.

### Examples

    iex> {:ok, holidays} = Tempo.Holidays.holidays(:AU)
    iex> holidays |> Tempo.RecurrenceSet.members() |> Enum.map(&Tempo.metadata(&1).name) |> Enum.take(2)
    ["New Year's Day", "Australia Day"]
    iex> Tempo.metadata(holidays)
    %{territory: "AU", dates: :substitute}

    iex> {:ok, banking} = Tempo.Holidays.holidays(:AT, include: :bank)
    iex> banking |> Tempo.RecurrenceSet.members() |> Enum.map(&Tempo.metadata(&1).type) |> Enum.uniq()
    [:bank]

    iex> import Tempo.Sigils
    iex> {:ok, england} = Tempo.Holidays.holidays(:GB, subdivision: "ENG", exclude: :observance)
    iex> {:ok, this_year} = Tempo.to_interval(england, within: ~o"2026")
    iex> Tempo.IntervalSet.count(this_year)
    8

# `remove`

```elixir
@spec remove(Tempo.t(), String.t() | [String.t()]) ::
  {:ok, Tempo.t()} | {:error, {atom(), term()} | Exception.t()}
```

Removes holidays from a set of holidays by id or name.

Names repeat where ids do not, so a name removes every holiday of that
name. A holiday that depends on one removed, a bridge day or a holiday
moved off another, no longer counts it, unless `holidays/2` left out the
type it depends on and it carries those holidays as they were read.

### Arguments

* `holidays` is a set of holidays as `holidays/2` returns it.

* `names` is a holiday's id or name, or a list of them. A holiday's id is
  its rule, unless `add/3` gave it another.

### Returns

* `{:ok, holidays}` — the set without the holidays named. A name no
  holiday has removes nothing, or

* `{:error, reason}` — `{:invalid_holidays, holidays}` for a value that is
  not a set of holidays, or `{:invalid_name, name}` for an id or name that
  is not a string.

### Examples

    iex> import Tempo.Sigils
    iex> {:ok, holidays} = Tempo.Holidays.holidays(:AU, include: :public)
    iex> {:ok, holidays} = Tempo.Holidays.remove(holidays, "Boxing Day")
    iex> {:ok, december} = Tempo.to_interval(holidays, within: ~o"2026-12")
    iex> Tempo.IntervalSet.map(december, &Tempo.metadata(&1).name)
    ["Christmas Day"]

---

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