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
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.
Complement of a Tempo value within a bounding universe. The
:bound option is required. See Tempo.Operations.complement/2.
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 interval's length as a %Tempo.Duration{}, or
:infinity for unbounded intervals. See Tempo.Interval.duration/1.
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.
Return the current time in the given IANA time zone as a
second-resolution t/0.
true when a and b share at least one instant.
See Tempo.Operations.overlaps?/3.
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.
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 Tempo struct into a Date.
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.
Return a selector that matches the weekend days of a 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.
Return a selector that matches the workdays of a territory — the days of week that are not in that 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
Functions
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.
Complement of a Tempo value within a bounding universe. The
:bound option is required. See Tempo.Operations.complement/2.
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 interval's length as a %Tempo.Duration{}, or
:infinity for unbounded intervals. See Tempo.Interval.duration/1.
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, visualizer components),
use Tempo.Explain.explain/1 directly and pick a formatter.
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.
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→ the finest Tempo-supported component that is non-zero. If all time components are zero (e.g. midnight on a date), the resolution falls back to:dayfor datetime types or:hourfor a bareTime.t. Microsecond is discarded (Tempo does not yet model sub-second resolution).
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"T10H30M"
iex> Tempo.from_elixir(~N[2022-06-15 10:30:00])
~o"2022Y6M15DT10H30M"
iex> Tempo.from_elixir(~N[2022-06-15 00:00:00])
~o"2022Y6M15D"
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()}
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_iso8601!(string :: String.t(), calendar :: Calendar.calendar()) :: t() | no_return()
@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 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()) :: {: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 mixing time-scale components and options. At least one time-scale component must be present.
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)
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]
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"
true when a and b share at least one instant.
See Tempo.Operations.overlaps?/3.
@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
@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}
Shift a t/0 by a keyword list of signed unit amounts,
returning a new t/0.
This is the ergonomic companion to Tempo.Math.add/2 — the
Duration-based API remains the principled path (durations carry
their own calendar and leap-second semantics), but for ad-hoc
shifts a keyword list reads more naturally.
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.unitsis 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. Amounts may be negative.
Returns
- The shifted
t/0.
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"
@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.
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_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). This is the canonical
representation used by the upcoming set-operations API
(union/2, intersection/2, coalesce/1).
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), aTempoalready at its finest resolution (no finer unit to bound the 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~"
Convert a Tempo struct into a NaiveDateTime.
@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"
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 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]
true when a fits inside b inclusive of shared
endpoints. The canonical "does this fit inside that window?"
predicate. See Tempo.Interval.within?/2.
@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 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