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 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 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
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."
Summary
Types
The dates a holiday observed on another day is given on: :substitute the
day it is observed on, :gazetted its own date, or :both.
The category a holiday falls under, following
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.
A holiday request's target: a positional CLDR territory code (atom or string,
always a territory — never a language), a Localize.LanguageTag.t/0, or a
keyword list carrying a :territory or :locale option.
Functions
Adds a holiday of your own to a set of holidays.
Reads a holiday rule into the holiday it describes.
Projects holiday occurrences onto the evening their day begins.
Returns a territory's holidays as a recurrence set.
Removes holidays from a set of holidays by id or name.
Types
@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.
@type holiday_type() :: :public | :bank | :school | :optional | :observance
The category a holiday falls under, following
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.
@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 Localize.LanguageTag.t/0, or a
keyword list carrying a :territory or :locale option.
Functions
@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
holidaysis a set of holidays asholidays/2returns it.ruleis a holiday rule in the date-holidays grammar. See the grammar.optionsis a keyword list of options.
Options
:name,:type,:idand:metadataare as forcompile/2. The holiday's dates are the set's, so:datesis not an option.
Returns
{:ok, holidays}— the set with the holiday added, or{:error, exception}— an exceptioncompile/2returns 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 reasoncompile/2returns 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"]
@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
ruleis a holiday rule in the date-holidays grammar. See the grammar.optionsis a keyword list of options.
Options
:name— the holiday's name. The default is the rule.:type— itsholiday_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,:gazettedor:both, as forholidays/2. The default is:substitute.
Returns
{:ok, holiday}— the holiday, aTempo.t/0: a yearly recurrence, a recurrence set of its date and its observed days, or a conditional member, tagged with:id,:name,:typeand your own metadata, and with:calendarfor a rule of another calendar than the Gregorian.{:error, exception}— aTempo.ParseErrorfor 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")
@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
occurrencesis a set (aTempo.t/0) of holiday occurrences, asTempo.to_interval/2converts the setholidays/2returns.day_startis how the day begins, aTempo.Holidays.DayStart.t/0::midnight— leaves the occurrences as they are.:eveningor: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:eveningor: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]}
@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
targetnames the territory — a positional CLDR territory code (:AU,"AU", always a territory, never a language) or aLocalize.LanguageTag.t/0. Omit it and pass the target by option instead. SeeTempo.Holidays.Locale.
Options
:territory— an explicit CLDR territory code, validated (territory: :SAis Saudi Arabia).:locale— a BCP 47 locale identifier orLocalize.LanguageTag.t/0whose 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'su-sdsubdivision. 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). SeeTempo.Holidays.Data.subdivision/2.:include— theholiday_type/0to keep, a type or a list of them. The default is every type.:exclude— theholiday_type/0to 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 (aTempo.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:datesit 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}, oran error
compile/2returns 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
@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
holidaysis a set of holidays asholidays/2returns it.namesis a holiday's id or name, or a list of them. A holiday's id is its rule, unlessadd/3gave 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"]