A length of time with no place on the time line: P1Y2M3DT4H5M6S.
A duration is the one companion of Tempo: it is not an expression and
denotes no span, and it is what an interval is formed from once it is
given a start (2026-06-01/P1M) and what a value is shifted by
(Tempo.shift).
new/1 builds one from its units and Tempo.duration/2 measures the
length of an expression. add/2, subtract/2, sum/1, multiply/2 and
negate/1 are its arithmetic; compare/3 orders two, and Tempo's
at_least?/2, at_most?/2 and exactly?/2 say how they compare.
to_unit/3 and to_microseconds/1 count one in a unit, and exact?/1
says whether it has a length without a place to begin, which a duration
of years or months does not.
Summary
Types
The units of a duration, coarsest first, each a whole number, negative
for a length in the reverse direction; a part of a second is
{microseconds, precision} as Elixir's types hold one.
Functions
The sum of two durations, unit by unit: an hour and a half plus forty-five minutes is an hour and seventy-five minutes, no unit carried into another, because an hour is sixty minutes but a month is not a fixed number of days. A fraction of a second is the one exception, being part of its second.
A duration in canonical form, as ISO 8601-2's Amendment 1 (2025) defines it (§14.7 and D.5): no part past the unit above it that it converts into, and a negative part only where no coarser part written can take it in.
canonical/2, returning its answer or raising the error.
Orders two durations by length, so that Tempo.Duration is a sorter:
Enum.sort(durations, Tempo.Duration).
Whether a duration has a fixed length: one of weeks, days and units of the clock alone. A month or a year is as long as the one it is counted from, so a duration with either is not exact.
A duration taken a whole number of times: each unit multiplied.
A duration in the reverse direction: each unit negated.
Builds a duration from its units.
Builds a duration as new/1 does, returning it or raising.
The first duration less the second: add/2 of the second negated.
The sum of a list of durations, as add/2 sums two.
The length of an exact duration in microseconds, with a week seven days and a day twenty-four hours.
to_microseconds/1, returning its answer or raising the error.
The length of a duration in one unit of fixed length, as a float.
to_unit/3, returning its answer or raising the error.
The units a duration is written in, coarsest first.
Types
@type t() :: %Tempo.Duration{units: units()}
@type units() :: [ {:year | :month | :week | :day | :hour | :minute | :second, integer()} | {:microsecond, {integer(), 0..6}} ]
The units of a duration, coarsest first, each a whole number, negative
for a length in the reverse direction; a part of a second is
{microseconds, precision} as Elixir's types hold one.
Functions
The sum of two durations, unit by unit: an hour and a half plus forty-five minutes is an hour and seventy-five minutes, no unit carried into another, because an hour is sixty minutes but a month is not a fixed number of days. A fraction of a second is the one exception, being part of its second.
Arguments
duration1andduration2are each at/0.
Returns
- The
t/0of their units added. A value that is no duration raises aTempo.ConversionError.
Examples
iex> Tempo.Duration.add(~o"PT1H30M", ~o"PT45M")
~o"PT1H75M"
iex> Tempo.Duration.add(~o"P1Y", ~o"P2M")
~o"P1Y2M"
iex> Tempo.Duration.add(~o"PT0.7S", ~o"PT0.6S")
~o"PT1.3S"
@spec canonical(t(), keyword()) :: {:ok, t()} | {:error, Exception.t()}
A duration in canonical form, as ISO 8601-2's Amendment 1 (2025) defines it (§14.7 and D.5): no part past the unit above it that it converts into, and a negative part only where no coarser part written can take it in.
Parts convert along two chains. Years and months convert by the months in
the calendar's year, where every year has as many. Weeks, days, hours,
minutes, seconds and parts of a second convert by the days in the
calendar's week, 24 hours, 60 minutes and 60 seconds. A minute is 60
seconds, as Tempo's clock has it, which is the nominal duration rule the
amendment allows (§14.6). Months and days do not convert, and nor do years
and weeks: the month and the days of P1M30D stay apart.
A chain of positive length is written from the coarsest unit it reaches:
PT26H is P1DT2H, and P10D is P1W3D. A chain of negative length
carries its sign on the coarsest unit written in it, the rest positive:
P-1Y-15M is P-3Y9M.
The canonical form is for comparing and for writing. A shift by it can land elsewhere in a zone across a change of the clock, where a day is a day of the calendar and 24 hours are elapsed time.
Arguments
durationis at/0.optionsis a keyword list of options.
Options
:calendaris the calendar whose years and weeks count the months and days. The default isCalendar.ISO.
Returns
{:ok, duration}in canonical form, or{:error, exception}for a value that is no duration, an option the function does not take, or a calendar Tempo does not know.
Examples
iex> Tempo.Duration.canonical(~o"P7Y24M")
{:ok, ~o"P9Y"}
iex> Tempo.Duration.canonical(~o"PT1H-10M")
{:ok, ~o"PT50M"}
iex> Tempo.Duration.canonical(~o"P3DT-10M")
{:ok, ~o"P2DT23H50M"}
canonical/2, returning its answer or raising the error.
Arguments
- The arguments are those of
canonical/2.
Returns
- What
canonical/2returns as{:ok, answer}, the answer alone. The error it returns as{:error, exception}is raised.
Examples
iex> Tempo.Duration.canonical!(~o"PT1H90M")
~o"PT2H30M"
Orders two durations by length, so that Tempo.Duration is a sorter:
Enum.sort(durations, Tempo.Duration).
Two exact durations are compared as lengths, however they are written:
PT90M and PT1H30M are equal. A duration with a month or a year has no
length until it is counted from a date, given as :relative_to. Without
one, years and months are counted in months by the calendar's year, as
ISO 8601-2's Amendment 1 (2025) converts them, and the rest as a length:
P7Y24M and P9Y are equal, and P1Y1D is longer than P12M. Where
the months differ one way and the rest the other (P1M against P30D),
or the calendar's years differ in months, the comparison raises, having
only three answers to give.
Arguments
aandbare each at/0.optionsis a keyword list of options.
Options
:relative_tois aTempo.t/0the two durations are counted from: each is laid from the value's start, and the two ends compared.:calendaris the calendar whose year counts the months of a year, without:relative_to. The default isCalendar.ISO.
Returns
:lt,:eqor:gt.
Examples
iex> Tempo.Duration.compare(~o"PT90M", ~o"PT1H30M")
:eq
iex> Tempo.Duration.compare(~o"P7Y24M", ~o"P9Y")
:eq
iex> Enum.sort([~o"PT2H", ~o"PT30M", ~o"PT1H"], Tempo.Duration)
[~o"PT30M", ~o"PT1H", ~o"PT2H"]
iex> Tempo.Duration.compare(~o"P1M", ~o"P30D", relative_to: ~o"2026-02-01")
:lt
Whether a duration has a fixed length: one of weeks, days and units of the clock alone. A month or a year is as long as the one it is counted from, so a duration with either is not exact.
Arguments
durationis at/0.
Returns
truefor a duration of weeks, days and units of the clock alone, andfalsefor one with a month or a year. A value that is no duration raises aTempo.ConversionError.
Examples
iex> Tempo.Duration.exact?(~o"P1W2DT3H")
true
iex> Tempo.Duration.exact?(~o"P1M")
false
A duration taken a whole number of times: each unit multiplied.
Arguments
durationis at/0.nis a whole number.
Returns
- The
t/0with each unit multiplied byn. A value that is no duration raises aTempo.ConversionError, and annthat is no whole number anArgumentError.
Examples
iex> Tempo.Duration.multiply(~o"P1M15D", 2)
~o"P2M30D"
A duration in the reverse direction: each unit negated.
Arguments
durationis at/0.
Returns
- The
t/0with each unit negated. A value that is no duration raises aTempo.ConversionError.
Examples
iex> Tempo.Duration.negate(~o"P1DT2H")
~o"-P1DT2H"
@spec new(keyword()) :: {:ok, t()} | {:error, Exception.t()}
Builds a duration from its units.
Arguments
componentsis a keyword list of units (:year,:month,:week,:day,:hour,:minute,:second,:microsecond), in any order, each a whole number, negative for a length in the reverse direction; a:microsecondis{microseconds, precision}.
Returns
{:ok, duration}, at/0with its units ordered coarsest first.{:error, %ArgumentError{}}when a key is not a unit of a duration, is given twice, or holds what is not a whole number.
Examples
iex> Tempo.Duration.new(month: 6, year: 1)
{:ok, ~o"P1Y6M"}
iex> Tempo.Duration.new(hour: 1, minute: 30)
{:ok, ~o"PT1H30M"}
iex> {:error, %ArgumentError{}} = Tempo.Duration.new(fortnight: 1)
Builds a duration as new/1 does, returning it or raising.
Arguments
componentsis a keyword list of units, as fornew/1.
Returns
Examples
iex> Tempo.Duration.new!(day: 1)
~o"P1D"
The first duration less the second: add/2 of the second negated.
Arguments
aandbare each at/0.
Returns
- The
t/0ofalessb, unit by unit. A value that is no duration raises aTempo.ConversionError.
Examples
iex> Tempo.Duration.subtract(~o"PT2H", ~o"PT30M")
~o"PT2H-30M"
The sum of a list of durations, as add/2 sums two.
Arguments
durationsis a list oft/0.
Returns
- The
t/0of their units added,Pwith no units for an empty list. A member that is no duration raises aTempo.ConversionError.
Examples
iex> Tempo.Duration.sum([~o"PT1H", ~o"PT2H", ~o"P1D"])
~o"P1DT3H"
@spec to_microseconds(t()) :: {:ok, integer()} | {:error, Exception.t()}
The length of an exact duration in microseconds, with a week seven days and a day twenty-four hours.
Returns
{:ok, microseconds}.{:error, %ArgumentError{}}for a duration with a month or a year, and{:error, %Tempo.ConversionError{}}for a value that is no duration.
Arguments
durationis at/0.
Examples
iex> Tempo.Duration.to_microseconds(~o"PT1H30M")
{:ok, 5_400_000_000}
iex> {:error, %ArgumentError{}} = Tempo.Duration.to_microseconds(~o"P1M")
to_microseconds/1, returning its answer or raising the error.
Arguments
- The arguments are those of
to_microseconds/1.
Returns
- What
to_microseconds/1returns as{:ok, answer}, the answer alone. The error it returns as{:error, exception}is raised.
Examples
iex> Tempo.Duration.to_microseconds!(~o"PT1H30M")
5_400_000_000
@spec to_unit(t(), :week | :day | :hour | :minute | :second | :microsecond, keyword()) :: {:ok, float()} | {:error, Exception.t()}
The length of a duration in one unit of fixed length, as a float.
Arguments
durationis at/0.unitis:week,:day,:hour,:minute,:secondor:microsecond.optionsis a keyword list of options.
Options
:relative_tois aTempo.t/0the duration is counted from, which a duration with a month or a year needs.
Returns
{:ok, length}, a float.{:error, %ArgumentError{}}when the unit is a month or a year, or the duration has one and no:relative_to.
Examples
iex> Tempo.Duration.to_unit(~o"PT90M", :hour)
{:ok, 1.5}
iex> Tempo.Duration.to_unit(~o"P1M", :day, relative_to: ~o"2026-02-01")
{:ok, 28.0}
@spec to_unit!( t(), :week | :day | :hour | :minute | :second | :microsecond, keyword() ) :: float()
to_unit/3, returning its answer or raising the error.
Arguments
- The arguments are those of
to_unit/3.
Options
- The options of
to_unit/3.
Returns
- What
to_unit/3returns as{:ok, answer}, the answer alone. The error it returns as{:error, exception}is raised.
Examples
iex> Tempo.Duration.to_unit!(~o"PT90M", :hour)
1.5
@spec units() :: [
:year | :month | :week | :day | :hour | :minute | :second | :microsecond,
...
]
The units a duration is written in, coarsest first.
Returns
- The list of unit atoms, from
:yearto:microsecond.
Examples
iex> Tempo.Duration.units()
[:year, :month, :week, :day, :hour, :minute, :second, :microsecond]