# `StatifierBlocks.Core.Duration`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/core/duration.ex#L1)

The one duration spelling a `:duration` field may hold, and the
attribute the engine reads.

## One grammar in, one attribute out

A `:duration` field is **stored** as the string a person types -
`30s`, `15m`, `1h30m`, `2d`, `3d8h`. That is the whole of the accepted
form. Whichever string was typed is what `config` holds, byte for byte:
nothing canonicalises on the way in. That is ADR-0005 decision 9 as
amended 2026-09-05 (clause 9a; ADR-0002 decision 7's field type is
untouched - `:duration` still holds a string, and what changed is which
strings). Every type with a `:duration` field reads this module:
`core.send`'s `delay` and `core.wait`'s `duration`.

Compiling one is a single step. `parse/1` hands the stored string to
`Predicator.Duration.parse/1` and gets back that grammar's normalised
duration; `to_delay/1` renders that normalised value as the `delay`
attribute the engine reads. There is no intermediate spelling and
therefore no third form to keep in step - clause 9c's whole point.

Two things follow that the earlier arrangement could not give.
Sub-second and fractional spellings become expressible: `500ms` renders
as `500ms`, and `1.5s` normalises to one second and five hundred
milliseconds and renders as `1s500ms`. Both are values
`Statifier.Duration.to_ms/1` resolves, because the engine reads the
same grammar this module does.

## The grammar has one home, and it is not here

`Predicator.Duration.parse/1` is the grammar's owner and this module
calls it rather than re-deriving any part of it. Whatever predicator
accepts, this module accepts; whatever predicator normalises - a
fraction expanded into whole components, a repeated unit accumulated,
the documented 30-day and 365-day approximations on `mo` and `y` - is
predicator's semantics arriving here already decided, not a second
opinion formed here. There is no value predicator parses that this
module refuses.

# `duration?`

```elixir
@spec duration?(term()) :: boolean()
```

True for a stored duration this module can compile. Total for any term -
a non-binary is simply not a duration.

    iex> StatifierBlocks.Core.Duration.duration?("1h30m")
    true

    iex> StatifierBlocks.Core.Duration.duration?("soon")
    false

    iex> StatifierBlocks.Core.Duration.duration?(nil)
    false

# `parse`

```elixir
@spec parse(term()) :: {:ok, map()} | :error
```

Parses a stored duration into the expression language's normalised
duration.

Anything the grammar does not accept is `:error`, and the caller turns
that into a finding rather than a raise.

    iex> StatifierBlocks.Core.Duration.parse("1h30m")
    {:ok, %{years: 0, months: 0, weeks: 0, days: 0, hours: 1, minutes: 30, seconds: 0, milliseconds: 0}}

    iex> StatifierBlocks.Core.Duration.parse("soon")
    :error

# `to_delay`

```elixir
@spec to_delay(map()) :: String.t()
```

Renders a normalised duration as the `delay` attribute the engine reads.

Component-wise and lossless, largest unit first, omitting the components
that are zero. A duration whose components are all zero renders as
`0s` rather than as the empty string, because an empty `delay` is not a
delay of no time.

    iex> {:ok, duration} = StatifierBlocks.Core.Duration.parse("2h")
    iex> StatifierBlocks.Core.Duration.to_delay(duration)
    "2h"

    iex> {:ok, duration} = StatifierBlocks.Core.Duration.parse("1.5s")
    iex> StatifierBlocks.Core.Duration.to_delay(duration)
    "1s500ms"

---

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