Predicator.Duration (predicator v8.0.0)

Copy Markdown View Source

Duration utilities for time span calculations in Predicator expressions.

This module provides functions to create, manipulate, and convert duration values for use in relative date expressions and date arithmetic.

Examples

iex> Predicator.Duration.new(days: 3, hours: 8)
%{years: 0, months: 0, weeks: 0, days: 3, hours: 8, minutes: 0, seconds: 0, milliseconds: 0}

iex> Predicator.Duration.from_units([{"3", "d"}, {"8", "h"}])
{:ok, %{years: 0, months: 0, weeks: 0, days: 3, hours: 8, minutes: 0, seconds: 0, milliseconds: 0}}

iex> Predicator.Duration.to_seconds(%{days: 1, hours: 2, minutes: 30})
95400

Summary

Functions

Adds a duration to a Date, returning a Date.

Adds a duration to a DateTime, returning a DateTime.

Adds a specific unit amount to a duration.

Expands a fractional duration component into whole-unit integer pairs.

Creates a duration from parsed unit pairs.

Creates a new duration with specified time units.

Parses a duration literal string into a duration map.

Subtracts a duration from a Date, returning a Date.

Subtracts a duration from a DateTime, returning a DateTime.

Converts a duration to total milliseconds (approximate for months and years).

Converts a duration to total seconds (approximate for months and years).

Converts a duration to a human-readable string.

Functions

add_to_date(date, duration)

@spec add_to_date(Date.t(), Predicator.Types.duration()) :: Date.t()

Adds a duration to a Date, returning a Date.

Examples

iex> date = ~D[2024-01-15]
iex> duration = Predicator.Duration.new(days: 3, weeks: 1)
iex> Predicator.Duration.add_to_date(date, duration)
~D[2024-01-25]

add_to_datetime(datetime, duration)

@spec add_to_datetime(DateTime.t(), Predicator.Types.duration()) :: DateTime.t()

Adds a duration to a DateTime, returning a DateTime.

Examples

iex> datetime = ~U[2024-01-15T10:30:00Z]
iex> duration = Predicator.Duration.new(days: 2, hours: 3, minutes: 30)
iex> Predicator.Duration.add_to_datetime(datetime, duration)
~U[2024-01-17T14:00:00Z]

add_unit(duration, unit, value)

Adds a specific unit amount to a duration.

Examples

iex> duration = Predicator.Duration.new(days: 1)
iex> Predicator.Duration.add_unit(duration, "h", 3)
%{years: 0, months: 0, weeks: 0, days: 1, hours: 3, minutes: 0, seconds: 0, milliseconds: 0}

expand_fraction(integer_part, fraction_digits, unit)

@spec expand_fraction(non_neg_integer(), binary(), binary()) ::
  {:ok, [{non_neg_integer(), binary()}]}
  | {:error, :subunit_remainder}
  | {:error, :unknown_unit}

Expands a fractional duration component into whole-unit integer pairs.

integer_part is the whole-number portion of the component and fraction_digits its fractional digits as a literal decimal string (never a parsed float - see Decision 2 in the decision record cited above: binary floats must not appear anywhere on this path). unit is the component's source unit.

The fraction is valid only if it converts to an exact whole number of milliseconds; a sub-millisecond remainder is {:error, :subunit_remainder} rather than rounded or truncated. A valid remainder decomposes greedily, largest-first, through d, h, m, s, ms only - never back into w, mo, or y - and the integer part (when non-zero) keeps its own source unit. An unrecognized unit is {:error, :unknown_unit}.

Examples

iex> Predicator.Duration.expand_fraction(1, "5", "s")
{:ok, [{1, "s"}, {500, "ms"}]}

iex> Predicator.Duration.expand_fraction(0, "5", "mo")
{:ok, [{15, "d"}]}

iex> Predicator.Duration.expand_fraction(0, "5", "ms")
{:error, :subunit_remainder}

iex> Predicator.Duration.expand_fraction(1, "5", "x")
{:error, :unknown_unit}

from_units(unit_pairs)

@spec from_units([{binary(), binary()}]) ::
  {:ok, Predicator.Types.duration()} | {:error, binary()}

Creates a duration from parsed unit pairs.

Takes a list of {value, unit} tuples and converts them to a duration.

Examples

iex> Predicator.Duration.from_units([{"3", "d"}, {"8", "h"}])
{:ok, %{years: 0, months: 0, weeks: 0, days: 3, hours: 8, minutes: 0, seconds: 0, milliseconds: 0}}

iex> Predicator.Duration.from_units([{"invalid", "d"}])
{:error, "Invalid duration value: invalid"}

new(opts \\ [])

Creates a new duration with specified time units.

All unspecified units default to 0.

Examples

iex> Predicator.Duration.new(days: 2, hours: 3)
%{years: 0, months: 0, weeks: 0, days: 2, hours: 3, minutes: 0, seconds: 0, milliseconds: 0}

iex> Predicator.Duration.new()
%{years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 0}

parse(string)

@spec parse(binary()) :: {:ok, Predicator.Types.duration()} | :error

Parses a duration literal string into a duration map.

This is the inverse of to_string/1: the whole string must be a sequence of one or more <digits>(.<digits>)?<unit> pairs, with no whitespace, no sign, and no partial consumption. The accepted units are exactly the eight to_string/1 emits - y, mo, w, d, h, m, s, ms - with mo and ms matched before the single-character units so "1mo" is one month, not one minute followed by a stray o. Values are non-negative integers, optionally with a decimal fraction - there is no bare fraction (".5s") and no trailing dot ("1.s"). A fractional component must convert to an exact whole number of milliseconds or the whole string is :error; a valid fraction expands to the integer part on its own unit plus a remainder decomposed largest-first through d, h, m, s, ms only. Fractions are permitted on every unit; mo and y fractions commit the documented 30-day and 365-day approximations at parse time (so parse("0.5mo") yields days: 15 and no months). Repeated units accumulate, matching add_unit/3, expansions included. Anything else - a bad unit, trailing or leading junk, a sign, a bare number, or the empty string - is :error.

Examples

iex> Predicator.Duration.parse("3d8h30m")
{:ok, %{years: 0, months: 0, weeks: 0, days: 3, hours: 8, minutes: 30, seconds: 0, milliseconds: 0}}

iex> Predicator.Duration.parse("0s")
{:ok, %{years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 0, milliseconds: 0}}

iex> Predicator.Duration.parse("1d ")
:error

iex> Predicator.Duration.parse("1.5s")
{:ok, %{years: 0, months: 0, weeks: 0, days: 0, hours: 0, minutes: 0, seconds: 1, milliseconds: 500}}

iex> Predicator.Duration.parse("0.5mo")
{:ok, %{years: 0, months: 0, weeks: 0, days: 15, hours: 0, minutes: 0, seconds: 0, milliseconds: 0}}

iex> Predicator.Duration.parse("0.5ms")
:error

subtract_from_date(date, duration)

@spec subtract_from_date(Date.t(), Predicator.Types.duration()) :: Date.t()

Subtracts a duration from a Date, returning a Date.

Examples

iex> date = ~D[2024-01-25]
iex> duration = Predicator.Duration.new(days: 3, weeks: 1)
iex> Predicator.Duration.subtract_from_date(date, duration)
~D[2024-01-15]

subtract_from_datetime(datetime, duration)

@spec subtract_from_datetime(DateTime.t(), Predicator.Types.duration()) ::
  DateTime.t()

Subtracts a duration from a DateTime, returning a DateTime.

Examples

iex> datetime = ~U[2024-01-17T14:00:00Z]
iex> duration = Predicator.Duration.new(days: 2, hours: 3, minutes: 30)
iex> Predicator.Duration.subtract_from_datetime(datetime, duration)
~U[2024-01-15T10:30:00Z]

to_milliseconds(duration)

@spec to_milliseconds(Predicator.Types.duration()) :: integer()

Converts a duration to total milliseconds (approximate for months and years).

Uses approximate conversions:

  • 1 month = 30 days
  • 1 year = 365 days

Examples

iex> Predicator.Duration.to_milliseconds(%{seconds: 1, milliseconds: 500})
1500

iex> Predicator.Duration.to_milliseconds(%{minutes: 1, seconds: 30, milliseconds: 250})
90250

to_seconds(duration)

@spec to_seconds(Predicator.Types.duration()) :: integer()

Converts a duration to total seconds (approximate for months and years).

Uses approximate conversions:

  • 1 month = 30 days
  • 1 year = 365 days

Examples

iex> Predicator.Duration.to_seconds(%{days: 1, hours: 2, minutes: 30, seconds: 15})
95415

iex> Predicator.Duration.to_seconds(%{weeks: 2})
1209600

to_string(duration)

@spec to_string(Predicator.Types.duration()) :: binary()

Converts a duration to a human-readable string.

Examples

iex> duration = Predicator.Duration.new(days: 3, hours: 8, minutes: 30)
iex> Predicator.Duration.to_string(duration)
"3d8h30m"

iex> duration = Predicator.Duration.new(weeks: 2)
iex> Predicator.Duration.to_string(duration)
"2w"