A calendar-relative duration — a list of {unit, amount}
pairs such as [year: 1, month: 6]. Produced by the ISO 8601
parser (P1Y6M), the RRULE encoder (as the FREQ + INTERVAL
cadence), and arithmetic helpers in Tempo.Math.
Summary
Functions
Add two durations component-wise, returning a new duration.
Compare two durations by length.
The same length in the opposite direction.
Construct a Tempo.Duration.t/0 from a keyword list of
{unit, amount} pairs.
Bang variant of new/1.
Subtract one duration from another component-wise, returning a new duration.
Sum a list of durations, returning a new duration.
Express a duration as a single magnitude in unit, as a float.
Bang variant of to_unit/3 — returns the float or raises.
Types
@type t() :: %Tempo.Duration{time: [{unit(), integer() | Tempo.Microsecond.t()}]}
@type unit() ::
:year
| :month
| :week
| :day
| :hour
| :minute
| :second
| :microsecond
| :day_of_year
| :day_of_week
Functions
Add two durations component-wise, returning a new duration.
Corresponding components sum — hours with hours, months with months —
and no unit is converted into another, so calendar-length units
(:year, :month) add exactly without needing a reference date.
Fractional seconds carry into whole seconds when they overflow, and
when the time-of-day components would otherwise disagree in sign
(40 h − 21 h 30 m) they borrow into canonical components sharing
one sign (18 h 30 m). Components that sum to zero are dropped, so
a duration and its negate/1 add to the zero duration.
Arguments
Returns
- A
t/0with each unit's components summed.
Examples
iex> Tempo.Duration.add(~o"PT7H30M", ~o"PT6H")
~o"PT13H30M"
iex> Tempo.Duration.add(~o"P1Y", ~o"P2M3D")
~o"P1Y2M3D"
iex> Tempo.Duration.add(~o"PT1.5S", ~o"PT0.7S")
~o"PT2.2S"
iex> Tempo.Duration.add(~o"PT1H", Tempo.Duration.negate(~o"PT1H")).time
[]
Compare two durations by length.
Present so that Enum.sort/2, Enum.max/2 and Enum.min/2 accept
this module the way they accept Date, Time and Tempo:
Enum.max([~o"PT30M", ~o"PT2H"], Tempo.Duration)Without it, "which of these is longer" is answered by converting both to seconds by hand at the call site, or worse by shifting a fabricated epoch and diffing — arithmetic that has nothing to do with the question being asked.
Comparison is by length, not by shape: PT90M and PT1H30M are the
same duration written two ways and compare :eq.
Arguments
duration_aandduration_bare each at/0.
Options
:relative_tois aTempo.t/0to resolve calendar-variable components against. A duration containing:monthor:yearhas no fixed length — February and August are not the same size — so comparing one without an anchor raises.
Returns
:lt,:eqor:gt; or raisesArgumentErrorwhen either duration has no fixed length and no:relative_towas given.
Examples
iex> Tempo.Duration.compare(~o"PT30M", ~o"PT2H")
:lt
iex> Tempo.Duration.compare(~o"PT90M", ~o"PT1H30M")
:eq
iex> Enum.sort([~o"PT2H", ~o"PT30M", ~o"PT1H"], Tempo.Duration)
[~o"PT30M", ~o"PT1H", ~o"PT2H"]A month is only comparable against a date that says which month:
iex> Tempo.Duration.compare(~o"P1M", ~o"P30D", relative_to: ~o"2026-02-01")
:lt
The same length in the opposite direction.
Every unit is negated, including the fractional part, so a negated duration shifts backwards by exactly what the original shifted forwards. Negating twice returns the original.
This is what a caller needs whenever the direction of a shift is decided at runtime rather than written literally — stepping back by a configured lead time, widening an interval at both ends, walking a recurrence in reverse.
Arguments
durationis at/0.
Returns
- a
t/0of the same magnitude and the opposite sign.
Examples
iex> Tempo.Duration.negate(~o"PT15M")
~o"PT-15M"
iex> Tempo.Duration.negate(~o"PT-15M")
~o"PT15M"Shifting by a duration and then by its negation returns where you started:
iex> lead = ~o"PT15M"
iex> ~o"2027-03-02T10:00:00"
...> |> Tempo.shift(Tempo.Duration.negate(lead))
...> |> Tempo.shift(lead)
~o"2027Y3M2DT10H0M0S"Every component flips, fractional seconds included:
iex> Tempo.Duration.negate(~o"P1Y2MT1.5S").time
[year: -1, month: -2, second: -1, microsecond: {-500000, 1}]
@spec new(keyword()) :: {:ok, t()} | {:error, Exception.t()}
Construct a Tempo.Duration.t/0 from a keyword list of
{unit, amount} pairs.
Components can be passed in any order; new/1 reorders them
coarse-to-fine before building the struct.
Arguments
componentsis a keyword list of duration units.
Options
Every value must be an integer. Negative values are permitted (reverse-direction duration).
:yearis the year count.:monthis the month count.:weekis the week count.:dayis the day count.:day_of_yearis a day-of-year offset (used by RRULE expansion).:day_of_weekis a day-of-week offset (used by RRULE expansion).:houris the hour count.:minuteis the minute count.:secondis the second count.
Returns
{:ok, t()}on success.{:error, reason}when a key is unknown or a value is not an integer.
Examples
iex> {:ok, d} = Tempo.Duration.new(year: 1, month: 6)
iex> d.time
[year: 1, month: 6]
iex> {:ok, d} = Tempo.Duration.new(month: 6, year: 1)
iex> d.time
[year: 1, month: 6]
Bang variant of new/1.
Subtract one duration from another component-wise, returning a new duration.
Equivalent to add(duration_a, negate(duration_b)): like units
subtract, no unit converts into another, fractional seconds borrow,
and components that cancel are dropped.
Arguments
Returns
- A
t/0withduration_b's components subtracted.
Examples
iex> Tempo.Duration.subtract(~o"PT21H30M", ~o"PT8H")
~o"PT13H30M"
iex> Tempo.Duration.subtract(~o"P1Y6M", ~o"P6M")
~o"P1Y"
iex> Tempo.Duration.subtract(~o"PT1H", ~o"PT1H").time
[]
Sum a list of durations, returning a new duration.
Reduces the list with add/2, so the same component-wise rules
apply: like units sum, nothing converts, fractional seconds carry.
An empty list sums to the zero duration.
Arguments
durationsis a list oft/0values.
Returns
- A
t/0with each unit's components summed across the list.
Raises
FunctionClauseErrorwhen a list member is not aTempo.Duration.t/0.
Examples
iex> Tempo.Duration.sum([~o"PT7H30M", ~o"PT6H", ~o"PT8H"])
~o"PT21H30M"
iex> Tempo.Duration.sum([]).time
[]
@spec to_unit(t(), unit(), keyword()) :: {:ok, float()} | {:error, Exception.t()}
Express a duration as a single magnitude in unit, as a float.
For a duration built only from fixed-length units (microsecond
through week, with day = 24 h and week = 7 d), the conversion
is exact and needs no context. A duration carrying :month or
:year has no fixed length, so it converts only against a
reference date supplied as :relative_to — the duration is applied
to that date and the elapsed time measured on the UTC time line
(DST-exact when the reference is zoned). Tempo never assumes a
nominal month or year; it returns an error instead.
Arguments
durationis at/0.unitis the target unit — one of:microsecond,:second,:minute,:hour,:day,:week. (:month/:yearare not fixed magnitudes and cannot be a target.)
Options
:relative_tois aTempo.t/0reference date. Required to convert a duration containing:monthor:year; optional otherwise, where a zoned reference makes:day/:weekDST-exact.
Returns
{:ok, magnitude}wheremagnitudeis afloat().{:error, reason}when the duration needs a:relative_toit was not given, the target unit is not fixed-length, or the reference is invalid.
Examples
iex> Tempo.Duration.to_unit(~o"PT90M", :hour)
{:ok, 1.5}
iex> Tempo.Duration.to_unit(~o"P2D", :hour)
{:ok, 48.0}
iex> Tempo.Duration.to_unit(~o"P1M", :day, relative_to: ~o"2026-02-01")
{:ok, 28.0}
Bang variant of to_unit/3 — returns the float or raises.
Examples
iex> Tempo.Duration.to_unit!(~o"PT8H", :hour)
8.0