Duration — Elixir v1.18.3 (original) (raw)

Struct and functions for handling durations.

A Duration struct represents a collection of time scale units, allowing for manipulation and calculation of durations.

Date and time scale units are represented as integers, allowing for both positive and negative values.

Microseconds are represented using a tuple {microsecond, precision}. This ensures compatibility with other calendar types implementing time, such as Time, DateTime, and NaiveDateTime.

Shifting

The most common use of durations in Elixir's standard library is to "shift" the calendar types.

iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]

In the example above, Date.shift/2 automatically converts the units into a Duration struct, although one can also be given directly:

iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]

It is important to note that shifting is not an arithmetic operation. For example, adding date + 1 month + 1 month does not yield the same result as date + 2 months. Let's see an example:

iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]

iex> ~D[2016-01-31] |> Date.shift(month: 2)
~D[2016-03-31]

As you can see above, the results differ, which explains why operations with durations are called "shift" rather than "add". This happens because, once we add one month to 2016-01-31, we get 2016-02-29. Then adding one extra month gives us 2016-03-29 instead of 2016-03-31.

In particular, when applying durations to Calendar.ISO types:

As the shift/2 functions are calendar aware, they are guaranteed to return valid date/times, considering leap years as well as DST in applicable time zones.

Intervals

Durations in Elixir can be combined with stream operations to build intervals. For example, to retrieve the next three Wednesdays starting from 17th April, 2024:

iex> ~D[2024-04-17] |> Stream.iterate(&Date.shift(&1, week: 1)) |> Enum.take(3)
[~D[2024-04-17], ~D[2024-04-24], ~D[2024-05-01]]

However, once again, it is important to remember that shifting a duration is not arithmetic, so you may want to use the functions in this module depending on what you to achieve. Compare the results of both examples below:

# Adding one month after the other
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.iterate(date, fn prev_date -> Date.shift(prev_date, duration) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-29]]

# Multiplying durations by an index
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.from_index(fn i -> Date.shift(date, Duration.multiply(duration, i)) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-31]]

The second example consistently points to the last day of the month, as it performs operations on the duration, rather than shifting date after date.

Summary

Types

The duration type specifies a %Duration{} struct or a keyword list of valid duration unit pairs.

The duration struct type.

The unit pair type specifies a pair of a valid duration unit key and value.

Functions

Adds units of given durations d1 and d2.

Multiplies duration units by given integer.

Creates a new Duration struct from given unit_pairs.

Subtracts units of given durations d1 and d2.

Converts the given duration to a human readable representation.

Types

@type duration() :: t() | [unit_pair()]

The duration type specifies a %Duration{} struct or a keyword list of valid duration unit pairs.

The duration struct type.

The unit pair type specifies a pair of a valid duration unit key and value.

Functions

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

Adds units of given durations d1 and d2.

Respects the the highest microsecond precision of the two.

Examples

iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: 3}
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
%Duration{microsecond: {1000, 6}}

@spec from_iso8601(String.t()) :: {:ok, t()} | {:error, atom()}

Parses an ISO 8601 formatted duration string to a Duration struct.

Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:

Duration designators must be provided in order of magnitude: P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S.

Only seconds may be specified with a decimal fraction, using either a comma or a full stop: P1DT4,5S.

Examples

iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
iex> Duration.from_iso8601("P3Y-2MT3H")
{:ok, %Duration{year: 3, month: -2, hour: 3}}
iex> Duration.from_iso8601("-PT10H-30M")
{:ok, %Duration{hour: -10, minute: 30}}
iex> Duration.from_iso8601("PT4.650S")
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}

Same as from_iso8601/1 but raises an ArgumentError.

Examples

iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
iex> Duration.from_iso8601!("P10D")
%Duration{day: 10}

Multiplies duration units by given integer.

Examples

iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
%Duration{day: 3, minute: 45, second: -30}
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
%Duration{microsecond: {600, 4}}

Negates duration units.

Examples

iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
%Duration{day: -1, minute: -15, second: 10}
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
%Duration{microsecond: {-500000, 4}}

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

Creates a new Duration struct from given unit_pairs.

Raises an ArgumentError when called with invalid unit pairs.

Examples

iex> Duration.new!(year: 1, week: 3, hour: 4, second: 1)
%Duration{year: 1, week: 3, hour: 4, second: 1}
iex> Duration.new!(second: 1, microsecond: {1000, 6})
%Duration{second: 1, microsecond: {1000, 6}}
iex> Duration.new!(month: 2)
%Duration{month: 2}

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

Subtracts units of given durations d1 and d2.

Respects the the highest microsecond precision of the two.

Examples

iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: -1}
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
%Duration{microsecond: {-200, 6}}

Converts the given duration to an ISO 8601-2:2019 formatted string.

This function implements the extension of ISO 8601:2019, allowing weeks to appear between months and days: P3M3W3D.

Examples

iex> Duration.to_iso8601(Duration.new!(year: 3))
"P3Y"
iex> Duration.to_iso8601(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"P40DT12H42M12S"
iex> Duration.to_iso8601(Duration.new!(second: 30))
"PT30S"

iex> Duration.to_iso8601(Duration.new!([]))
"PT0S"

iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {2_200, 3}))
"PT1.002S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"PT-0.2000S"

Converts the given duration to a human readable representation.

Options

[  
  year: "a",  
  month: "mo",  
  week: "wk",  
  day: "d",  
  hour: "h",  
  minute: "min",  
  second: "s"  
]  

Examples

iex> Duration.to_string(Duration.new!(second: 30))
"30s"
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"40d 12h 42min 12s"

By default, this function uses ISO 80000-3 units, which uses "a" for years. But you can customize all units via the units option:

iex> Duration.to_string(Duration.new!(year: 3))
"3a"
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
"3y"

You may also choose the separator:

iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
"40d, 12h, 42min, 12s"

A duration without components is rendered as "0s":

iex> Duration.to_string(Duration.new!([]))
"0s"

Microseconds are rendered as part of seconds with the appropriate precision:

iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
"1.002s"
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"-0.2000s"