StatifierBlocks.Core.Duration (StatifierBlocks v0.30.0)

Copy Markdown View Source

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.

Summary

Functions

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

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

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

Functions

duration?(value)

@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(value)

@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(duration)

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