Tempo (Tempo v0.5.0)

Copy Markdown View Source

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.

t()

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 Date.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 Time.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.

Shift a t/0 by a keyword list of signed unit amounts, returning a new t/0.

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 the current UTC time as a second-resolution t/0 anchored in Etc/UTC.

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

error_reason()

@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.

extended_info()

@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 from u-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-ca elective tagged suffixes.

qualification()

@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 (%).

  • nil when no qualification was supplied.

qualifications()

@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.

t()

@type t() :: %Tempo{
  calendar: Calendar.calendar() | nil,
  extended: extended_info() | nil,
  qualification: qualification(),
  qualifications: qualifications(),
  shift: time_shift(),
  time: token_list()
}

time_shift()

@type time_shift() :: [hour: integer(), minute: integer()] | nil

time_unit()

@type time_unit() :: :year | :month | :week | :day | :hour | :minute | :second

token()

@type token() :: integer() | list() | tuple()

token_list()

@type token_list() :: [
  year: token(),
  month: token(),
  week: token(),
  day: token(),
  day_of_year: token(),
  day_of_week: token() | [integer()],
  hour: token(),
  minute: token(),
  second: token()
]

Functions

adjacent?(a, b)

true when the two intervals touch at a single boundary (Allen's :meets | :met_by). See Tempo.Interval.adjacent?/2.

after?(a, b)

true when a starts strictly after b ends (Allen's :preceded_by). See Tempo.Interval.after?/2.

anchor(anchored, non_anchored)

@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

  • anchored is an anchored Elixir.Tempo.t/0 (has a year component) — typically a date like ~o"2026-01-04".

  • non_anchored is a non-anchored Elixir.Tempo.t/0 (pure time-of-day) — typically a time like ~o"T10:30".

Returns

  • A new t/0 combining the two. The date components come from anchored; the time components come from non_anchored.

Raises

  • ArgumentError when either argument has the wrong anchor class — if anchored is non-anchored or non_anchored is already anchored.

Examples

iex> Tempo.anchor(~o"2026-01-04", ~o"T10:30")
~o"2026Y1M4DT10H30M"

anchored?(tempo)

@spec anchored?(tempo :: t()) :: boolean()

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

Returns

  • true or false

Examples

iex> Tempo.anchored? ~o"2022"
true

iex> Tempo.anchored? ~o"2M"
false

at_least?(interval, duration)

true when the interval is at least as long as the given duration. See Tempo.Interval.at_least?/2.

at_most?(interval, duration)

true when the interval is at most as long as the given duration. See Tempo.Interval.at_most?/2.

at_resolution(tempo, target_unit)

@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

  • tempo is any Elixir.Tempo.t/0.

  • target_unit is 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"

before?(a, b)

true when a ends strictly before b starts (Allen's :precedes). See Tempo.Interval.before?/2.

beginning_of_day(tempo)

@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

  • tempo is a t/0 with 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"

beginning_of_month(tempo)

@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

  • tempo is a t/0 with 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"

bounded?(interval)

true when both endpoints of the interval are concrete (neither :undefined nor nil). See Tempo.Interval.bounded?/1.

complement(set, opts)

Complement of a Tempo value within a bounding universe. The :bound option is required. See Tempo.Operations.complement/2.

contains?(a, b, opts \\ [])

true when every instant of b is also in a. Alias for subset?(b, a, opts). See Tempo.Operations.contains?/3.

day(value)

@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

Returns

  • The component value as an integer when unambiguous.

  • nil when the value doesn't specify that unit (e.g. Tempo.day(~o"2026") returns nil — the year value has no day).

  • Raises ArgumentError when called on an interval whose span covers multiple values of that unit (e.g. Tempo.day/1 on a month-spanning interval is ambiguous).

Examples

iex> Tempo.day(~o"2026-06-15T10:30:45")
15

iex> Tempo.day(~o"2026")
nil

day_of_week(tempo, starting_on \\ :default)

@spec day_of_week(t(), atom()) :: 1..7

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

  • tempo is a t/0 anchored with at least year/month/day components.

  • starting_on is a day-of-week atom controlling which day is numbered 1. Accepts :default (calendar's default), :monday, :tuesday, :wednesday, :thursday, :friday, :saturday, or :sunday. Defaults to :default.

Returns

  • Integer 1..7.

Raises

Examples

iex> Tempo.day_of_week(~o"2026-06-15")
1

iex> Tempo.day_of_week(~o"2026-06-15", :sunday)
2

day_of_year(tempo)

@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

  • tempo is a t/0 anchored with at least year/month/day components.

Returns

  • A positive integer.

Raises

Examples

iex> Tempo.day_of_year(~o"2026-01-01")
1

iex> Tempo.day_of_year(~o"2024-12-31")
366

days_in_month(tempo)

@spec days_in_month(t()) :: pos_integer()

Return the number of days in the Tempo's month under its calendar.

Arguments

  • tempo is a t/0 with year and month components.

Returns

  • A positive integer.

Raises

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, opts \\ [])

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.

disjoint?(a, b, opts \\ [])

true when a and b share no instants. See Tempo.Operations.disjoint?/3.

duration(interval)

Return the interval's length as a %Tempo.Duration{}, or :infinity for unbounded intervals. See Tempo.Interval.duration/1.

during?(a, b)

true when a is strictly inside b (Allen's :during). See Tempo.Interval.during?/2.

empty?(interval)

true when the interval has zero length. See Tempo.Interval.empty?/1.

end_of_day(tempo)

@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

  • tempo is a t/0 with 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"

end_of_month(tempo)

@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

  • tempo is a t/0 with 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"

equal?(a, b, opts \\ [])

true when a and b span the same instants (at their aligned resolution). See Tempo.Operations.equal?/3.

exactly?(interval, duration)

true when the interval's length equals the given duration. See Tempo.Interval.exactly?/2.

explain(value)

@spec explain(term()) :: String.t()

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.

extend(tempo, unit \\ nil)

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"}

extend!(tempo, unit \\ nil)

extend_resolution(tempo, target_unit)

@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

  • tempo is any Elixir.Tempo.t/0.

  • target_unit is the finer resolution to pad to. Must be finer than or equal to tempo's current resolution.

Returns

  • The padded t/0, or

  • {:error, reason} when target_unit is coarser than the current resolution (use trunc/2 for that direction) or when no path exists from the current unit to target_unit under the tempo's calendar.

Examples

iex> Tempo.extend_resolution(~o"2020Y", :day)
~o"2020Y1M1D"

iex> Tempo.extend_resolution(~o"2020Y6M", :hour)
~o"2020Y6M1DT0H"

from_date(map)

@spec from_date(date :: Date.t()) :: t()

Creates a Tempo.t/0 struct from a Date.t/0.

Arguments

Returns

  • t/0 or

  • {:error, reason}

Examples

iex> Tempo.from_date ~D[2022-11-20]
~o"2022Y11M20D"

from_date_time(date_time)

@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

Returns

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"

from_elixir(value, options \\ [])

@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 :day for datetime types or :hour for a bare Time.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

Options

  • :resolution is a time unit atom (:year, :month, :day, :hour, :minute, :second) overriding the inferred resolution.

Returns

  • The t/0 at the chosen resolution, or

  • {:error, reason} if :resolution is 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"

from_iso8601(string)

@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

Returns

  • {:ok, t} where the returned struct's :extended field is populated when an IXDTF suffix was parsed, or nil otherwise.

  • {: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]")

from_iso8601(string, calendar)

@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()}

from_iso8601!(string)

@spec from_iso8601!(string :: String.t()) :: t() | no_return()

Creates a Tempo.t/0 struct from an ISO8601 string.

The parser supports the vast majority of ISO8601 parts 1 and 2.

Arguments

Returns

  • t/0 or

  • raises an exception

Examples

iex> Tempo.from_iso8601!("2022-11-20")
~o"2022Y11M20D"

iex> Tempo.from_iso8601!("2022Y")
~o"2022Y"

from_iso8601!(string, calendar)

@spec from_iso8601!(string :: String.t(), calendar :: Calendar.calendar()) ::
  t() | no_return()

from_naive_date_time(map)

@spec from_naive_date_time(naive_date_time :: NaiveDateTime.t()) :: t()

Creates a Tempo.t/0 struct from a NaiveDateTime.t/0.

Arguments

Returns

  • t/0 or

  • {:error, reason}

Examples

iex> Tempo.from_naive_date_time ~N[2022-11-20 10:37:00]
~o"2022Y11M20DT10H37M0S"

from_time(map)

@spec from_time(time :: Time.t()) :: t()

Creates a Tempo.t/0 struct from a Time.t/0.

Arguments

Returns

  • t/0 or

  • {:error, reason}

Examples

iex> Tempo.from_time ~T[10:09:00]
~o"T10H9M0S"

hour(value)

@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

Returns

  • The component value as an integer when unambiguous.

  • nil when the value doesn't specify that unit (e.g. Tempo.day(~o"2026") returns nil — the year value has no day).

  • Raises ArgumentError when called on an interval whose span covers multiple values of that unit (e.g. Tempo.day/1 on a month-spanning interval is ambiguous).

Examples

iex> Tempo.hour(~o"2026-06-15T10:30:45")
10

iex> Tempo.hour(~o"2026")
nil

intersection(a, b, opts \\ [])

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.

leap_year?(tempo)

@spec leap_year?(t()) :: boolean()

Return true when the Tempo's year is a leap year under its calendar.

Arguments

  • tempo is a t/0 with at least a year component.

Returns

  • true or false.

Raises

Examples

iex> Tempo.leap_year?(~o"2024")
true

iex> Tempo.leap_year?(~o"2025")
false

longer_than?(interval, duration)

true when the interval is strictly longer than the given duration. See Tempo.Interval.longer_than?/2.

meets?(a, b)

true when a's end coincides exactly with b's start (Allen's :meets). See Tempo.Interval.meets?/2.

members_in_exactly_one(a, b, opts \\ [])

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.

members_outside(a, b, opts \\ [])

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.

members_overlapping(a, b, opts \\ [])

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.

merge(base, from)

minute(value)

@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

Returns

  • The component value as an integer when unambiguous.

  • nil when the value doesn't specify that unit (e.g. Tempo.day(~o"2026") returns nil — the year value has no day).

  • Raises ArgumentError when called on an interval whose span covers multiple values of that unit (e.g. Tempo.day/1 on a month-spanning interval is ambiguous).

Examples

iex> Tempo.minute(~o"2026-06-15T10:30:45")
30

iex> Tempo.minute(~o"2026")
nil

month(value)

@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

Returns

  • The component value as an integer when unambiguous.

  • nil when the value doesn't specify that unit (e.g. Tempo.day(~o"2026") returns nil — the year value has no day).

  • Raises ArgumentError when called on an interval whose span covers multiple values of that unit (e.g. Tempo.day/1 on a month-spanning interval is ambiguous).

Examples

iex> Tempo.month(~o"2026-06-15T10:30:45")
6

iex> Tempo.month(~o"2026")
nil

new(components)

@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

  • components is 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.

  • :year is the calendar year.

  • :month is the calendar month (Gregorian axis).

  • :week is the ISO week number (ISO-week axis).

  • :day is the day of month (Gregorian axis).

  • :day_of_year is the ordinal day within the year (ordinal axis).

  • :day_of_week is the ISO day-of-week number (ISO-week axis).

  • :hour is the clock hour 0..23.

  • :minute is the clock minute 0..59.

  • :second is the clock second 0..59 (or 60 on a leap-second date).

Options

  • :calendar is the Calendrical calendar module used to interpret and validate the components. Defaults to Calendrical.Gregorian.

  • :zone is an IANA time-zone name as a binary (e.g. "Australia/Sydney"). Sets extended.zone_id. Requires at least one of :hour, :minute, :second to be present — a zoned value without a time of day has no UTC projection.

  • :shift is a manual UTC offset expressed as [hour: n] or [hour: n, minute: m].

  • :qualification marks the value's EDTF qualification. One of :uncertain, :approximate, or :uncertain_and_approximate.

  • :metadata is a free-form map attached to extended.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)

new!(components)

@spec new!(keyword()) :: t()

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]

now(zone \\ "Etc/UTC")

@spec now(String.t()) :: t()

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

  • zone is an IANA time zone name (e.g. "Europe/Paris", "America/New_York"). Defaults to "Etc/UTC", in which case now/1 is equivalent to utc_now/0.

Returns

  • A t/0 at second resolution with extended.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"

overlaps?(a, b, opts \\ [])

true when a and b share at least one instant. See Tempo.Operations.overlaps?/3.

quarter_of_year(tempo)

@spec quarter_of_year(t()) :: 1..4

Return the 1-based quarter of the year (1..4) for Gregorian-like calendars.

Arguments

  • tempo is a t/0 anchored with at least year/month components.

Returns

  • An integer 1..4.

Raises

Examples

iex> Tempo.quarter_of_year(~o"2026-01-15")
1

iex> Tempo.quarter_of_year(~o"2026-11-30")
4

relation(a, b)

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

resolution(tempo)

@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

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}

round(tempo, round_to \\ :day)

@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

Returns

  • rounded is 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"

second(value)

@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

Returns

  • The component value as an integer when unambiguous.

  • nil when the value doesn't specify that unit (e.g. Tempo.day(~o"2026") returns nil — the year value has no day).

  • Raises ArgumentError when called on an interval whose span covers multiple values of that unit (e.g. Tempo.day/1 on a month-spanning interval is ambiguous).

Examples

iex> Tempo.second(~o"2026-06-15T10:30:45")
45

iex> Tempo.second(~o"2026")
nil

select(base, selector)

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(tempo, units)

@spec shift(
  t(),
  keyword()
) :: t()

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

  • tempo is any t/0.

  • units is 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"

shift_zone(tempo, target_zone)

@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

  • tempo is a t/0 that carries zone information — either an IANA zone on extended.zone_id, a numeric zone_offset, or a shift keyword list. A floating Tempo (no zone info) cannot be projected because its UTC instant is undefined.

  • target_zone is an IANA zone name ("Europe/Paris", "America/New_York", "Etc/UTC", …).

Returns

  • {:ok, tempo} at second resolution in target_zone, or

  • {:error, reason} when tempo is not zoned or target_zone is 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]

shorter_than?(interval, duration)

true when the interval is strictly shorter than the given duration. See Tempo.Interval.shorter_than?/2.

split(tempo)

Split a tempo struct into a date and time.

subset?(a, b, opts \\ [])

true when every instant of a is also in b. See Tempo.Operations.subset?/3.

symmetric_difference(a, b, opts \\ [])

Symmetric difference a △ b — instants in exactly one of the two operands. Trimmed/instant-level. See Tempo.Operations.symmetric_difference/3.

to_calendar(tempo)

to_date(value)

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. D and O both parse to the same :day key, 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]

to_interval(value, opts \\ [])

@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

Options

  • :bound is a Tempo value whose upper endpoint limits expansion of an unbounded recurrence (recurrence: :infinity with no UNTIL). Required to materialise such rules; ignored otherwise.

  • :coalesce controls 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 — pass false; 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 bare Tempo.Duration (no anchor), a Tempo already at its finest resolution (no finer unit to bound the span), a one-of Tempo.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)

to_interval!(value)

Raising version of to_interval/1.

Arguments

Returns

Raises

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]}

to_interval_set(value)

@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

Returns

  • {:ok, interval_set} on success, or {:error, reason} for the same cases that to_interval/1 errors on.

Examples

iex> {:ok, tempo} = Tempo.from_iso8601("2026-01")
iex> {:ok, set} = Tempo.to_interval_set(tempo)
iex> length(set.intervals)
1

to_iso8601(value)

@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

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~"

to_naive_date_time(value)

Convert a Tempo struct into a NaiveDateTime.

to_relative_string(value, options \\ [])

@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

Options

  • :from is a t/0 — the reference point the output is relative to. Defaults to Tempo.utc_now/0.

  • :unit forces the output unit (:second, :minute, :hour, :day, :week, :month, :year). Omit to let Localize auto-derive.

  • :format is :standard, :narrow, or :short. Defaults to :standard.

  • :locale is a CLDR locale. Defaults to Localize's configured default.

Returns

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"

to_rrule(interval)

@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.

  • :recurrence of :infinity (no COUNT), a positive integer (COUNT), or 1 combined with :to (UNTIL).

  • :repeat_rule of nil, or a %Tempo{} whose :time holds 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"))

to_rrule!(value)

@spec to_rrule!(Tempo.Interval.t()) :: String.t() | no_return()

Bang variant of to_rrule/1.

Returns

to_string(value, options \\ [])

@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

Options

Returns

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"

to_time(value)

Convert a Tempo struct into a Time.

today(zone \\ "Etc/UTC")

@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

  • zone is an IANA time zone name. Defaults to "Etc/UTC".

Returns

  • A t/0 at day resolution whose wall date is the date in zone at the current UTC instant.

Examples

iex> Tempo.today("Etc/UTC") |> Tempo.resolution()
{:day, 1}

trunc(tempo, truncate_to \\ :day)

@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

  • tempo is any Elixir.Tempo.t/0.

  • truncate_to is any time unit. The default is :day.

Returns

  • truncated is 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(a, b, opts \\ [])

Union of two Tempo values — every instant in either operand. See Tempo.Operations.union/3 for full details.

unit_min_max(units)

@spec unit_min_max(tempo :: t() | token_list()) :: {time_unit(), time_unit()}

Returns the maximum and minimum time units as a 2-tuple.

Arguments

Returns

  • {max_unit, min_unit}

Examples

  iex> Tempo.unit_min_max ~o"2022Y1M2G3DU"
  {:day, :year}

  iex> Tempo.unit_min_max ~o"2022"
  {:year, :year}

utc_now()

@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/0 at second resolution with shift: [hour: 0] and extended.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}

utc_today()

@spec utc_today() :: t() | {:error, error_reason()}

Return today's date in UTC as a day-resolution t/0.

Returns

  • A t/0 at day resolution anchored in Etc/UTC.

Examples

iex> Tempo.utc_today() |> Tempo.resolution()
{:day, 1}

weekend(territory \\ nil)

@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

  • territory is an atom, string, locale, or %Localize.LanguageTag{} resolved through Tempo.Territory.resolve/1. Defaults to nil, which walks the territory-resolution chain.

Returns

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]

within?(a, b)

true when a fits inside b inclusive of shared endpoints. The canonical "does this fit inside that window?" predicate. See Tempo.Interval.within?/2.

workdays(territory \\ nil)

@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

  • territory is an atom, string, locale, or %Localize.LanguageTag{} resolved through Tempo.Territory.resolve/1. Defaults to nil, which walks the territory-resolution chain (app config, then ambient locale).

Returns

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]]

year(value)

@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

Returns

  • The component value as an integer when unambiguous.

  • nil when the value doesn't specify that unit (e.g. Tempo.day(~o"2026") returns nil — the year value has no day).

  • Raises ArgumentError when called on an interval whose span covers multiple values of that unit (e.g. Tempo.day/1 on a month-spanning interval is ambiguous).

Examples

iex> Tempo.year(~o"2026-06-15T10:30:45")
2026

iex> Tempo.year(~o"2026")
2026