Tempo (Tempo v0.20.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

Shift tempo by count working days, skipping the territory's weekend.

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

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

Combine a date-like value with a time-of-day value into a datetime.

Returns a boolean indicating if a Tempo.t/0 struct is anchored to the timeline.

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

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

Return a Tempo at the specified resolution, dispatching to trunc/2 or extend_resolution/2 based on whether target_unit is coarser or finer than the current resolution.

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

Return a second-resolution t/0 at the start (00:00:00) of the day that contains tempo.

Return a second-resolution t/0 at the start of the month (YYYY-MM-01T00:00:00) that contains tempo.

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

true when a starts after b ends for every placement of their ± margins. See Tempo.Interval.certainly_after?/2.

true when a ends before b starts for every placement of their ± margins. See Tempo.Interval.certainly_before?/2.

true when a and b intersect for every placement of their ± margins. See Tempo.Interval.certainly_overlaps?/2.

true when a falls within b for every placement of their ± margins. See Tempo.Interval.certainly_within?/2.

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

Compose two Allen relations — the relations possible from A to C given A r1 B and B r2 C.

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

Return the day component of a Tempo value, or nil if the value doesn't specify one.

Return the day of the week as an integer (1..7) using the Tempo value's calendar.

Return the 1-based ordinal day of the year (1..365 or 1..366 in a leap year) using the Tempo value's calendar.

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

Difference a \ b — every instant in a that is not in b. Each result interval is the trimmed remainder; a members can split into multiple fragments. See Tempo.Operations.difference/3.

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

Return the length of an interval — or the total covered length of an interval set — as a %Tempo.Duration{}.

Return the duration between two endpoints as a %Tempo.Duration{}.

Bang variant of duration/2. Raises on invalid endpoints and returns the %Tempo.Duration{} directly.

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

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

Return a second-resolution t/0 at the exclusive end of the day that contains tempo — i.e. 00:00:00 of the following day.

Return a second-resolution t/0 at the exclusive end of the month that contains tempo — i.e. the first day of the following month at 00:00:00.

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

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

Return a multi-line prose explanation of any Tempo value — what it is, what it spans, and how to work with it.

Adds an extended enumeration to a Tempo.

Extend a Tempo's resolution by padding finer units with their start-of-unit minimum values.

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

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

The next working day strictly after tempo in the territory.

Return the current time in the given IANA time zone as a second-resolution t/0.

The three-valued certainty that a and b intersect, given their ± margins. See Tempo.Interval.overlap_certainty/2.

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

Parse a locale-formatted date, time, datetime, or interval string into a Tempo value.

Bang variant of parse/2. Raises on parse failure or invalid component combination.

true when a could start after b ends for some placement of their ± margins. See Tempo.Interval.possibly_after?/2.

true when a could end before b starts for some placement of their ± margins. See Tempo.Interval.possibly_before?/2.

true when a and b could intersect for some placement of their ± margins. See Tempo.Interval.possibly_overlaps?/2.

true when a could fall within b for some placement of their ± margins. See Tempo.Interval.possibly_within?/2.

The working day immediately before tempo in the territory.

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

Classify the Allen interval-algebra relation between two interval-like values.

The three-valued certainty that relation(a, b) is (one of) target. See Tempo.Interval.relation_certainty/3.

Returns the resolution of a Tempo.t/0 struct.

Truncates a tempo struct to the specified resolution.

Return the second component of a Tempo value, or nil if the value doesn't specify one.

Narrow a Tempo span by a selector — the composition primitive for "workdays of June", "the 15th of every month", and similar queries.

Shift a t/0 by a duration, 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 t/0 to its best-fit native Elixir calendar type.

Convert a Tempo struct into a Date.

Convert a zoned Tempo into a DateTime.

Convert a Tempo value to its native Elixir equivalent — the outbound mirror of from_elixir/2.

Convert an implicit-span Elixir.Tempo.t/0 into the equivalent explicit Tempo.Interval.t/0 or Tempo.IntervalSet.t/0.

Raising version of to_interval/1.

Convert any Tempo value to a Tempo.IntervalSet.t/0.

Encode a Tempo value back into an ISO 8601-2 string.

Convert a Tempo struct into a NaiveDateTime.

Format a Tempo as a locale-aware relative time string like "3 hours ago" or "in 2 days".

Encode a Tempo.Interval.t/0 into an RFC 5545 RRULE string.

Bang variant of to_rrule/1.

Format a Tempo value as a locale-aware string.

Convert a Tempo struct into a Time.

Return today's date in the given IANA time zone as a day-resolution t/0.

Truncates a tempo struct to the specified resolution.

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

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

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

Check that an IXDTF value's explicit numeric offset agrees with its IANA time zone at the value's wall instant.

Return a selector that matches the weekend days of a territory.

Return true when tempo falls on a weekend day in the given territory.

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

The three-valued certainty that a falls within b, given their ± margins. See Tempo.Interval.within_certainty/2.

Return true when tempo falls on a workday — a day that is not in the territory's weekend.

Return a selector that matches the workdays of a territory — the days of week that are not in that territory's weekend.

Count the working days within an interval, excluding the territory's weekend.

Return the year component of a Tempo value, or nil if the value doesn't specify one.

Types

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 | :microsecond

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(),
  microsecond: Tempo.Microsecond.t()
]

Functions

add_working_days(tempo, count, territory \\ nil)

@spec add_working_days(t(), integer(), Tempo.Territory.input()) :: t()

Shift tempo by count working days, skipping the territory's weekend.

A positive count moves forward, a negative count backward, and 0 returns the value unchanged. Each counted step lands on a working day, so adding one working day to a Friday returns the following Monday (in a Saturday/Sunday-weekend territory). The time of day, calendar, and zone are preserved.

Arguments

  • tempo is a t/0 that denotes a single day (a date or datetime).

  • count is the integer number of working days to add — negative to subtract.

  • territory is resolved through Tempo.Territory.resolve/1, as for weekend?/2.

Returns

  • a t/0 that is count working days from tempo.

Examples

iex> Tempo.add_working_days(~o"2026-06-12", 1, :US)
~o"2026Y6M15D"

iex> Tempo.add_working_days(~o"2026-06-15", -1, :US)
~o"2026Y6M12D"

iex> # Five working days on from a Monday is the next Monday.
iex> Tempo.add_working_days(~o"2026-06-15", 5, :US)
~o"2026Y6M22D"

iex> # The weekend differs by territory (Saudi Arabia: Friday/Saturday).
iex> Tempo.add_working_days(~o"2026-06-11", 1, :SA)
~o"2026Y6M14D"

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.

certainly_after?(a, b)

true when a starts after b ends for every placement of their ± margins. See Tempo.Interval.certainly_after?/2.

certainly_before?(a, b)

true when a ends before b starts for every placement of their ± margins. See Tempo.Interval.certainly_before?/2.

certainly_overlaps?(a, b)

true when a and b intersect for every placement of their ± margins. See Tempo.Interval.certainly_overlaps?/2.

certainly_within?(a, b)

true when a falls within b for every placement of their ± margins. See Tempo.Interval.certainly_within?/2.

complement(set, opts)

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

compose(relation1, relation2)

Compose two Allen relations — the relations possible from A to C given A r1 B and B r2 C.

Thin delegate to Tempo.Interval.compose/2. Allen's composition (1983) chains a qualitative inference without holding any interval: "if A precedes B and B is during C, how can A relate to C?"

Examples

iex> Tempo.compose(:precedes, :during)
[:precedes, :meets, :overlaps, :starts, :during]

iex> Tempo.compose(:contains, :during)
[:overlaps, :finished_by, :contains, :starts, :equals, :started_by, :during, :finishes, :overlapped_by]

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(set)

Return the length of an interval — or the total covered length of an interval set — as a %Tempo.Duration{}.

Unbounded intervals return :infinity. See Tempo.Interval.duration/1 and Tempo.IntervalSet.duration/1.

duration(from, to)

@spec duration(t(), t()) :: {:ok, Tempo.Duration.t()} | {:error, Exception.t()}

Return the duration between two endpoints as a %Tempo.Duration{}.

A convenience that builds the interval [from, to) internally and measures it — Tempo.duration(now, deadline) instead of constructing a Tempo.Interval.t/0 first. The length is measured on the UTC time line, so zoned endpoints spanning a DST transition yield the true elapsed duration (a 23- or 25-hour day), not the wall-clock difference.

Unlike duration/1, whose interval argument is valid by construction, this takes raw endpoints that may not form a measurable interval, so it returns a tagged tuple. Use duration!/2 when the endpoints are known-good.

Arguments

  • from is the start Tempo.t/0 — must be anchored (carry a year).

  • to is the end Tempo.t/0 — anchored, and strictly later than from.

Returns

  • {:ok, duration} where duration is a Tempo.Duration.t/0.

  • {:error, reason} when an endpoint is non-anchored, the endpoints are of incompatible calendars, or from is not strictly earlier than to.

Examples

iex> {:ok, duration} = Tempo.duration(~o"2026-06-15T09", ~o"2026-06-15T17")
iex> duration
~o"PT28800S"

iex> match?({:error, _reason}, Tempo.duration(~o"2026-06-15T17", ~o"2026-06-15T09"))
true

duration!(from, to)

@spec duration!(t(), t()) :: Tempo.Duration.t()

Bang variant of duration/2. Raises on invalid endpoints and returns the %Tempo.Duration{} directly.

Examples

iex> Tempo.duration!(~o"2026-06-15T09", ~o"2026-06-15T17")
~o"PT28800S"

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), use Tempo.Explain.explain/1 directly and pick a formatter.

extend(tempo, unit \\ nil)

@spec extend(t(), nil) :: {:ok, t()} | {:error, error_reason()}

Adds an extended enumeration to a Tempo.

This has the effect of increasing the resolution of the the Tempo struct but still covering the same interval.

Example

iex> Tempo.extend(~o"2020")
{:ok, ~o"2020Y{1..12}M"}

extend!(tempo, unit \\ nil)

@spec extend!(t(), nil) :: t()

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(dt)

@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. See the interop guide for what the resulting value spans once materialised with to_interval/1.

The intended resolution is either given explicitly via the :resolution option or inferred from the input:

  • Date.t:day (Date has no time components).

  • Time.t, NaiveDateTime.t, DateTime.t:second, or :microsecond when the value carries a declared sub-second precision. These types are second-granular by construction, so the resolution follows the type's declared precision rather than the magnitude of the components — 09:00:00 is a fully specified second, not an under-specified hour. Pass an explicit :resolution to widen to a coarser span (e.g. :day for a midnight value you want to treat as a whole day).

When an explicit :resolution is given, the resulting Tempo is passed through at_resolution/2 to either truncate or pad to that resolution.

Arguments

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

iex> Tempo.from_elixir(~N[2022-06-15 10:30:00])
~o"2022Y6M15DT10H30M0S"

iex> Tempo.from_elixir(~N[2022-06-15 00:00:00])
~o"2022Y6M15DT0H0M0S"

iex> Tempo.from_elixir(~D[2022-06-15], resolution: :hour)
~o"2022Y6M15DT0H"

iex> Tempo.from_elixir(~N[2022-06-15 10:30:00], resolution: :day)
~o"2022Y6M15D"

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, options)

@spec from_iso8601(string :: String.t(), calendar :: Calendar.calendar()) ::
  {:ok,
   t()
   | Tempo.Interval.t()
   | Tempo.Duration.t()
   | Tempo.Set.t()
   | Tempo.Range.t()}
  | {:error, error_reason()}
@spec from_iso8601(string :: String.t(), options :: keyword()) ::
  {:ok,
   t()
   | Tempo.Interval.t()
   | Tempo.Duration.t()
   | Tempo.Set.t()
   | Tempo.Range.t()}
  | {:error, error_reason()}

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, options)

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

from_naive_date_time(naive)

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

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

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

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() | map()) :: {:ok, t()} | {:error, error_reason()}

Construct a Tempo.t/0 from a keyword list of time-scale components and options.

The companion to ~o sigils and Tempo.from_iso8601/1: where the sigil is ideal for literal values and from_iso8601/1 for already- formatted strings, new/1 is the right constructor when you have structured data at runtime — form inputs, database rows, API payloads, test fixtures.

Components can be passed in any order; new/1 reorders them coarse-to-fine (year → month → day → hour → minute → second) before building the struct.

Axis coherence is enforced: the Gregorian axis (:month, :day), the ISO-week axis (:week, :day_of_week), and the ordinal axis (:day_of_year) are mutually exclusive.

Arguments

  • components is a keyword list — or a map with the same atom keys — mixing time-scale components and options. At least one time-scale component must be present.

    Maps are convenient for interop with Elixir's standard date and time types: a Date.t/0, Time.t/0, NaiveDateTime.t/0, or a bare Calendrical.parse/2 :map result can be passed directly. Calendar.ISO (Elixir's default) is silently normalised to Calendrical.Gregorian so calendar-aware validation works.

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)

iex> {:ok, tempo} = Tempo.new(%{year: 2026, month: 6, day: 15})
iex> tempo.time
[year: 2026, month: 6, day: 15]

iex> {:ok, tempo} = Tempo.new(Map.from_struct(~D[2026-06-15]))
iex> tempo.time
[year: 2026, month: 6, day: 15]

new!(components)

@spec new!(keyword() | map()) :: 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]

next_working_day(tempo, territory \\ nil)

@spec next_working_day(t(), Tempo.Territory.input()) :: t()

The next working day strictly after tempo in the territory.

Equivalent to add_working_days(tempo, 1, territory).

Examples

iex> Tempo.next_working_day(~o"2026-06-12", :US)
~o"2026Y6M15D"

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"

overlap_certainty(a, b)

The three-valued certainty that a and b intersect, given their ± margins. See Tempo.Interval.overlap_certainty/2.

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

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

parse(input, options \\ [])

@spec parse(String.t(), Keyword.t()) ::
  {:ok, t() | Tempo.Interval.t()} | {:error, Exception.t()}

Parse a locale-formatted date, time, datetime, or interval string into a Tempo value.

Delegates to Calendrical.parse/2 with as: :map, then routes the resulting field map (or {from_map, to_map} interval pair) through Tempo.new/1. Useful when the input shape is not known up-front — a single text field that may carry any of "2026-05-16", "14:30", "May 16, 2026 2:30 PM", or "May 5 – May 10, 2026".

For ISO 8601 / IXDTF input prefer from_iso8601/1, which preserves EDTF qualification and IXDTF extended suffixes that the locale-style parser does not understand.

Arguments

Options

Notable keys forwarded to Calendrical.parse/2:

  • :locale is the locale used to interpret month names, AM/PM markers, and similar locale-dependent tokens. Defaults to Localize.get_locale/0.

  • :calendar is the CLDR calendar key or calendar module to parse against. Defaults to :gregorian.

  • :reference_date is the "today" anchor used for two-digit-year pivoting and partial-date inheritance.

The :as option is set to :map regardless of any caller-supplied value — Tempo always asks Calendrical for the field-map form so it can rebuild a Tempo value from the parsed fields.

Returns

  • {:ok, t()} for a single-value input.

  • {:ok, Tempo.Interval.t()} when the input names a range.

  • {:error, exception} when Calendrical cannot parse the string, or when Tempo.new/1 rejects the resulting field map.

Examples

iex> {:ok, tempo} = Tempo.parse("2026-05-16", locale: :en)
iex> tempo.time
[year: 2026, month: 5, day: 16]

iex> {:ok, tempo} = Tempo.parse("May 16, 2026", locale: :en)
iex> tempo.time
[year: 2026, month: 5, day: 16]

iex> {:ok, tempo} = Tempo.parse("14:30", locale: :en)
iex> tempo.time
[hour: 14, minute: 30]

iex> {:ok, %Tempo.Interval{}} = Tempo.parse("2026-05-05 – 2026-05-10", locale: :en)

parse!(input, options \\ [])

@spec parse!(String.t(), Keyword.t()) :: t() | Tempo.Interval.t()

Bang variant of parse/2. Raises on parse failure or invalid component combination.

Examples

iex> Tempo.parse!("2026-05-16", locale: :en).time
[year: 2026, month: 5, day: 16]

possibly_after?(a, b)

true when a could start after b ends for some placement of their ± margins. See Tempo.Interval.possibly_after?/2.

possibly_before?(a, b)

true when a could end before b starts for some placement of their ± margins. See Tempo.Interval.possibly_before?/2.

possibly_overlaps?(a, b)

true when a and b could intersect for some placement of their ± margins. See Tempo.Interval.possibly_overlaps?/2.

possibly_within?(a, b)

true when a could fall within b for some placement of their ± margins. See Tempo.Interval.possibly_within?/2.

previous_working_day(tempo, territory \\ nil)

@spec previous_working_day(t(), Tempo.Territory.input()) :: t()

The working day immediately before tempo in the territory.

Equivalent to add_working_days(tempo, -1, territory).

Examples

iex> Tempo.previous_working_day(~o"2026-06-15", :US)
~o"2026Y6M12D"

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

relation_certainty(a, b, target)

The three-valued certainty that relation(a, b) is (one of) target. See Tempo.Interval.relation_certainty/3.

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, duration)

@spec shift(t(), Tempo.Duration.t() | keyword()) ::
  t() | Tempo.Set.t() | Tempo.IntervalSet.t() | {:error, error_reason()}

Shift a t/0 by a duration, returning a new t/0.

The shift amount may be given either as a Tempo.Duration.t/0 (when you already have one, e.g. ~o"P1M") or as a keyword list of signed unit amounts (the ergonomic ad-hoc form). Both delegate to Tempo.Math.add/2, which is the principled path when composing durations directly.

Units are applied largest-to-smallest with the standard month-end clamping rule (e.g. ~o"2024-01-31" + 1 month is 2024-02-29, not 2024-03-02).

Arguments

  • tempo is any t/0.

  • shift is either a Tempo.Duration.t/0, or a keyword list of {unit, amount} pairs such as [month: 1, day: -5] or [year: 2]. Valid units: :year, :month, :week, :day, :hour, :minute, :second. Keyword amounts may be negative.

Returns

  • The shifted t/0.

  • {:error, %Tempo.RequiresAnchorError{}} when the value has no :year (an un-anchored month/day, bare-day, or time-of-day value) and the shift's result would depend on the missing year. The rule: an un-anchored shift is computed when its result is invariant to the year, and errors when it isn't (it never raises). So ~o"1M31D" plus one day is ~o"2M1D" (January always has 31 days) and a whole-year step is a no-op (~o"1M31D" plus P1Y is ~o"1M31D"), but ~o"1M31D" plus one month lands on an unresolvable "Feb 31" and ~o"2M28D" plus one day (Feb 29 or Mar 1?) both error.

Examples

iex> Tempo.shift(~o"2026-06-15", month: 1, day: -5)
~o"2026Y7M10D"

iex> Tempo.shift(~o"2026-01-31", month: 1)
~o"2026Y2M28D"

iex> Tempo.shift(~o"2026-06-15T10:00:00", hour: -3)
~o"2026Y6M15DT7H0M0S"

iex> Tempo.shift(~o"2026", ~o"P2Y")
~o"2028Y"

iex> Tempo.shift(~o"1M31D", ~o"P1D")
~o"2M1D"

iex> Tempo.shift(~o"1M31D", ~o"P1Y")
~o"1M31D"

iex> match?({:error, %Tempo.RequiresAnchorError{}}, Tempo.shift(~o"1M31D", ~o"P1M"))
true

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)

@spec split(t()) :: {t() | nil, t() | nil}

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)

@spec to_calendar(t()) ::
  {:ok, Date.t() | Time.t() | NaiveDateTime.t()}
  | {:error, Tempo.ConversionError.t()}

Convert a t/0 to its best-fit native Elixir calendar type.

Dispatches by resolution: a full date becomes a Date, a time-of-day becomes a Time, and a full date-and-time becomes a NaiveDateTime. A value too coarse to pin an instant (a bare year or month) or one carrying a UTC offset cannot be represented by a single native type and returns an error. For a specific target type use to_date/1, to_time/1, or to_naive_date_time/1.

Arguments

  • tempo is a t/0.

Returns

  • {:ok, native} where native is a Date, Time, or NaiveDateTime; or

  • {:error, t:Tempo.ConversionError.t/0} when the value is too coarse to convert, or carries a zone offset.

Examples

iex> Tempo.to_calendar(~o"2026-06-15")
{:ok, ~D[2026-06-15]}

iex> Tempo.to_calendar(~o"2026-06-15T10:30:00")
{:ok, ~N[2026-06-15 10:30:00.000000]}

iex> match?({:error, %Tempo.ConversionError{}}, Tempo.to_calendar(~o"2026"))
true

to_date(value)

@spec to_date(t()) :: {:ok, Date.t()} | {:error, error_reason()}

Convert a Tempo struct into a Date.

Accepts the three single-day Tempo shapes:

  • Calendar date — [year: Y, month: M, day: D].

  • Ordinal date — [year: Y, day: DDD]. When the Tempo carries only year and day (no month), the day is interpreted as day-of-year per ISO 8601-2 §4.3.4. 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_date_time(tempo)

@spec to_date_time(t()) :: {:ok, DateTime.t()} | {:error, error_reason()}

Convert a zoned Tempo into a DateTime.

The lossless inverse of from_elixir/2 on a DateTime.t/0: it preserves the named time zone, re-deriving the UTC offset from the time-zone database so the result is DST-correct. This is the conversion to reach for when the zone matters — unlike to_naive_date_time/1, which deliberately drops it.

Arguments

  • tempo is a t/0 resolved to at least second resolution and carrying a named IANA zone on extended.zone_id (every value built from a DateTime via from_elixir/2 does). Offset-only values (a numeric shift with no named zone) and floating values cannot name a DateTime zone and return an error — attach a zone with shift_zone/2 first, or use to_naive_date_time/1.

Returns

  • {:ok, date_time}, or

  • {:error, reason} when the value lacks a full datetime or a named zone, or when the wall-clock time falls in a spring-forward gap that does not exist in the zone.

Examples

iex> Tempo.to_date_time(~o"2022-11-19T01:02:03Z[Etc/UTC]")
{:ok, ~U[2022-11-19 01:02:03.000000Z]}

iex> {:error, _} = Tempo.to_date_time(~o"2022-11-19T01:02:03")

to_elixir(duration)

@spec to_elixir(t() | Tempo.Duration.t()) ::
  {:ok, Duration.t() | Date.t() | Time.t() | NaiveDateTime.t()}
  | {:error, Tempo.ConversionError.t()}

Convert a Tempo value to its native Elixir equivalent — the outbound mirror of from_elixir/2.

Arguments

Returns

  • {:ok, native} where native is an Elixir Duration, Date, Time, or NaiveDateTime.

  • {:error, t:Tempo.ConversionError.t/0} when the value cannot be represented by a single native type.

Examples

iex> Tempo.to_elixir(~o"PT8H")
{:ok, Duration.new!(hour: 8)}

iex> Tempo.to_elixir(~o"2026-06-15")
{:ok, ~D[2026-06-15]}

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). The span is one unit at the value's resolution: a day value becomes a one-day interval, a second value a one-second interval. This is the canonical representation used by the set-operations API (union/2, intersection/2, coalesce/1). See the interop guide for how converted Elixir date/time values materialise.

When the input expands to multiple disjoint spans — a set of explicit values, a range over a time unit, a stepped range — the result is a %Tempo.IntervalSet{} with intervals sorted and coalesced. The conversion is idempotent on values that are already explicit.

Arguments

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 at a resolution with no finer unit available to bound the span (microsecond materialises to a one-microsecond span; only exotic selector resolutions have no span), a one-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)

@spec to_naive_date_time(t()) :: {:ok, NaiveDateTime.t()} | {:error, error_reason()}

Convert a Tempo struct into a NaiveDateTime.

A NaiveDateTime is a zoneless wall-clock reading, so a zoned Tempo projects to its wall-clock components with the offset dropped — the same lossy projection DateTime.to_naive/1 performs in the stdlib, and consistent with to_date/1 / to_time/1. No time-zone math is involved: the wall-clock fields are read verbatim. To keep the zone, use to_date_time/1; to normalise to UTC wall time first, shift_zone(tempo, "Etc/UTC").

Arguments

  • tempo is a t/0 resolved to at least second resolution (year through second, optionally microsecond). Coarser values cannot fill a NaiveDateTime and return an error.

Returns

  • {:ok, naive_date_time}, or

  • {:error, reason} when the value is not resolved to a full datetime.

Examples

iex> Tempo.to_naive_date_time(~o"2022-11-19T01:02:03")
{:ok, ~N[2022-11-19 01:02:03.000000]}

iex> {:error, _} = Tempo.to_naive_date_time(~o"2022-11")

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)

@spec to_time(t()) :: {:ok, Time.t()} | {:error, error_reason()}

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}

validate_zone_offset(tempo)

@spec validate_zone_offset(t()) :: :ok | {:error, Tempo.ZoneOffsetMismatchError.t()}

Check that an IXDTF value's explicit numeric offset agrees with its IANA time zone at the value's wall instant.

A value such as 2022-11-20T10:37:00+05:00[Europe/Paris] carries both a numeric offset and a zone; Paris is +01:00 in November, so the stated +05:00 is inconsistent. By default the zone wins and the offset is consulted only for DST disambiguation; this surfaces the disagreement instead (RFC 9557 §4.2). The same check backs the strict: true option of from_iso8601/2.

Arguments

  • tempo is a t/0.

Returns

  • :ok when the offset agrees, or when there is nothing to check (no zone, no explicit offset, or a non-anchored value).

  • {:error, t:Tempo.ZoneOffsetMismatchError.t/0} on disagreement.

Examples

iex> {:ok, t} = Tempo.from_iso8601("2022-11-20T10:37:00+01:00[Europe/Paris]")
iex> Tempo.validate_zone_offset(t)
:ok

iex> {:error, %Tempo.ZoneOffsetMismatchError{}} =
...>   Tempo.from_iso8601("2022-11-20T10:37:00+05:00[Europe/Paris]", strict: true)

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]

weekend?(tempo, territory \\ nil)

@spec weekend?(t(), Tempo.Territory.input()) :: boolean()

Return true when tempo falls on a weekend day in the given territory.

Different territories weekend on different days — the United States on [Saturday, Sunday], Saudi Arabia on [Friday, Saturday], India on [Sunday] — read from CLDR via Localize. The day of week is taken from Date.day_of_week/1 (ISO 1 = Monday … 7 = Sunday), computed in the value's own calendar so a non-Gregorian value (Japanese, Islamic, Indian, …) is classified by the correct weekday.

Arguments

  • tempo is a t/0 that denotes a single day — a date or datetime, including the day-of-year and ISO week-date forms.

  • 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

  • true when the value's day of week is one of the territory's weekend days, false otherwise.

  • Raises ArgumentError when tempo does not denote a day (for example a year- or month-resolution value).

Examples

iex> Tempo.weekend?(~o"2026-06-13", :US)
true

iex> Tempo.weekend?(~o"2026-06-15", :US)
false

iex> # The same Friday: a weekend in Saudi Arabia, a workday in the US.
iex> Tempo.weekend?(~o"2026-06-12", :SA)
true
iex> Tempo.weekend?(~o"2026-06-12", :US)
false

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.

within_certainty(a, b)

The three-valued certainty that a falls within b, given their ± margins. See Tempo.Interval.within_certainty/2.

workday?(tempo, territory \\ nil)

@spec workday?(t(), Tempo.Territory.input()) :: boolean()

Return true when tempo falls on a workday — a day that is not in the territory's weekend.

The complement of weekend?/2; together they partition the week. This is the weekend/weekday distinction only — it does not consult public-holiday calendars.

Arguments

Returns

  • true when the value's day of week is not a weekend day in the territory, false otherwise.

Examples

iex> Tempo.workday?(~o"2026-06-15", :US)
true

iex> Tempo.workday?(~o"2026-06-13", :US)
false

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

working_days_in(interval, territory \\ nil)

@spec working_days_in(Tempo.Interval.t(), Tempo.Territory.input()) ::
  non_neg_integer()

Count the working days within an interval, excluding the territory's weekend.

The interval is half-open [from, to), so the result is the number of working days from its start up to but not including its end. Any day-yielding value works the same way through EnumEnum.count(value, &Tempo.workday?(&1, territory)).

Arguments

Returns

  • the count of working days in the interval.

Examples

iex> {:ok, june} = Tempo.Interval.new(from: ~o"2026-06-01", to: ~o"2026-07-01")
iex> Tempo.working_days_in(june, :US)
22

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