# `Predicator.Duration`
[🔗](https://github.com/riddler/predicator-ex/blob/v9.4.1/lib/predicator/duration.ex#L1)

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

# `add_to_date`

```elixir
@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`

```elixir
@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`

```elixir
@spec add_unit(Predicator.Types.duration(), binary(), non_neg_integer()) ::
  Predicator.Types.duration()
```

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`

```elixir
@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`

```elixir
@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`

```elixir
@spec new(keyword()) :: Predicator.Types.duration()
```

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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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"

---

*Consult [api-reference.md](api-reference.md) for complete listing*
