Tempo.Duration (Tempo v2.1.1)

Copy Markdown View Source

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

t()

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

t()

@type t() :: %Tempo.Duration{units: 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

add(a, b)

@spec add(t(), t()) :: t()

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

  • duration1 and duration2 are each a t/0.

Returns

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"

canonical(duration, options \\ [])

@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

  • duration is a t/0.

  • options is a keyword list of options.

Options

  • :calendar is the calendar whose years and weeks count the months and days. The default is Calendar.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!(duration, options \\ [])

@spec canonical!(t(), keyword()) :: t()

canonical/2, returning its answer or raising the error.

Arguments

Returns

  • What canonical/2 returns as {:ok, answer}, the answer alone. The error it returns as {:error, exception} is raised.

Examples

iex> Tempo.Duration.canonical!(~o"PT1H90M")
~o"PT2H30M"

compare(a, b, options \\ [])

@spec compare(t(), t(), keyword()) :: :lt | :eq | :gt

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

  • a and b are each a t/0.

  • options is a keyword list of options.

Options

  • :relative_to is a Tempo.t/0 the two durations are counted from: each is laid from the value's start, and the two ends compared.

  • :calendar is the calendar whose year counts the months of a year, without :relative_to. The default is Calendar.ISO.

Returns

  • :lt, :eq or :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

exact?(other)

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

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

  • duration is a t/0.

Returns

  • true for a duration of weeks, days and units of the clock alone, and false for one with a month or a year. A value that is no duration raises a Tempo.ConversionError.

Examples

iex> Tempo.Duration.exact?(~o"P1W2DT3H")
true

iex> Tempo.Duration.exact?(~o"P1M")
false

multiply(other, n)

@spec multiply(t(), integer()) :: t()

A duration taken a whole number of times: each unit multiplied.

Arguments

  • duration is a t/0.

  • n is a whole number.

Returns

Examples

iex> Tempo.Duration.multiply(~o"P1M15D", 2)
~o"P2M30D"

negate(duration)

@spec negate(t()) :: t()

A duration in the reverse direction: each unit negated.

Arguments

  • duration is a t/0.

Returns

Examples

iex> Tempo.Duration.negate(~o"P1DT2H")
~o"-P1DT2H"

new(components)

@spec new(keyword()) :: {:ok, t()} | {:error, Exception.t()}

Builds a duration from its units.

Arguments

  • components is 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 :microsecond is {microseconds, precision}.

Returns

  • {:ok, duration}, a t/0 with 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)

new!(components)

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

Builds a duration as new/1 does, returning it or raising.

Arguments

  • components is a keyword list of units, as for new/1.

Returns

  • The t/0, or raises the error new/1 returns.

Examples

iex> Tempo.Duration.new!(day: 1)
~o"P1D"

subtract(a, b)

@spec subtract(t(), t()) :: t()

The first duration less the second: add/2 of the second negated.

Arguments

  • a and b are each a t/0.

Returns

Examples

iex> Tempo.Duration.subtract(~o"PT2H", ~o"PT30M")
~o"PT2H-30M"

sum(durations)

@spec sum([t()]) :: t()

The sum of a list of durations, as add/2 sums two.

Arguments

  • durations is a list of t/0.

Returns

  • The t/0 of their units added, P with no units for an empty list. A member that is no duration raises a Tempo.ConversionError.

Examples

iex> Tempo.Duration.sum([~o"PT1H", ~o"PT2H", ~o"P1D"])
~o"P1DT3H"

to_microseconds(duration)

@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

  • duration is a t/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!(duration)

@spec to_microseconds!(t()) :: integer()

to_microseconds/1, returning its answer or raising the error.

Arguments

Returns

  • What to_microseconds/1 returns 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

to_unit(duration, unit, options \\ [])

@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

  • duration is a t/0.

  • unit is :week, :day, :hour, :minute, :second or :microsecond.

  • options is a keyword list of options.

Options

  • :relative_to is a Tempo.t/0 the 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}

to_unit!(duration, unit, options \\ [])

@spec to_unit!(
  t(),
  :week | :day | :hour | :minute | :second | :microsecond,
  keyword()
) :: float()

to_unit/3, returning its answer or raising the error.

Arguments

Options

Returns

  • What to_unit/3 returns 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

units()

@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 :year to :microsecond.

Examples

iex> Tempo.Duration.units()
[:year, :month, :week, :day, :hour, :minute, :second, :microsecond]