Documentation for Tempo.
Terminology
The following terms, defined by ISO 8601, are used throughout Tempo. For further information consult:
Date
A time on the the calendar time scale. Common forms of date include calendar date, ordinal date or week date.
Time
A mark attributed to an instant or a time interval on a specified time scale.
The term “time” is often used in common language. However, it should only be used if the meaning is clearly visible from the context.
On a time scale consisting of successive time intervals, such as a clock or calendar, distinct instants may be expressed by the same time.
This definition corresponds with the definition of the term “date” in IEC 60050-113:2011, 113-01-12.
Instant
A point on the time axis. An instantaneous event occurs at a specific instant.
Time axis
A mathematical representation of the succession in time according to the space-time model of instantaneous events along a unique axis/
According to the theory of special relativity, the time axis depends on the choice of a spatial reference frame.
In IEC 60050-113:2011, 113-01-03, time according to the space-time model is defined to be the one-dimensional subspace of space-time, locally orthogonal to space.
Time scale
A system of ordered marks which can be attributed to instants on the time axis, one instant being chosen as the origin.
A time scale may amongst others be chosen as:
continuous, e.g. international atomic time (TAI) (see IEC 60050-713:1998, 713-05-18);
continuous with discontinuities, e.g. UTC due to leap seconds, standard time due to summer time and winter time;
successive steps, e.g. calendars, where the time axis is split up into a succession of consecutive time intervals and the same mark is attributed to all instants of each time interval;
discrete, e.g. in digital techniques.
Time interval
A part of the time axis limited by two instants including, unless otherwise stated, the limiting instants themselves.
Time scale unit
A unit of measurement of a duration
For example:
Calendar year, calendar month and calendar day are time scale units of the Gregorian calendar.
Clock hour, clock minutes and clock seconds are time scale units of the 24-hour clock.
In Tempo, time scale units are referred to by the shortened term "unit". When a "unit" is combined with a value, the combination is referred to as a "component".
Duration
A non-negative quantity of time equal to the difference between the final and initial instants of a time interval
The duration is one of the base quantities in the International System of Quantities (ISQ) on which the International System of Units (SI) is based. The term “time” instead of “duration” is often used in this context and also for an infinitesimal duration.
For the term “duration”, expressions such as “time” or “time interval” are often used, but the term “time” is not recommended in this sense and the term “time interval” is deprecated in this sense to avoid confusion with the concept of “time interval”.
The exact duration of a time scale unit depends on the time scale used. For example, the durations of a year, month, week, day, hour or minute, may depend on when they occur (in a Gregorian calendar, a calendar month can have a duration of 28, 29, 30, or 31 days; in a 24-hour clock, a clock minute can have a duration of 59, 60, or 61 seconds, etc.). Therefore, the exact duration can only be evaluated if the exact duration of each is known.
Summary
Types
The error payload returned inside {:error, reason} tuples.
Extended information parsed from an IXDTF suffix.
ISO 8601-2 / EDTF date qualification.
Per-component qualifications parsed from an EDTF Level 2 date.
Functions
Shift tempo by count working days, skipping the territory's
weekend.
true when the two intervals touch at a single boundary
(Allen's :meets | :met_by). See Tempo.Interval.adjacent?/2.
true when a starts strictly after b ends (Allen's
:preceded_by). See Tempo.Interval.after?/2.
Combine a date-like value with a time-of-day value into a datetime.
Returns a boolean indicating if a Tempo.t/0 struct
is anchored to the timeline.
true when the interval is at least as long as the given
duration. See Tempo.Interval.at_least?/2.
true when the interval is at most as long as the given
duration. See Tempo.Interval.at_most?/2.
Return a Tempo at the specified resolution, dispatching to
trunc/2 or extend_resolution/2 based on whether target_unit
is coarser or finer than the current resolution.
true when a ends strictly before b starts (Allen's
:precedes). See Tempo.Interval.before?/2.
Return a second-resolution t/0 at the start (00:00:00) of
the day that contains tempo.
Return a second-resolution t/0 at the start of the month
(YYYY-MM-01T00:00:00) that contains tempo.
true when both endpoints of the interval are concrete
(neither :undefined nor nil). See Tempo.Interval.bounded?/1.
true when a starts after b ends for every placement of their
± margins. See Tempo.Interval.certainly_after?/2.
true when a ends before b starts for every placement of their
± margins. See Tempo.Interval.certainly_before?/2.
true when a and b intersect for every placement of their ±
margins. See Tempo.Interval.certainly_overlaps?/2.
true when a falls within b for every placement of their ±
margins. See Tempo.Interval.certainly_within?/2.
Complement of a Tempo value within a bounding universe. The
:bound option is required. See Tempo.Operations.complement/2.
Compose two Allen relations — the relations possible from A to C
given A r1 B and B r2 C.
true when every instant of b is also in a. Alias for
subset?(b, a, opts). See Tempo.Operations.contains?/3.
Return the day component of a Tempo value, or nil if
the value doesn't specify one.
Return the day of the week as an integer (1..7) using the
Tempo value's calendar.
Return the 1-based ordinal day of the year (1..365 or 1..366
in a leap year) using the Tempo value's calendar.
Return the number of days in the Tempo's month under its calendar.
Difference a \ b — every instant in a that is not in
b. Each result interval is the trimmed remainder; a
members can split into multiple fragments. See
Tempo.Operations.difference/3.
true when a and b share no instants.
See Tempo.Operations.disjoint?/3.
Return the length of an interval — or the total covered length of
an interval set — as a %Tempo.Duration{}.
Return the duration between two endpoints as a %Tempo.Duration{}.
Bang variant of duration/2. Raises on invalid endpoints and
returns the %Tempo.Duration{} directly.
true when a is strictly inside b (Allen's :during).
See Tempo.Interval.during?/2.
true when the interval has zero length. See
Tempo.Interval.empty?/1.
Return a second-resolution t/0 at the exclusive end of
the day that contains tempo — i.e. 00:00:00 of the following
day.
Return a second-resolution t/0 at the exclusive end of
the month that contains tempo — i.e. the first day of the
following month at 00:00:00.
true when a and b span the same instants (at their
aligned resolution). See Tempo.Operations.equal?/3.
true when the interval's length equals the given duration.
See Tempo.Interval.exactly?/2.
Return a multi-line prose explanation of any Tempo value — what it is, what it spans, and how to work with it.
Adds an extended enumeration to a Tempo.
Extend a Tempo's resolution by padding finer units with their start-of-unit minimum values.
Creates a Tempo.t/0 struct from a DateTime.t/0.
Create a Tempo.t/0 from any Elixir date/time type.
Creates a Tempo.t/0 struct from an ISO 8601 or IXDTF
string.
Creates a Tempo.t/0 struct from an ISO8601
string.
Creates a Tempo.t/0 struct from a NaiveDateTime.t/0.
Return the hour component of a Tempo value, or nil if
the value doesn't specify one.
Intersection of two Tempo values — every instant in both
operands. Each result interval is the trimmed overlap; a
members can split into multiple fragments. See
Tempo.Operations.intersection/3.
Return true when the Tempo's year is a leap year under its
calendar.
true when the interval is strictly longer than the given
duration. See Tempo.Interval.longer_than?/2.
true when a's end coincides exactly with b's start
(Allen's :meets). See Tempo.Interval.meets?/2.
Member-preserving symmetric-difference filter — members of
either operand that don't overlap any member of the other,
kept whole. See Tempo.Operations.members_in_exactly_one/3.
Member-preserving anti-overlap filter — returns the whole
members of a that do NOT overlap any member of b, kept
whole with their original metadata. Use this when the
question is about which events survive the filter (e.g.
"which workdays aren't holidays?"). See
Tempo.Operations.members_outside/3.
Member-preserving overlap filter — returns the whole members of
a that overlap any member of b, with their original
metadata. Use this when the question is about which events
hit the query window. See Tempo.Operations.members_overlapping/3.
Return the minute component of a Tempo value, or nil if
the value doesn't specify one.
Return the month component of a Tempo value, or nil if
the value doesn't specify one.
Construct a Tempo.t/0 from a keyword list of time-scale
components and options.
Bang variant of new/1 — raises on invalid input.
The next working day strictly after tempo in the territory.
Return the current time in the given IANA time zone as a
second-resolution t/0.
The three-valued certainty that a and b intersect, given their
± margins. See Tempo.Interval.overlap_certainty/2.
true when a and b share at least one instant.
See Tempo.Operations.overlaps?/3.
Parse a locale-formatted date, time, datetime, or interval string into a Tempo value.
Bang variant of parse/2. Raises on parse failure or invalid
component combination.
true when a could start after b ends for some placement of
their ± margins. See Tempo.Interval.possibly_after?/2.
true when a could end before b starts for some placement of
their ± margins. See Tempo.Interval.possibly_before?/2.
true when a and b could intersect for some placement of their
± margins. See Tempo.Interval.possibly_overlaps?/2.
true when a could fall within b for some placement of their
± margins. See Tempo.Interval.possibly_within?/2.
The working day immediately before tempo in the territory.
Return the 1-based quarter of the year (1..4) for Gregorian-like
calendars.
Classify the Allen interval-algebra relation between two interval-like values.
The three-valued certainty that relation(a, b) is (one of) target.
See Tempo.Interval.relation_certainty/3.
Returns the resolution of a Tempo.t/0 struct.
Truncates a tempo struct to the specified resolution.
Return the second component of a Tempo value, or nil if
the value doesn't specify one.
Narrow a Tempo span by a selector — the composition primitive for "workdays of June", "the 15th of every month", and similar queries.
Project a zoned or UTC-anchored Tempo into another IANA time zone, preserving the UTC instant.
true when the interval is strictly shorter than the given
duration. See Tempo.Interval.shorter_than?/2.
Split a tempo struct into a date and time.
true when every instant of a is also in b.
See Tempo.Operations.subset?/3.
Symmetric difference a △ b — instants in exactly one of
the two operands. Trimmed/instant-level. See
Tempo.Operations.symmetric_difference/3.
Convert a t/0 to its best-fit native Elixir calendar type.
Convert a Tempo struct into a Date.
Convert a zoned Tempo into a DateTime.
Convert a Tempo value to its native Elixir equivalent — the
outbound mirror of from_elixir/2.
Convert an implicit-span Elixir.Tempo.t/0 into the
equivalent explicit Tempo.Interval.t/0 or
Tempo.IntervalSet.t/0.
Raising version of to_interval/1.
Convert any Tempo value to a Tempo.IntervalSet.t/0.
Encode a Tempo value back into an ISO 8601-2 string.
Convert a Tempo struct into a NaiveDateTime.
Format a Tempo as a locale-aware relative time string like
"3 hours ago" or "in 2 days".
Encode a Tempo.Interval.t/0 into an RFC 5545 RRULE string.
Bang variant of to_rrule/1.
Format a Tempo value as a locale-aware string.
Convert a Tempo struct into a Time.
Return today's date in the given IANA time zone as a
day-resolution t/0.
Truncates a tempo struct to the specified resolution.
Union of two Tempo values — every instant in either operand.
See Tempo.Operations.union/3 for full details.
Returns the maximum and minimum time units as a 2-tuple.
Return today's date in UTC as a day-resolution t/0.
Check that an IXDTF value's explicit numeric offset agrees with its IANA time zone at the value's wall instant.
Return a selector that matches the weekend days of a territory.
Return true when tempo falls on a weekend day in the given
territory.
true when a fits inside b inclusive of shared
endpoints. The canonical "does this fit inside that window?"
predicate. See Tempo.Interval.within?/2.
The three-valued certainty that a falls within b, given their
± margins. See Tempo.Interval.within_certainty/2.
Return true when tempo falls on a workday — a day that is not
in the territory's weekend.
Return a selector that matches the workdays of a territory — the days of week that are not in that territory's weekend.
Count the working days within an interval, excluding the territory's weekend.
Return the year component of a Tempo value, or nil if
the value doesn't specify one.
Types
@type error_reason() :: Exception.t()
The error payload returned inside {:error, reason} tuples.
As of v0.21, every originating error site in Tempo returns an
Exception-conforming struct (one of the types under
lib/tempo/exception/), mirroring the convention in Localize
and Calendrical. The atom() | binary() members are retained
transiently for backward compatibility during the migration
and will be removed once all callers are updated.
@type extended_info() :: %{ calendar: atom() | nil, zone_id: String.t() | nil, zone_offset: integer() | nil, tags: %{optional(String.t()) => [String.t()]} }
Extended information parsed from an IXDTF suffix.
:calendar— calendar identifier atom derived fromu-ca=.:zone_id— IANA time zone name such as"Europe/Paris".:zone_offset— numeric offset in minutes from[+HH:MM].:tags— map of non-u-caelective tagged suffixes.
@type qualification() :: :uncertain | :approximate | :uncertain_and_approximate | nil
ISO 8601-2 / EDTF date qualification.
:uncertain— the value is uncertain (?).:approximate— the value is approximate (~), e.g. "circa".:uncertain_and_approximate— both (%).nilwhen no qualification was supplied.
@type qualifications() :: %{optional(atom()) => qualification()} | nil
Per-component qualifications parsed from an EDTF Level 2 date.
A map from the component unit (:year, :month, :day) to its
qualification atom. nil when no component-level qualification
was present in the parsed string.
@type t() :: %Tempo{ calendar: Calendar.calendar() | nil, extended: extended_info() | nil, qualification: qualification(), qualifications: qualifications(), shift: time_shift(), time: token_list() }
@type time_unit() ::
:year | :month | :week | :day | :hour | :minute | :second | :microsecond
Functions
@spec add_working_days(t(), integer(), Tempo.Territory.input()) :: t()
Shift tempo by count working days, skipping the territory's
weekend.
A positive count moves forward, a negative count backward, and
0 returns the value unchanged. Each counted step lands on a working
day, so adding one working day to a Friday returns the following
Monday (in a Saturday/Sunday-weekend territory). The time of day,
calendar, and zone are preserved.
Arguments
tempois at/0that denotes a single day (a date or datetime).countis the integer number of working days to add — negative to subtract.territoryis resolved throughTempo.Territory.resolve/1, as forweekend?/2.
Returns
- a
t/0that iscountworking days fromtempo.
Examples
iex> Tempo.add_working_days(~o"2026-06-12", 1, :US)
~o"2026Y6M15D"
iex> Tempo.add_working_days(~o"2026-06-15", -1, :US)
~o"2026Y6M12D"
iex> # Five working days on from a Monday is the next Monday.
iex> Tempo.add_working_days(~o"2026-06-15", 5, :US)
~o"2026Y6M22D"
iex> # The weekend differs by territory (Saudi Arabia: Friday/Saturday).
iex> Tempo.add_working_days(~o"2026-06-11", 1, :SA)
~o"2026Y6M14D"
true when the two intervals touch at a single boundary
(Allen's :meets | :met_by). See Tempo.Interval.adjacent?/2.
true when a starts strictly after b ends (Allen's
:preceded_by). See Tempo.Interval.after?/2.
@spec anchor(t(), t()) :: t() | {:error, error_reason()}
Combine a date-like value with a time-of-day value into a datetime.
This is axis composition, not a set operation. Set
operations require both operands to share an anchor class;
anchor/2 is how the user explicitly composes cross-axis
values before set operations run. No set-algebra laws apply
to anchor/2 — it's a constructor, not an operator.
Arguments
anchoredis an anchoredElixir.Tempo.t/0(has a year component) — typically a date like~o"2026-01-04".non_anchoredis a non-anchoredElixir.Tempo.t/0(pure time-of-day) — typically a time like~o"T10:30".
Returns
- A new
t/0combining the two. The date components come fromanchored; the time components come fromnon_anchored.
Raises
ArgumentErrorwhen either argument has the wrong anchor class — ifanchoredis non-anchored ornon_anchoredis already anchored.
Examples
iex> Tempo.anchor(~o"2026-01-04", ~o"T10:30")
~o"2026Y1M4DT10H30M"
Returns a boolean indicating if a Tempo.t/0 struct
is anchored to the timeline.
Anchored means that the time representation contains
enough information for it to be located in a single
location on the timeline. In practise this means the
if the tempo struct has a :year value then
it is anchored.
Arguments
tempois anyElixir.Tempo.t/0.
Returns
trueorfalse
Examples
iex> Tempo.anchored? ~o"2022"
true
iex> Tempo.anchored? ~o"2M"
false
true when the interval is at least as long as the given
duration. See Tempo.Interval.at_least?/2.
true when the interval is at most as long as the given
duration. See Tempo.Interval.at_most?/2.
@spec at_resolution(tempo :: t(), target_unit :: time_unit()) :: t() | {:error, error_reason()}
Return a Tempo at the specified resolution, dispatching to
trunc/2 or extend_resolution/2 based on whether target_unit
is coarser or finer than the current resolution.
This is the unified entry point for normalising resolution. It
is idempotent when target_unit matches the current resolution.
Arguments
tempois anyElixir.Tempo.t/0.target_unitis any time unit atom (:year,:month,:day,:hour,:minute,:second, …).
Returns
The Tempo at the requested resolution, or
{:error, reason}.
Examples
iex> Tempo.at_resolution(~o"2020Y", :day)
~o"2020Y1M1D"
iex> Tempo.at_resolution(~o"2020Y6M15DT10H", :day)
~o"2020Y6M15D"
iex> Tempo.at_resolution(~o"2020Y6M15D", :day)
~o"2020Y6M15D"
true when a ends strictly before b starts (Allen's
:precedes). See Tempo.Interval.before?/2.
@spec beginning_of_day(t()) :: t() | {:error, error_reason()}
Return a second-resolution t/0 at the start (00:00:00) of
the day that contains tempo.
Preserves the input's calendar, shift, and zone metadata so that
beginning-of-day in [Europe/Paris] still names midnight Paris
time, not midnight UTC.
Arguments
tempois at/0with at least year/month/day components.
Returns
- A second-resolution
t/0.
Examples
iex> Tempo.beginning_of_day(~o"2026-06-15T14:30:00")
~o"2026Y6M15DT0H0M0S"
iex> Tempo.beginning_of_day(~o"2026-06-15")
~o"2026Y6M15DT0H0M0S"
@spec beginning_of_month(t()) :: t() | {:error, error_reason()}
Return a second-resolution t/0 at the start of the month
(YYYY-MM-01T00:00:00) that contains tempo.
Arguments
tempois at/0with at least year/month components.
Returns
- A second-resolution
t/0.
Examples
iex> Tempo.beginning_of_month(~o"2026-06-15T14:30:00")
~o"2026Y6M1DT0H0M0S"
iex> Tempo.beginning_of_month(~o"2026-06")
~o"2026Y6M1DT0H0M0S"
true when both endpoints of the interval are concrete
(neither :undefined nor nil). See Tempo.Interval.bounded?/1.
true when a starts after b ends for every placement of their
± margins. See Tempo.Interval.certainly_after?/2.
true when a ends before b starts for every placement of their
± margins. See Tempo.Interval.certainly_before?/2.
true when a and b intersect for every placement of their ±
margins. See Tempo.Interval.certainly_overlaps?/2.
true when a falls within b for every placement of their ±
margins. See Tempo.Interval.certainly_within?/2.
Complement of a Tempo value within a bounding universe. The
:bound option is required. See Tempo.Operations.complement/2.
Compose two Allen relations — the relations possible from A to C
given A r1 B and B r2 C.
Thin delegate to Tempo.Interval.compose/2. Allen's composition
(1983) chains a qualitative inference without holding any interval:
"if A precedes B and B is during C, how can A relate to C?"
Examples
iex> Tempo.compose(:precedes, :during)
[:precedes, :meets, :overlaps, :starts, :during]
iex> Tempo.compose(:contains, :during)
[:overlaps, :finished_by, :contains, :starts, :equals, :started_by, :during, :finishes, :overlapped_by]
true when every instant of b is also in a. Alias for
subset?(b, a, opts). See Tempo.Operations.contains?/3.
@spec day(t() | Tempo.Interval.t()) :: integer() | nil
Return the day component of a Tempo value, or nil if
the value doesn't specify one.
The accessors (year/1, month/1, day/1, hour/1,
minute/1, second/1) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.
Arguments
valueis at/0orTempo.Interval.t/0.
Returns
The component value as an integer when unambiguous.
nilwhen the value doesn't specify that unit (e.g.Tempo.day(~o"2026")returnsnil— the year value has no day).Raises
ArgumentErrorwhen called on an interval whose span covers multiple values of that unit (e.g.Tempo.day/1on a month-spanning interval is ambiguous).
Examples
iex> Tempo.day(~o"2026-06-15T10:30:45")
15
iex> Tempo.day(~o"2026")
nil
Return the day of the week as an integer (1..7) using the
Tempo value's calendar.
For the Gregorian calendar with :default ordering, 1 is
Monday and 7 is Sunday — matching Date.day_of_week/1.
Arguments
tempois at/0anchored with at least year/month/day components.starting_onis a day-of-week atom controlling which day is numbered1. Accepts:default(calendar's default),:monday,:tuesday,:wednesday,:thursday,:friday,:saturday, or:sunday. Defaults to:default.
Returns
- Integer
1..7.
Raises
ArgumentErrorwhentempohas no date components.
Examples
iex> Tempo.day_of_week(~o"2026-06-15")
1
iex> Tempo.day_of_week(~o"2026-06-15", :sunday)
2
@spec day_of_year(t()) :: pos_integer()
Return the 1-based ordinal day of the year (1..365 or 1..366
in a leap year) using the Tempo value's calendar.
Arguments
tempois at/0anchored with at least year/month/day components.
Returns
- A positive integer.
Raises
ArgumentErrorwhentempohas no date components.
Examples
iex> Tempo.day_of_year(~o"2026-01-01")
1
iex> Tempo.day_of_year(~o"2024-12-31")
366
@spec days_in_month(t()) :: pos_integer()
Return the number of days in the Tempo's month under its calendar.
Arguments
tempois at/0with year and month components.
Returns
- A positive integer.
Raises
ArgumentErrorwhentempohas no year or no month component.
Examples
iex> Tempo.days_in_month(~o"2024-02-15")
29
iex> Tempo.days_in_month(~o"2025-02")
28
iex> Tempo.days_in_month(~o"2026-04")
30
Difference a \ b — every instant in a that is not in
b. Each result interval is the trimmed remainder; a
members can split into multiple fragments. See
Tempo.Operations.difference/3.
true when a and b share no instants.
See Tempo.Operations.disjoint?/3.
Return the length of an interval — or the total covered length of
an interval set — as a %Tempo.Duration{}.
Unbounded intervals return :infinity. See
Tempo.Interval.duration/1 and Tempo.IntervalSet.duration/1.
@spec duration(t(), t()) :: {:ok, Tempo.Duration.t()} | {:error, Exception.t()}
Return the duration between two endpoints as a %Tempo.Duration{}.
A convenience that builds the interval [from, to) internally and
measures it — Tempo.duration(now, deadline) instead of
constructing a Tempo.Interval.t/0 first. The length is measured
on the UTC time line, so zoned endpoints spanning a DST transition
yield the true elapsed duration (a 23- or 25-hour day), not the
wall-clock difference.
Unlike duration/1, whose interval argument is valid by
construction, this takes raw endpoints that may not form a
measurable interval, so it returns a tagged tuple. Use
duration!/2 when the endpoints are known-good.
Arguments
fromis the startTempo.t/0— must be anchored (carry a year).tois the endTempo.t/0— anchored, and strictly later thanfrom.
Returns
{:ok, duration}wheredurationis aTempo.Duration.t/0.{:error, reason}when an endpoint is non-anchored, the endpoints are of incompatible calendars, orfromis not strictly earlier thanto.
Examples
iex> {:ok, duration} = Tempo.duration(~o"2026-06-15T09", ~o"2026-06-15T17")
iex> duration
~o"PT28800S"
iex> match?({:error, _reason}, Tempo.duration(~o"2026-06-15T17", ~o"2026-06-15T09"))
true
@spec duration!(t(), t()) :: Tempo.Duration.t()
Bang variant of duration/2. Raises on invalid endpoints and
returns the %Tempo.Duration{} directly.
Examples
iex> Tempo.duration!(~o"2026-06-15T09", ~o"2026-06-15T17")
~o"PT28800S"
true when a is strictly inside b (Allen's :during).
See Tempo.Interval.during?/2.
true when the interval has zero length. See
Tempo.Interval.empty?/1.
@spec end_of_day(t()) :: t() | {:error, error_reason()}
Return a second-resolution t/0 at the exclusive end of
the day that contains tempo — i.e. 00:00:00 of the following
day.
Tempo follows the half-open [from, to) convention everywhere,
so end_of_day/1 returns the upper bound at which the day ends
and the next day begins. This is the right argument for
interval construction — pairing beginning_of_day/1 and
end_of_day/1 gives you the 24-hour (or DST-adjusted) window.
Arguments
tempois at/0with at least year/month/day components.
Returns
- A second-resolution
t/0.
Examples
iex> Tempo.end_of_day(~o"2026-06-15T14:30:00")
~o"2026Y6M16DT0H0M0S"
iex> Tempo.end_of_day(~o"2026-12-31")
~o"2027Y1M1DT0H0M0S"
@spec end_of_month(t()) :: t() | {:error, error_reason()}
Return a second-resolution t/0 at the exclusive end of
the month that contains tempo — i.e. the first day of the
following month at 00:00:00.
Half-open by design; see end_of_day/1 for the rationale.
Arguments
tempois at/0with at least year/month components.
Returns
- A second-resolution
t/0.
Examples
iex> Tempo.end_of_month(~o"2026-06-15")
~o"2026Y7M1DT0H0M0S"
iex> Tempo.end_of_month(~o"2026-12")
~o"2027Y1M1DT0H0M0S"
true when a and b span the same instants (at their
aligned resolution). See Tempo.Operations.equal?/3.
true when the interval's length equals the given duration.
See Tempo.Interval.exactly?/2.
Return a multi-line prose explanation of any Tempo value — what it is, what it spans, and how to work with it.
Returns a plain string suitable for iex. For structured output
that renderers can style (ANSI, HTML),
use Tempo.Explain.explain/1 directly and pick a formatter.
@spec extend(t(), nil) :: {:ok, t()} | {:error, error_reason()}
Adds an extended enumeration to a Tempo.
This has the effect of increasing the resolution of the the Tempo struct but still covering the same interval.
Example
iex> Tempo.extend(~o"2020")
{:ok, ~o"2020Y{1..12}M"}
@spec extend_resolution(tempo :: t(), target_unit :: time_unit()) :: t() | {:error, error_reason()}
Extend a Tempo's resolution by padding finer units with their start-of-unit minimum values.
extend_resolution/2 is the scalar counterpart to extend/2:
where extend/2 adds an implicit enumeration (turning ~o"2020Y"
into ~o"2020Y{1..12}M" — a range), extend_resolution/2 fills
in concrete minimums (turning ~o"2020Y" into ~o"2020Y1M1D"
when extended to :day). This is the operation needed to align
resolutions before interval comparison.
Arguments
tempois anyElixir.Tempo.t/0.target_unitis the finer resolution to pad to. Must be finer than or equal totempo's current resolution.
Returns
The padded
t/0, or{:error, reason}whentarget_unitis coarser than the current resolution (usetrunc/2for that direction) or when no path exists from the current unit totarget_unitunder the tempo's calendar.
Examples
iex> Tempo.extend_resolution(~o"2020Y", :day)
~o"2020Y1M1D"
iex> Tempo.extend_resolution(~o"2020Y6M", :hour)
~o"2020Y6M1DT0H"
Creates a Tempo.t/0 struct from a Date.t/0.
Arguments
dateis anyDate.t/0.
Returns
t/0or{:error, reason}
Examples
iex> Tempo.from_date ~D[2022-11-20]
~o"2022Y11M20D"
@spec from_date_time(DateTime.t()) :: t()
Creates a Tempo.t/0 struct from a DateTime.t/0.
The DateTime's time zone information is preserved on the Tempo:
the total offset (utc_offset + std_offset) populates the
:shift field, and the IANA zone identifier is stored on the
:extended map under :zone_id. Iteration on the returned
Tempo carries both pieces of metadata through.
Arguments
date_timeis anyDateTime.t/0.
Returns
t/0.
Examples
iex> Tempo.from_date_time(~U[2022-11-20 10:37:00Z]).time
[year: 2022, month: 11, day: 20, hour: 10, minute: 37, second: 0]
iex> Tempo.from_date_time(~U[2022-11-20 10:37:00Z]).shift
[hour: 0]
iex> Tempo.from_date_time(~U[2022-11-20 10:37:00Z]).extended.zone_id
"Etc/UTC"
@spec from_elixir( value :: Date.t() | Time.t() | NaiveDateTime.t() | DateTime.t(), options :: Keyword.t() ) :: t() | {:error, error_reason()}
Create a Tempo.t/0 from any Elixir date/time type.
Unifies Date.t, Time.t, NaiveDateTime.t, and DateTime.t
into the single Tempo.t representation under the principle
that every date/time value is a bounded interval on the time
line at some resolution. See the
interop guide for what the resulting value
spans once materialised with to_interval/1.
The intended resolution is either given explicitly via the
:resolution option or inferred from the input:
Date.t→:day(Date has no time components).Time.t,NaiveDateTime.t,DateTime.t→:second, or:microsecondwhen the value carries a declared sub-second precision. These types are second-granular by construction, so the resolution follows the type's declared precision rather than the magnitude of the components —09:00:00is a fully specified second, not an under-specified hour. Pass an explicit:resolutionto widen to a coarser span (e.g.:dayfor a midnight value you want to treat as a whole day).
When an explicit :resolution is given, the resulting Tempo is
passed through at_resolution/2 to either truncate or pad to
that resolution.
Arguments
valueis anyDate.t/0,Time.t/0,NaiveDateTime.t/0, orDateTime.t/0.
Options
:resolutionis a time unit atom (:year,:month,:day,:hour,:minute,:second) overriding the inferred resolution.
Returns
The
t/0at the chosen resolution, or{:error, reason}if:resolutionis incompatible with the input.
Examples
iex> Tempo.from_elixir(~D[2022-06-15])
~o"2022Y6M15D"
iex> Tempo.from_elixir(~T[10:30:00])
~o"T10H30M0S"
iex> Tempo.from_elixir(~N[2022-06-15 10:30:00])
~o"2022Y6M15DT10H30M0S"
iex> Tempo.from_elixir(~N[2022-06-15 00:00:00])
~o"2022Y6M15DT0H0M0S"
iex> Tempo.from_elixir(~D[2022-06-15], resolution: :hour)
~o"2022Y6M15DT0H"
iex> Tempo.from_elixir(~N[2022-06-15 10:30:00], resolution: :day)
~o"2022Y6M15D"
@spec from_iso8601(string :: String.t()) :: {:ok, t() | Tempo.Interval.t() | Tempo.Duration.t() | Tempo.Set.t() | Tempo.Range.t()} | {:error, error_reason()}
Creates a Tempo.t/0 struct from an ISO 8601 or IXDTF
string.
The parser supports the vast majority of ISO 8601 parts 1 and 2 as well as the Internet Extended Date/Time Format (IXDTF) defined in draft-ietf-sedate-datetime-extended-09.
An IXDTF suffix follows the ISO 8601 production and consists of
an optional time zone ([Europe/Paris] or [+08:45]) followed
by zero or more tagged suffixes ([u-ca=hebrew], [_key=value]).
Any bracket may be prefixed with ! to mark it critical —
unrecognised critical suffixes cause the parse to fail; elective
suffixes are retained verbatim under extended.tags.
Arguments
stringis any ISO 8601 formatted string, optionally followed by an IXDTF suffix.calendar(optional) is anyCalendar.calendar/0. When passed, the explicit calendar always wins over any[u-ca=NAME]tag in the IXDTF suffix. When omitted, the[u-ca=NAME]tag is resolved to aCalendrical.*module viaCalendrical.calendar_from_cldr_calendar_type/1; if no tag is present,Calendrical.Gregorianis used.
Returns
{:ok, t}where the returned struct's:extendedfield is populated when an IXDTF suffix was parsed, ornilotherwise.{:error, reason}when the string cannot be parsed or a critical IXDTF suffix is unrecognised.
Examples
iex> Tempo.from_iso8601("2022-11-20")
{:ok, ~o"2022Y11M20D"}
iex> Tempo.from_iso8601("2022Y")
{:ok, ~o"2022Y"}
iex> {:error, %Tempo.ParseError{}} = Tempo.from_iso8601("invalid")
iex> {:ok, tempo} = Tempo.from_iso8601("5786-10-30[u-ca=hebrew]")
iex> tempo.calendar
Calendrical.Hebrew
iex> {:ok, tempo} = Tempo.from_iso8601("2022-11-20T10:30:00Z[Europe/Paris][u-ca=hebrew]")
iex> {tempo.extended.zone_id, tempo.extended.calendar, tempo.calendar}
{"Europe/Paris", :hebrew, Calendrical.Hebrew}
iex> {:error, %Tempo.UnknownZoneError{zone_id: "Continent/Imaginary"}} =
...> Tempo.from_iso8601("2022-11-20T10:30:00Z[!Continent/Imaginary]")
@spec from_iso8601(string :: String.t(), calendar :: Calendar.calendar()) :: {:ok, t() | Tempo.Interval.t() | Tempo.Duration.t() | Tempo.Set.t() | Tempo.Range.t()} | {:error, error_reason()}
@spec from_iso8601(string :: String.t(), options :: keyword()) :: {:ok, t() | Tempo.Interval.t() | Tempo.Duration.t() | Tempo.Set.t() | Tempo.Range.t()} | {:error, error_reason()}
Creates a Tempo.t/0 struct from an ISO8601
string.
The parser supports the vast majority of ISO8601 parts 1 and 2.
Arguments
stringis any ISO8601 formatted stringcalendaris anyCalendar.calendar/0. The default isCalendrical.Gregorian.
Returns
t/0orraises an exception
Examples
iex> Tempo.from_iso8601!("2022-11-20")
~o"2022Y11M20D"
iex> Tempo.from_iso8601!("2022Y")
~o"2022Y"
@spec from_naive_date_time(naive_date_time :: NaiveDateTime.t()) :: t()
Creates a Tempo.t/0 struct from a NaiveDateTime.t/0.
Arguments
naive_date_timeis anyNaiveDateTime.t/0.
Returns
t/0or{:error, reason}
Examples
iex> Tempo.from_naive_date_time ~N[2022-11-20 10:37:00]
~o"2022Y11M20DT10H37M0S"
Creates a Tempo.t/0 struct from a Time.t/0.
Arguments
timeis anyTime.t/0.
Returns
t/0or{:error, reason}
Examples
iex> Tempo.from_time ~T[10:09:00]
~o"T10H9M0S"
@spec hour(t() | Tempo.Interval.t()) :: integer() | nil
Return the hour component of a Tempo value, or nil if
the value doesn't specify one.
The accessors (year/1, month/1, day/1, hour/1,
minute/1, second/1) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.
Arguments
valueis at/0orTempo.Interval.t/0.
Returns
The component value as an integer when unambiguous.
nilwhen the value doesn't specify that unit (e.g.Tempo.day(~o"2026")returnsnil— the year value has no day).Raises
ArgumentErrorwhen called on an interval whose span covers multiple values of that unit (e.g.Tempo.day/1on a month-spanning interval is ambiguous).
Examples
iex> Tempo.hour(~o"2026-06-15T10:30:45")
10
iex> Tempo.hour(~o"2026")
nil
Intersection of two Tempo values — every instant in both
operands. Each result interval is the trimmed overlap; a
members can split into multiple fragments. See
Tempo.Operations.intersection/3.
Return true when the Tempo's year is a leap year under its
calendar.
Arguments
tempois at/0with at least a year component.
Returns
trueorfalse.
Raises
ArgumentErrorwhentempohas no year component.
Examples
iex> Tempo.leap_year?(~o"2024")
true
iex> Tempo.leap_year?(~o"2025")
false
true when the interval is strictly longer than the given
duration. See Tempo.Interval.longer_than?/2.
true when a's end coincides exactly with b's start
(Allen's :meets). See Tempo.Interval.meets?/2.
Member-preserving symmetric-difference filter — members of
either operand that don't overlap any member of the other,
kept whole. See Tempo.Operations.members_in_exactly_one/3.
Member-preserving anti-overlap filter — returns the whole
members of a that do NOT overlap any member of b, kept
whole with their original metadata. Use this when the
question is about which events survive the filter (e.g.
"which workdays aren't holidays?"). See
Tempo.Operations.members_outside/3.
Member-preserving overlap filter — returns the whole members of
a that overlap any member of b, with their original
metadata. Use this when the question is about which events
hit the query window. See Tempo.Operations.members_overlapping/3.
@spec merge(t(), t()) :: t() | {:error, error_reason()}
@spec minute(t() | Tempo.Interval.t()) :: integer() | nil
Return the minute component of a Tempo value, or nil if
the value doesn't specify one.
The accessors (year/1, month/1, day/1, hour/1,
minute/1, second/1) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.
Arguments
valueis at/0orTempo.Interval.t/0.
Returns
The component value as an integer when unambiguous.
nilwhen the value doesn't specify that unit (e.g.Tempo.day(~o"2026")returnsnil— the year value has no day).Raises
ArgumentErrorwhen called on an interval whose span covers multiple values of that unit (e.g.Tempo.day/1on a month-spanning interval is ambiguous).
Examples
iex> Tempo.minute(~o"2026-06-15T10:30:45")
30
iex> Tempo.minute(~o"2026")
nil
@spec month(t() | Tempo.Interval.t()) :: integer() | nil
Return the month component of a Tempo value, or nil if
the value doesn't specify one.
The accessors (year/1, month/1, day/1, hour/1,
minute/1, second/1) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.
Arguments
valueis at/0orTempo.Interval.t/0.
Returns
The component value as an integer when unambiguous.
nilwhen the value doesn't specify that unit (e.g.Tempo.day(~o"2026")returnsnil— the year value has no day).Raises
ArgumentErrorwhen called on an interval whose span covers multiple values of that unit (e.g.Tempo.day/1on a month-spanning interval is ambiguous).
Examples
iex> Tempo.month(~o"2026-06-15T10:30:45")
6
iex> Tempo.month(~o"2026")
nil
@spec new(keyword() | map()) :: {:ok, t()} | {:error, error_reason()}
Construct a Tempo.t/0 from a keyword list of time-scale
components and options.
The companion to ~o sigils and Tempo.from_iso8601/1: where the
sigil is ideal for literal values and from_iso8601/1 for already-
formatted strings, new/1 is the right constructor when you have
structured data at runtime — form inputs, database rows, API
payloads, test fixtures.
Components can be passed in any order; new/1 reorders them
coarse-to-fine (year → month → day → hour → minute → second)
before building the struct.
Axis coherence is enforced: the Gregorian axis (:month, :day),
the ISO-week axis (:week, :day_of_week), and the ordinal axis
(:day_of_year) are mutually exclusive.
Arguments
componentsis a keyword list — or a map with the same atom keys — mixing time-scale components and options. At least one time-scale component must be present.Maps are convenient for interop with Elixir's standard date and time types: a
Date.t/0,Time.t/0,NaiveDateTime.t/0, or a bareCalendrical.parse/2:mapresult can be passed directly.Calendar.ISO(Elixir's default) is silently normalised toCalendrical.Gregorianso calendar-aware validation works.
Time-scale components
Every component value must be an integer.
:yearis the calendar year.:monthis the calendar month (Gregorian axis).:weekis the ISO week number (ISO-week axis).:dayis the day of month (Gregorian axis).:day_of_yearis the ordinal day within the year (ordinal axis).:day_of_weekis the ISO day-of-week number (ISO-week axis).:houris the clock hour0..23.:minuteis the clock minute0..59.:secondis the clock second0..59(or60on a leap-second date).
Options
:calendaris theCalendricalcalendar module used to interpret and validate the components. Defaults toCalendrical.Gregorian.:zoneis an IANA time-zone name as a binary (e.g."Australia/Sydney"). Setsextended.zone_id. Requires at least one of:hour,:minute,:secondto be present — a zoned value without a time of day has no UTC projection.:shiftis a manual UTC offset expressed as[hour: n]or[hour: n, minute: m].:qualificationmarks the value's EDTF qualification. One of:uncertain,:approximate, or:uncertain_and_approximate.:metadatais a free-form map attached toextended.tags.
Returns
{:ok, t()}on success.{:error, reason}when components are missing, have non-integer values, mix axes, or name a non-existent zone.
Examples
iex> {:ok, tempo} = Tempo.new(year: 2026, month: 6, day: 15)
iex> tempo.time
[year: 2026, month: 6, day: 15]
iex> {:ok, tempo} = Tempo.new(day: 15, month: 6, year: 2026)
iex> tempo.time
[year: 2026, month: 6, day: 15]
iex> {:ok, meeting} = Tempo.new(year: 2026, month: 6, day: 15, hour: 14, minute: 30)
iex> meeting.time
[year: 2026, month: 6, day: 15, hour: 14, minute: 30]
iex> {:ok, ww} = Tempo.new(year: 2026, week: 24, day_of_week: 3)
iex> ww.time
[year: 2026, week: 24, day_of_week: 3]
iex> {:error, _} = Tempo.new(year: 2026, month: 13)
iex> {:error, _} = Tempo.new(year: 2026, month: 6, week: 24)
iex> {:ok, tempo} = Tempo.new(%{year: 2026, month: 6, day: 15})
iex> tempo.time
[year: 2026, month: 6, day: 15]
iex> {:ok, tempo} = Tempo.new(Map.from_struct(~D[2026-06-15]))
iex> tempo.time
[year: 2026, month: 6, day: 15]
Bang variant of new/1 — raises on invalid input.
Examples
iex> Tempo.new!(year: 2026, month: 6, day: 15).time
[year: 2026, month: 6, day: 15]
@spec next_working_day(t(), Tempo.Territory.input()) :: t()
The next working day strictly after tempo in the territory.
Equivalent to add_working_days(tempo, 1, territory).
Examples
iex> Tempo.next_working_day(~o"2026-06-12", :US)
~o"2026Y6M15D"
Return the current time in the given IANA time zone as a
second-resolution t/0.
Reads from the configured clock (as utc_now/0 does) and shifts
the result into zone. The returned Tempo's wall-clock time is
the zone-local reading of the current UTC instant.
Arguments
zoneis an IANA time zone name (e.g."Europe/Paris","America/New_York"). Defaults to"Etc/UTC", in which casenow/1is equivalent toutc_now/0.
Returns
- A
t/0at second resolution withextended.zone_id: zone.
Examples
iex> tempo = Tempo.now("Europe/London")
iex> tempo.extended.zone_id
"Europe/London"
iex> Tempo.now("Etc/UTC").extended.zone_id
"Etc/UTC"
The three-valued certainty that a and b intersect, given their
± margins. See Tempo.Interval.overlap_certainty/2.
true when a and b share at least one instant.
See Tempo.Operations.overlaps?/3.
@spec parse(String.t(), Keyword.t()) :: {:ok, t() | Tempo.Interval.t()} | {:error, Exception.t()}
Parse a locale-formatted date, time, datetime, or interval string into a Tempo value.
Delegates to Calendrical.parse/2 with as: :map, then routes
the resulting field map (or {from_map, to_map} interval pair)
through Tempo.new/1. Useful when the input shape is not known
up-front — a single text field that may carry any of
"2026-05-16", "14:30", "May 16, 2026 2:30 PM", or
"May 5 – May 10, 2026".
For ISO 8601 / IXDTF input prefer from_iso8601/1, which
preserves EDTF qualification and IXDTF extended suffixes that
the locale-style parser does not understand.
Arguments
inputis the raw user input string.optionsis a keyword list forwarded toCalendrical.parse/2.
Options
Notable keys forwarded to Calendrical.parse/2:
:localeis the locale used to interpret month names, AM/PM markers, and similar locale-dependent tokens. Defaults toLocalize.get_locale/0.:calendaris the CLDR calendar key or calendar module to parse against. Defaults to:gregorian.:reference_dateis the "today" anchor used for two-digit-year pivoting and partial-date inheritance.
The :as option is set to :map regardless of any caller-supplied
value — Tempo always asks Calendrical for the field-map form so it
can rebuild a Tempo value from the parsed fields.
Returns
{:ok, t()}for a single-value input.{:ok, Tempo.Interval.t()}when the input names a range.{:error, exception}when Calendrical cannot parse the string, or whenTempo.new/1rejects the resulting field map.
Examples
iex> {:ok, tempo} = Tempo.parse("2026-05-16", locale: :en)
iex> tempo.time
[year: 2026, month: 5, day: 16]
iex> {:ok, tempo} = Tempo.parse("May 16, 2026", locale: :en)
iex> tempo.time
[year: 2026, month: 5, day: 16]
iex> {:ok, tempo} = Tempo.parse("14:30", locale: :en)
iex> tempo.time
[hour: 14, minute: 30]
iex> {:ok, %Tempo.Interval{}} = Tempo.parse("2026-05-05 – 2026-05-10", locale: :en)
@spec parse!(String.t(), Keyword.t()) :: t() | Tempo.Interval.t()
Bang variant of parse/2. Raises on parse failure or invalid
component combination.
Examples
iex> Tempo.parse!("2026-05-16", locale: :en).time
[year: 2026, month: 5, day: 16]
true when a could start after b ends for some placement of
their ± margins. See Tempo.Interval.possibly_after?/2.
true when a could end before b starts for some placement of
their ± margins. See Tempo.Interval.possibly_before?/2.
true when a and b could intersect for some placement of their
± margins. See Tempo.Interval.possibly_overlaps?/2.
true when a could fall within b for some placement of their
± margins. See Tempo.Interval.possibly_within?/2.
@spec previous_working_day(t(), Tempo.Territory.input()) :: t()
The working day immediately before tempo in the territory.
Equivalent to add_working_days(tempo, -1, territory).
Examples
iex> Tempo.previous_working_day(~o"2026-06-15", :US)
~o"2026Y6M12D"
@spec quarter_of_year(t()) :: 1..4
Return the 1-based quarter of the year (1..4) for Gregorian-like
calendars.
Arguments
tempois at/0anchored with at least year/month components.
Returns
- An integer
1..4.
Raises
ArgumentErrorwhentempohas no year/month components.
Examples
iex> Tempo.quarter_of_year(~o"2026-01-15")
1
iex> Tempo.quarter_of_year(~o"2026-11-30")
4
Classify the Allen interval-algebra relation between two interval-like values.
Thin delegate to Tempo.Interval.relation/2 — see that
function's docs for the full table of 13 relations.
Named relation (not compare) because it returns one of 13
Allen relations rather than stdlib's ternary :lt | :eq | :gt
— using compare would invite the wrong mental model at the
call site.
Use Tempo.IntervalSet.relation_matrix/2 when both operands
are multi-member sets and you want the per-pair breakdown.
Examples
iex> Tempo.relation(~o"2026-06-15", ~o"2026-06-16")
:meets
iex> a = Tempo.Interval.new!(from: ~o"2026-06-01", to: ~o"2026-06-10")
iex> b = Tempo.Interval.new!(from: ~o"2026-06-05", to: ~o"2026-06-15")
iex> Tempo.relation(a, b)
:overlaps
The three-valued certainty that relation(a, b) is (one of) target.
See Tempo.Interval.relation_certainty/3.
@spec resolution(tempo :: t()) :: {time_unit(), time_unit() | non_neg_integer()}
Returns the resolution of a Tempo.t/0 struct.
The resolution is the smallest time unit of the struct and an appropriate scale.
Arguments
tempois anyElixir.Tempo.t/0.
Returns
{time_unit, scale}
Examples
iex> Tempo.resolution ~o"2022"
{:year, 1}
iex> Tempo.resolution ~o"2022-11"
{:month, 1}
iex> Tempo.resolution ~o"2022-11-20"
{:day, 1}
iex> Tempo.resolution ~o"2022Y1M2G3DU"
{:day, 3}
@spec round(tempo :: t(), round_to :: time_unit()) :: t() | {:error, error_reason()}
Truncates a tempo struct to the specified resolution.
Rounding rounds to the specified time unit resolution.
Arguments
tempois anyElixir.Tempo.t/0.round_tois any time unit. The default is:day.
Returns
roundedis a tempo struct that is rounded or{:error, reason}
Examples
iex> Tempo.round ~o"2022-11-21", :day
~o"2022Y11M21D"
iex> Tempo.round ~o"2022-11-21", :month
~o"2022Y12M"
iex> Tempo.round ~o"2022-11-21", :year
~o"2023Y"
@spec second(t() | Tempo.Interval.t()) :: integer() | nil
Return the second component of a Tempo value, or nil if
the value doesn't specify one.
The accessors (year/1, month/1, day/1, hour/1,
minute/1, second/1) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.
Arguments
valueis at/0orTempo.Interval.t/0.
Returns
The component value as an integer when unambiguous.
nilwhen the value doesn't specify that unit (e.g.Tempo.day(~o"2026")returnsnil— the year value has no day).Raises
ArgumentErrorwhen called on an interval whose span covers multiple values of that unit (e.g.Tempo.day/1on a month-spanning interval is ambiguous).
Examples
iex> Tempo.second(~o"2026-06-15T10:30:45")
45
iex> Tempo.second(~o"2026")
nil
Narrow a Tempo span by a selector — the composition primitive for "workdays of June", "the 15th of every month", and similar queries.
Tempo.select/2 is a pure function: the selector is a value,
not an ambient configuration. Locale-dependent constraints are
constructed by Tempo.workdays/1 and Tempo.weekend/1 and
composed in at the call site.
See Tempo.Select for the full vocabulary.
Examples
iex> {:ok, set} = Tempo.select(~o"2026-02", [1, 15])
iex> set |> Tempo.IntervalSet.to_list() |> Enum.map(& &1.from.time[:day])
[1, 15]
iex> {:ok, set} = Tempo.select(~o"2026", ~o"12-25")
iex> [xmas] = Tempo.IntervalSet.to_list(set)
iex> {xmas.from.time[:year], xmas.from.time[:month], xmas.from.time[:day]}
{2026, 12, 25}
@spec shift(t(), Tempo.Duration.t() | keyword()) :: t() | Tempo.Set.t() | Tempo.IntervalSet.t() | {:error, error_reason()}
Shift a t/0 by a duration, returning a new t/0.
The shift amount may be given either as a Tempo.Duration.t/0
(when you already have one, e.g. ~o"P1M") or as a keyword list of
signed unit amounts (the ergonomic ad-hoc form). Both delegate to
Tempo.Math.add/2, which is the principled path when composing
durations directly.
Units are applied largest-to-smallest with the standard
month-end clamping rule (e.g. ~o"2024-01-31" + 1 month is
2024-02-29, not 2024-03-02).
Arguments
tempois anyt/0.shiftis either aTempo.Duration.t/0, or a keyword list of{unit, amount}pairs such as[month: 1, day: -5]or[year: 2]. Valid units::year,:month,:week,:day,:hour,:minute,:second. Keyword amounts may be negative.
Returns
The shifted
t/0.{:error, %Tempo.RequiresAnchorError{}}when the value has no:year(an un-anchored month/day, bare-day, or time-of-day value) and the shift's result would depend on the missing year. The rule: an un-anchored shift is computed when its result is invariant to the year, and errors when it isn't (it never raises). So~o"1M31D"plus one day is~o"2M1D"(January always has 31 days) and a whole-year step is a no-op (~o"1M31D"plusP1Yis~o"1M31D"), but~o"1M31D"plus one month lands on an unresolvable "Feb 31" and~o"2M28D"plus one day (Feb 29 or Mar 1?) both error.
Examples
iex> Tempo.shift(~o"2026-06-15", month: 1, day: -5)
~o"2026Y7M10D"
iex> Tempo.shift(~o"2026-01-31", month: 1)
~o"2026Y2M28D"
iex> Tempo.shift(~o"2026-06-15T10:00:00", hour: -3)
~o"2026Y6M15DT7H0M0S"
iex> Tempo.shift(~o"2026", ~o"P2Y")
~o"2028Y"
iex> Tempo.shift(~o"1M31D", ~o"P1D")
~o"2M1D"
iex> Tempo.shift(~o"1M31D", ~o"P1Y")
~o"1M31D"
iex> match?({:error, %Tempo.RequiresAnchorError{}}, Tempo.shift(~o"1M31D", ~o"P1M"))
true
@spec shift_zone(t(), String.t()) :: {:ok, t()} | {:error, error_reason()}
Project a zoned or UTC-anchored Tempo into another IANA time zone, preserving the UTC instant.
The returned Tempo names the same point on the time line, but the
wall-clock reading is the one an observer in target_zone would
see. This is the stdlib analogue of DateTime.shift_zone/2: in
Tempo it routes through Tempo.Compare.to_utc_seconds/1 so zone
rules are re-evaluated from Tzdata at call time.
Arguments
tempois at/0that carries zone information — either an IANA zone onextended.zone_id, a numericzone_offset, or ashiftkeyword list. A floating Tempo (no zone info) cannot be projected because its UTC instant is undefined.target_zoneis an IANA zone name ("Europe/Paris","America/New_York","Etc/UTC", …).
Returns
{:ok, tempo}at second resolution intarget_zone, or{:error, reason}whentempois not zoned ortarget_zoneis unknown to Tzdata.
Examples
iex> paris = Tempo.from_iso8601!("2026-06-15T14:00:00[Europe/Paris]")
iex> {:ok, new_york} = Tempo.shift_zone(paris, "America/New_York")
iex> new_york.extended.zone_id
"America/New_York"
iex> Keyword.take(new_york.time, [:hour, :minute])
[hour: 8, minute: 0]
true when the interval is strictly shorter than the given
duration. See Tempo.Interval.shorter_than?/2.
Split a tempo struct into a date and time.
true when every instant of a is also in b.
See Tempo.Operations.subset?/3.
Symmetric difference a △ b — instants in exactly one of
the two operands. Trimmed/instant-level. See
Tempo.Operations.symmetric_difference/3.
@spec to_calendar(t()) :: {:ok, Date.t() | Time.t() | NaiveDateTime.t()} | {:error, Tempo.ConversionError.t()}
Convert a t/0 to its best-fit native Elixir calendar type.
Dispatches by resolution: a full date becomes a Date, a
time-of-day becomes a Time, and a full date-and-time becomes a
NaiveDateTime. A value too coarse to pin an instant (a bare year
or month) or one carrying a UTC offset cannot be represented by a
single native type and returns an error. For a specific target
type use to_date/1, to_time/1, or to_naive_date_time/1.
Arguments
tempois at/0.
Returns
{:ok, native}wherenativeis aDate,Time, orNaiveDateTime; or{:error, t:Tempo.ConversionError.t/0}when the value is too coarse to convert, or carries a zone offset.
Examples
iex> Tempo.to_calendar(~o"2026-06-15")
{:ok, ~D[2026-06-15]}
iex> Tempo.to_calendar(~o"2026-06-15T10:30:00")
{:ok, ~N[2026-06-15 10:30:00.000000]}
iex> match?({:error, %Tempo.ConversionError{}}, Tempo.to_calendar(~o"2026"))
true
@spec to_date(t()) :: {:ok, Date.t()} | {:error, error_reason()}
Convert a Tempo struct into a Date.
Accepts the three single-day Tempo shapes:
Calendar date —
[year: Y, month: M, day: D].Ordinal date —
[year: Y, day: DDD]. When the Tempo carries only year and day (no month), the day is interpreted as day-of-year per ISO 8601-2 §4.3.4.DandOboth parse to the same:daykey, so~o"2020-166",~o"2020Y166O", and~o"2020Y166D"all convert correctly.ISO week date —
[year: Y, week: W, day_of_week: K].
Returns
{:ok, %Date{}}on success.{:error, reason}when the Tempo covers a span rather than a single day, or the components don't form a valid date.
Examples
iex> {:ok, date} = Tempo.to_date(~o"2020-06-15")
iex> date
~D[2020-06-15]
iex> {:ok, date} = Tempo.to_date(~o"2020-166")
iex> date
~D[2020-06-14]
iex> {:ok, date} = Tempo.to_date(~o"2020-W24-3")
iex> date
~D[2020-06-10]
@spec to_date_time(t()) :: {:ok, DateTime.t()} | {:error, error_reason()}
Convert a zoned Tempo into a DateTime.
The lossless inverse of from_elixir/2 on a DateTime.t/0:
it preserves the named time zone, re-deriving the UTC offset from
the time-zone database so the result is DST-correct. This is the
conversion to reach for when the zone matters — unlike
to_naive_date_time/1, which deliberately drops it.
Arguments
tempois at/0resolved to at least second resolution and carrying a named IANA zone onextended.zone_id(every value built from aDateTimeviafrom_elixir/2does). Offset-only values (a numeric shift with no named zone) and floating values cannot name aDateTimezone and return an error — attach a zone withshift_zone/2first, or useto_naive_date_time/1.
Returns
{:ok, date_time}, or{:error, reason}when the value lacks a full datetime or a named zone, or when the wall-clock time falls in a spring-forward gap that does not exist in the zone.
Examples
iex> Tempo.to_date_time(~o"2022-11-19T01:02:03Z[Etc/UTC]")
{:ok, ~U[2022-11-19 01:02:03.000000Z]}
iex> {:error, _} = Tempo.to_date_time(~o"2022-11-19T01:02:03")
@spec to_elixir(t() | Tempo.Duration.t()) :: {:ok, Duration.t() | Date.t() | Time.t() | NaiveDateTime.t()} | {:error, Tempo.ConversionError.t()}
Convert a Tempo value to its native Elixir equivalent — the
outbound mirror of from_elixir/2.
A
Tempo.Duration.t/0becomes an ElixirDuration: the units and the{value, precision}microsecond map one-to-one. Tempo- only components (:day_of_year,:day_of_week) have no ElixirDurationequivalent and return an error.A
t/0becomes its best-fit calendar type viato_calendar/1— aDate,Time, orNaiveDateTime. For a specific target (including a zonedDateTime) useto_date/1,to_time/1,to_date_time/1, orto_naive_date_time/1.
Arguments
valueis at/0or aTempo.Duration.t/0.
Returns
{:ok, native}wherenativeis an ElixirDuration,Date,Time, orNaiveDateTime.{:error, t:Tempo.ConversionError.t/0}when the value cannot be represented by a single native type.
Examples
iex> Tempo.to_elixir(~o"PT8H")
{:ok, Duration.new!(hour: 8)}
iex> Tempo.to_elixir(~o"2026-06-15")
{:ok, ~D[2026-06-15]}
@spec to_interval( t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t() | Tempo.Duration.t(), keyword() ) :: {:ok, Tempo.Interval.t() | Tempo.IntervalSet.t()} | {:error, error_reason()}
Convert an implicit-span Elixir.Tempo.t/0 into the
equivalent explicit Tempo.Interval.t/0 or
Tempo.IntervalSet.t/0.
Every Tempo value represents a bounded interval on the time
line. ~o"2026-01" is the interval [2026-01-01, 2026-02-01)
— to_interval/1 materialises that implicit span as a pair of
concrete endpoints under the half-open [from, to) convention
(from inclusive, to exclusive). The span is one unit at the
value's resolution: a day value becomes a one-day interval, a
second value a one-second interval. This is the canonical
representation used by the set-operations API (union/2,
intersection/2, coalesce/1). See the
interop guide for how converted Elixir
date/time values materialise.
When the input expands to multiple disjoint spans — a set of
explicit values, a range over a time unit, a stepped range —
the result is a %Tempo.IntervalSet{} with intervals sorted
and coalesced. The conversion is idempotent on values that are
already explicit.
Arguments
valueis aElixir.Tempo.t/0,Tempo.Interval.t/0,Tempo.IntervalSet.t/0, orTempo.Set.t/0.
Options
:boundis a Tempo value whose upper endpoint limits expansion of an unbounded recurrence (recurrence: :infinitywith noUNTIL). Required to materialise such rules; ignored otherwise.:coalescecontrols whether the resulting IntervalSet merges adjacent or overlapping intervals (true, the default) or preserves each expanded occurrence as a distinct interval (false). Expansion consumers that care about event identity —Tempo.ICal, the RRULE expander — passfalse; ordinary implicit-span materialisation uses the default.
Returns
{:ok, interval}when the value materialises to a single contiguous span.{:ok, interval_set}when the value expands to multiple disjoint spans.{:error, reason}when the input cannot be materialised — a bareTempo.Duration(no anchor), aTempoat a resolution with no finer unit available to bound the span (microsecond materialises to a one-microsecond span; only exotic selector resolutions have no span), a one-ofTempo.Set(epistemic disjunction is not an interval list; the user must pick one or handle the disjunction themselves), or an unbounded recurrence with no:bound.
Examples
iex> {:ok, tempo} = Tempo.from_iso8601("2026-01")
iex> {:ok, interval} = Tempo.to_interval(tempo)
iex> interval.from.time
[year: 2026, month: 1, day: 1]
iex> interval.to.time
[year: 2026, month: 2, day: 1]
iex> {:ok, tempo} = Tempo.from_iso8601("156X")
iex> {:ok, interval} = Tempo.to_interval(tempo)
iex> {interval.from.time, interval.to.time}
{[year: 1560], [year: 1570]}
iex> {:ok, duration} = Tempo.from_iso8601("P3M")
iex> {:error, %Tempo.MaterialisationError{reason: :bare_duration}} = Tempo.to_interval(duration)
@spec to_interval!( t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t() | Tempo.Duration.t() ) :: Tempo.Interval.t() | Tempo.IntervalSet.t()
Raising version of to_interval/1.
Arguments
valueis aElixir.Tempo.t/0,Tempo.Interval.t/0,Tempo.IntervalSet.t/0, orTempo.Set.t/0.
Returns
- The materialised
Tempo.Interval.t/0orTempo.IntervalSet.t/0.
Raises
ArgumentErrorwhen the input cannot be materialised. Seeto_interval/1for the error cases.
Examples
iex> {:ok, tempo} = Tempo.from_iso8601("2026")
iex> interval = Tempo.to_interval!(tempo)
iex> {interval.from.time, interval.to.time}
{[year: 2026, month: 1], [year: 2027, month: 1]}
@spec to_interval_set( t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Set.t() | Tempo.Duration.t() ) :: {:ok, Tempo.IntervalSet.t()} | {:error, error_reason()}
Convert any Tempo value to a Tempo.IntervalSet.t/0.
Unlike to_interval/1 (which may return either a single
interval or an IntervalSet), to_interval_set/1 always returns
an IntervalSet — wrapping a single interval in a one-element
set if needed. This is the convenient form when the caller
wants a uniform shape (e.g. to pipe into set operations).
Arguments
valueis aElixir.Tempo.t/0,Tempo.Interval.t/0,Tempo.IntervalSet.t/0, orTempo.Set.t/0.
Returns
{:ok, interval_set}on success, or{:error, reason}for the same cases thatto_interval/1errors on.
Examples
iex> {:ok, tempo} = Tempo.from_iso8601("2026-01")
iex> {:ok, set} = Tempo.to_interval_set(tempo)
iex> length(set.intervals)
1
@spec to_iso8601(t() | Tempo.Interval.t() | Tempo.Duration.t() | Tempo.Set.t()) :: String.t()
Encode a Tempo value back into an ISO 8601-2 string.
The output uses the explicit-suffix form (2022Y11M20D), which
is a valid ISO 8601-2 / EDTF representation that round-trips
cleanly through from_iso8601/1. Constructs that exist only
in ISO 8601-2 Part 2 (seasons, groups, selections,
uncertainty qualifiers, unspecified digits) are preserved in
their explicit form.
IXDTF suffixes ([Europe/Paris], [u-ca=hebrew]) are not
emitted by this function — the :extended field is currently
ignored. Round-trip of IXDTF-enriched values is a future
extension.
Arguments
valueis aTempo.t/0,Tempo.Interval.t/0,Tempo.Duration.t/0, orTempo.Set.t/0.
Returns
- An ISO 8601-2 binary that parses back to the same AST.
Examples
iex> Tempo.from_iso8601!("2022-11-20") |> Tempo.to_iso8601()
"2022Y11M20D"
iex> Tempo.from_iso8601!("R5/2022-01-01/P1M") |> Tempo.to_iso8601()
"R5/2022Y1M1D/P1M"
iex> {:ok, i} = Tempo.from_iso8601("1984?/2004~")
iex> Tempo.to_iso8601(i)
"1984Y?/2004Y~"
@spec to_naive_date_time(t()) :: {:ok, NaiveDateTime.t()} | {:error, error_reason()}
Convert a Tempo struct into a NaiveDateTime.
A NaiveDateTime is a zoneless wall-clock reading, so a zoned
Tempo projects to its wall-clock components with the offset
dropped — the same lossy projection DateTime.to_naive/1
performs in the stdlib, and consistent with to_date/1 /
to_time/1. No time-zone math is involved: the wall-clock fields
are read verbatim. To keep the zone, use to_date_time/1; to
normalise to UTC wall time first, shift_zone(tempo, "Etc/UTC").
Arguments
tempois at/0resolved to at least second resolution (year through second, optionally microsecond). Coarser values cannot fill aNaiveDateTimeand return an error.
Returns
{:ok, naive_date_time}, or{:error, reason}when the value is not resolved to a full datetime.
Examples
iex> Tempo.to_naive_date_time(~o"2022-11-19T01:02:03")
{:ok, ~N[2022-11-19 01:02:03.000000]}
iex> {:error, _} = Tempo.to_naive_date_time(~o"2022-11")
@spec to_relative_string( t() | Tempo.Interval.t(), keyword() ) :: String.t()
Format a Tempo as a locale-aware relative time string like
"3 hours ago" or "in 2 days".
Routes through Localize's CLDR relativeTime patterns. The
reference point ("now") comes from Tempo.utc_now/0 unless
overridden with the :from option — which makes this safe to
use in tests via Tempo.Clock.Test.
For intervals, the :from endpoint of the interval is used as
the target — "the meeting starts in 2 hours" rather than
"lasts 2 hours" (for duration phrasing, use Tempo.to_string/2
on a Tempo.Duration).
Arguments
valueis at/0orTempo.Interval.t/0. The value must be anchored (have a year component); non-anchored values raiseTempo.NonAnchoredError.
Options
:fromis at/0— the reference point the output is relative to. Defaults toTempo.utc_now/0.:unitforces the output unit (:second,:minute,:hour,:day,:week,:month,:year). Omit to let Localize auto-derive.:formatis:standard,:narrow, or:short. Defaults to:standard.:localeis a CLDR locale. Defaults to Localize's configured default.
Returns
- A
String.t/0.
Examples
iex> now = Tempo.from_iso8601!("2026-06-15T12:00:00Z")
iex> Tempo.to_relative_string(~o"2026-06-14T12:00:00Z", from: now)
"yesterday"
iex> now = Tempo.from_iso8601!("2026-06-15T12:00:00Z")
iex> Tempo.to_relative_string(~o"2026-06-15T15:00:00Z", from: now)
"in 3 hours"
iex> now = Tempo.from_iso8601!("2026-06-15T12:00:00Z")
iex> Tempo.to_relative_string(~o"2026-06-10T12:00:00Z", from: now)
"5 days ago"
@spec to_rrule(Tempo.Interval.t() | term()) :: {:ok, String.t()} | {:error, Tempo.ConversionError.t()}
Encode a Tempo.Interval.t/0 into an RFC 5545 RRULE string.
The output does not include the leading RRULE: prefix,
nor a DTSTART property — RRULE is a recurrence pattern, not
a full iCalendar record. Callers wanting the full record
prepend DTSTART themselves using the interval's :from
field.
Supported inputs
A
%Tempo.Interval{}with a single-unit%Tempo.Duration{}cadence. Supported units::second,:minute,:hour,:day,:week,:month,:year.:recurrenceof:infinity(no COUNT), a positive integer (COUNT), or1combined with:to(UNTIL).:repeat_ruleofnil, or a%Tempo{}whose:timeholds a single{:selection, [...]}entry. Selection entries for:month,:day(→ BYMONTHDAY),:day_of_year,:week,:hour,:minute,:second, and the paired:day_of_week/:instance(→ BYDAY with optional ordinals) are encoded directly.
Returns
{:ok, rrule_string}on success.{:error, reason}when the interval cannot be expressed as an RRULE (e.g. multi-unit duration, unsupported selection entry).
Examples
iex> {:ok, i} = Tempo.RRule.parse("FREQ=DAILY;COUNT=10")
iex> Tempo.to_rrule(i)
{:ok, "COUNT=10;FREQ=DAILY"}
iex> {:ok, i} = Tempo.RRule.parse("FREQ=YEARLY;BYMONTH=11;BYDAY=4TH")
iex> Tempo.to_rrule(i)
{:ok, "FREQ=YEARLY;BYMONTH=11;BYDAY=4TH"}
iex> {:error, %Tempo.ConversionError{}} =
...> Tempo.to_rrule(Tempo.from_iso8601!("2022-06-15"))
@spec to_rrule!(Tempo.Interval.t()) :: String.t() | no_return()
Bang variant of to_rrule/1.
Returns
The RRULE string on success.
Raises
Tempo.ConversionErrorotherwise.
@spec to_string( t() | Tempo.Interval.t() | Tempo.IntervalSet.t() | Tempo.Duration.t(), keyword() ) :: String.t()
Format a Tempo value as a locale-aware string.
Routes through Localize so format patterns, month and weekday
names, day periods, and punctuation all follow CLDR data for
the chosen locale. The default format is keyed off the Tempo's
resolution — a year-only value renders as "2026", a month
value as "Jun 2026", a day value as "Jun 15, 2026", and so
on.
Tempo.to_string/1,2 is the end-user display function.
inspect/1 remains the programmer-facing form and returns the
~o"…" sigil representation unchanged.
Arguments
valueis at/0,Tempo.Interval.t/0, orTempo.IntervalSet.t/0.
Options
:formatis a CLDR format atom (:short | :medium | :long | :full), a skeleton atom (:yMMM,:yMMMd,:hm, …), or a pattern string. Defaults to a resolution-appropriate choice (see the module doc ofTempo.Formatfor the table).:localeis a CLDR locale identifier such as"en","en-GB", or"de". Defaults to Localize's configured default locale.Any other option accepted by
Localize.Date.to_string/2,Localize.Time.to_string/2,Localize.DateTime.to_string/2, orLocalize.Interval.to_string/3is forwarded verbatim.
Returns
- A
String.t/0.
Raises
- Any exception Localize raises for invalid locales, missing CLDR data, or unresolvable format skeletons.
Examples
iex> Tempo.to_string(~o"2026")
"Jan – Dec 2026"
iex> Tempo.to_string(~o"2026-06")
"Jun 1 – 30, 2026"
iex> Tempo.to_string(~o"2026-06-15")
"Jun 15, 2026"
iex> Tempo.to_string(~o"2026-06-15", format: :long)
"June 15, 2026"
iex> Tempo.to_string(~o"2026", format: :long)
"January – December 2026"
iex> Tempo.to_string(~o"P1Y6M")
"1 year and 6 months"
iex> Tempo.to_string(~o"P3DT2H", style: :short)
"3 days and 2 hr"
@spec to_time(t()) :: {:ok, Time.t()} | {:error, error_reason()}
Convert a Tempo struct into a Time.
@spec today(String.t()) :: t() | {:error, error_reason()}
Return today's date in the given IANA time zone as a
day-resolution t/0.
"Today" is zone-relative: at 11pm New York on the 14th it is already the 15th in Paris. This function answers the zone-local question.
Arguments
zoneis an IANA time zone name. Defaults to"Etc/UTC".
Returns
- A
t/0at day resolution whose wall date is the date inzoneat the current UTC instant.
Examples
iex> Tempo.today("Etc/UTC") |> Tempo.resolution()
{:day, 1}
@spec trunc(tempo :: t(), truncate_to :: time_unit()) :: t() | {:error, error_reason()}
Truncates a tempo struct to the specified resolution.
Truncation removes the time units that have a
higher resolution than the specified truncate_to
option.
Arguments
tempois anyElixir.Tempo.t/0.truncate_tois any time unit. The default is:day.
Returns
truncatedis a tempo struct that is truncated or{:error, reason}
Examples
iex> Tempo.trunc ~o"2022-11-21T09:30:00"
~o"2022Y11M21D"
iex> Tempo.trunc ~o"2022-11-21T09:30:00", :minute
~o"2022Y11M21DT9H30M"
iex> Tempo.trunc ~o"2022-11-21T09:30:00", :year
~o"2022Y"
Union of two Tempo values — every instant in either operand.
See Tempo.Operations.union/3 for full details.
@spec unit_min_max(tempo :: t() | token_list()) :: {time_unit(), time_unit()}
Returns the maximum and minimum time units as a 2-tuple.
Arguments
tempois anyElixir.Tempo.t/0.
Returns
{max_unit, min_unit}
Examples
iex> Tempo.unit_min_max ~o"2022Y1M2G3DU"
{:day, :year}
iex> Tempo.unit_min_max ~o"2022"
{:year, :year}
@spec utc_now() :: t()
Return the current UTC time as a second-resolution t/0
anchored in Etc/UTC.
Reads from the clock configured under :ex_tempo, :clock,
defaulting to Tempo.Clock.System. Tests that need determinism
should configure Tempo.Clock.Test — see its module doc.
Returns
- A
t/0at second resolution withshift: [hour: 0]andextended.zone_id: "Etc/UTC".
Examples
iex> tempo = Tempo.utc_now()
iex> tempo.extended.zone_id
"Etc/UTC"
iex> Tempo.utc_now() |> Tempo.resolution()
{:second, 1}
@spec utc_today() :: t() | {:error, error_reason()}
Return today's date in UTC as a day-resolution t/0.
Returns
- A
t/0at day resolution anchored inEtc/UTC.
Examples
iex> Tempo.utc_today() |> Tempo.resolution()
{:day, 1}
@spec validate_zone_offset(t()) :: :ok | {:error, Tempo.ZoneOffsetMismatchError.t()}
Check that an IXDTF value's explicit numeric offset agrees with its IANA time zone at the value's wall instant.
A value such as 2022-11-20T10:37:00+05:00[Europe/Paris] carries both
a numeric offset and a zone; Paris is +01:00 in November, so the
stated +05:00 is inconsistent. By default the zone wins and the
offset is consulted only for DST disambiguation; this surfaces the
disagreement instead (RFC 9557 §4.2). The same check backs the
strict: true option of from_iso8601/2.
Arguments
tempois at/0.
Returns
:okwhen the offset agrees, or when there is nothing to check (no zone, no explicit offset, or a non-anchored value).{:error, t:Tempo.ZoneOffsetMismatchError.t/0}on disagreement.
Examples
iex> {:ok, t} = Tempo.from_iso8601("2022-11-20T10:37:00+01:00[Europe/Paris]")
iex> Tempo.validate_zone_offset(t)
:ok
iex> {:error, %Tempo.ZoneOffsetMismatchError{}} =
...> Tempo.from_iso8601("2022-11-20T10:37:00+05:00[Europe/Paris]", strict: true)
@spec weekend(Tempo.Territory.input()) :: t()
Return a selector that matches the weekend days of a territory.
Different territories weekend on different days: the United
States is [Saturday, Sunday], Saudi Arabia is [Friday, Saturday], India is [Sunday]. Tempo.weekend/1 reads that
definition from CLDR via Localize and returns it as a
composable selector.
Arguments
territoryis an atom, string, locale, or%Localize.LanguageTag{}resolved throughTempo.Territory.resolve/1. Defaults tonil, which walks the territory-resolution chain.
Returns
- A
Tempo.t/0value carrying aday_of_weeklist. Composable directly withTempo.select/2.
Examples
iex> {:ok, us} = Tempo.select(~o"2026-02", Tempo.weekend(:US))
iex> us |> Tempo.IntervalSet.to_list() |> Enum.map(& &1.from.time[:day])
[1, 7, 8, 14, 15, 21, 22, 28]
iex> {:ok, sa} = Tempo.select(~o"2026-02", Tempo.weekend(:SA))
iex> sa |> Tempo.IntervalSet.to_list() |> Enum.map(& &1.from.time[:day])
[6, 7, 13, 14, 20, 21, 27, 28]
@spec weekend?(t(), Tempo.Territory.input()) :: boolean()
Return true when tempo falls on a weekend day in the given
territory.
Different territories weekend on different days — the United States
on [Saturday, Sunday], Saudi Arabia on [Friday, Saturday], India
on [Sunday] — read from CLDR via Localize. The day of week is taken
from Date.day_of_week/1 (ISO 1 = Monday … 7 = Sunday), computed
in the value's own calendar so a non-Gregorian value (Japanese,
Islamic, Indian, …) is classified by the correct weekday.
Arguments
tempois at/0that denotes a single day — a date or datetime, including the day-of-year and ISO week-date forms.territoryis an atom, string, locale, or%Localize.LanguageTag{}resolved throughTempo.Territory.resolve/1. Defaults tonil, which walks the territory-resolution chain (app config, then ambient locale).
Returns
truewhen the value's day of week is one of the territory's weekend days,falseotherwise.Raises
ArgumentErrorwhentempodoes not denote a day (for example a year- or month-resolution value).
Examples
iex> Tempo.weekend?(~o"2026-06-13", :US)
true
iex> Tempo.weekend?(~o"2026-06-15", :US)
false
iex> # The same Friday: a weekend in Saudi Arabia, a workday in the US.
iex> Tempo.weekend?(~o"2026-06-12", :SA)
true
iex> Tempo.weekend?(~o"2026-06-12", :US)
false
true when a fits inside b inclusive of shared
endpoints. The canonical "does this fit inside that window?"
predicate. See Tempo.Interval.within?/2.
The three-valued certainty that a falls within b, given their
± margins. See Tempo.Interval.within_certainty/2.
@spec workday?(t(), Tempo.Territory.input()) :: boolean()
Return true when tempo falls on a workday — a day that is not
in the territory's weekend.
The complement of weekend?/2; together they partition the week.
This is the weekend/weekday distinction only — it does not consult
public-holiday calendars.
Arguments
tempois at/0that denotes a single day.territoryis resolved throughTempo.Territory.resolve/1, as forweekend?/2.
Returns
truewhen the value's day of week is not a weekend day in the territory,falseotherwise.
Examples
iex> Tempo.workday?(~o"2026-06-15", :US)
true
iex> Tempo.workday?(~o"2026-06-13", :US)
false
@spec workdays(Tempo.Territory.input()) :: t()
Return a selector that matches the workdays of a territory — the days of week that are not in that territory's weekend.
Together, workdays/1 and weekend/1 partition the seven days
of the week: workdays(:US) ++ weekend(:US) spans Monday..Sunday.
Arguments
territoryis an atom, string, locale, or%Localize.LanguageTag{}resolved throughTempo.Territory.resolve/1. Defaults tonil, which walks the territory-resolution chain (app config, then ambient locale).
Returns
- A
Tempo.t/0value carrying aday_of_weeklist. Composable directly withTempo.select/2.
Examples
iex> {:ok, set} = Tempo.select(~o"2026-02", Tempo.workdays(:US))
iex> Tempo.IntervalSet.count(set)
20
iex> Tempo.workdays(:US).time
[day_of_week: [1, 2, 3, 4, 5]]
@spec working_days_in(Tempo.Interval.t(), Tempo.Territory.input()) :: non_neg_integer()
Count the working days within an interval, excluding the territory's weekend.
The interval is half-open [from, to), so the result is the number
of working days from its start up to but not including its end. Any
day-yielding value works the same way through Enum —
Enum.count(value, &Tempo.workday?(&1, territory)).
Arguments
intervalis aTempo.Interval.t/0whose boundaries denote days.territoryis resolved throughTempo.Territory.resolve/1.
Returns
- the count of working days in the interval.
Examples
iex> {:ok, june} = Tempo.Interval.new(from: ~o"2026-06-01", to: ~o"2026-07-01")
iex> Tempo.working_days_in(june, :US)
22
@spec year(t() | Tempo.Interval.t()) :: integer() | nil
Return the year component of a Tempo value, or nil if
the value doesn't specify one.
The accessors (year/1, month/1, day/1, hour/1,
minute/1, second/1) are commodity component extractors so
callers never have to reach into struct fields in user-facing
code.
Arguments
valueis at/0orTempo.Interval.t/0.
Returns
The component value as an integer when unambiguous.
nilwhen the value doesn't specify that unit (e.g.Tempo.day(~o"2026")returnsnil— the year value has no day).Raises
ArgumentErrorwhen called on an interval whose span covers multiple values of that unit (e.g.Tempo.day/1on a month-spanning interval is ambiguous).
Examples
iex> Tempo.year(~o"2026-06-15T10:30:45")
2026
iex> Tempo.year(~o"2026")
2026