StatifierBlocks.Core.Duration (StatifierBlocks v0.9.0)

Copy Markdown View Source

The two duration spellings a :duration field may hold, and the one form the engine reads.

The three forms, and why there are three

A :duration field may be stored two ways:

  • a predicator duration string - 1h30m, 2d, 3d8h - the primary form, because it is the form a person types;
  • ISO-8601 with integer components - PT2H, P1DT6H - which is what documents written before the two-spelling rule hold and what ADR-0001 decision 6's no-floats rule permits.

Whichever was typed is what config holds, byte for byte: the stored form is the author's own spelling and nothing canonicalises on the way in. That is ADR-0005's 2026-08-29 :duration amendment (accepted; 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.

Neither stored form is what SCXML's delay attribute wants. Statifier resolves a delay through Statifier.Duration.to_ms/1, which reads the predicator unit grammar only - it answers {:error, {:invalid_delay, "PT2H"}} for the ISO spelling - so the emitted form is the shorthand 48h, never PT48H. A chart carrying ISO in a delay attribute compiles and then fails to arm, which is why an emitter never writes out what it was handed.

So a compile is two steps rather than one: to_iso/1 canonicalises whichever spelling was stored into ISO-8601, and to_delay/1 renders that canonical value as the attribute the engine reads. ISO is the pivot because it is the spelling ADR-0001 already admits into config - a single canonical form keeps "which of the two did the author type?" out of everything downstream of the emitter.

The predicator 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 exactly one value predicator parses that this module still refuses: a duration with milliseconds left in it after normalisation. ISO-8601 has no millisecond component and ADR-0001 decision 6 forbids the fractional seconds that would be needed to spell one, so 500ms has no canonical form to compile to. That is a fact about the pivot, not a disagreement about the grammar, and it is the whole of the gap.

sb-709's control refuses a wider set - ms, fractional components and repeated units alike. Being the more permissive of the two is the safe direction: every value that control can write, this module compiles.

Summary

Functions

True for either stored spelling: an ISO-8601 duration, or a predicator duration string this module can canonicalise.

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

Renders a canonical ISO-8601 duration as the delay attribute the engine reads.

Canonicalises a stored duration to ISO-8601.

Functions

duration?(value)

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

True for either stored spelling: an ISO-8601 duration, or a predicator duration string this module can canonicalise.

predicator?(value)

@spec predicator?(term()) :: boolean()

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

to_delay(arg)

@spec to_delay(String.t()) :: String.t()

Renders a canonical ISO-8601 duration as the delay attribute the engine reads.

Component-wise and lossless. Statifier.Duration recognizes y, mo, w, d, h, m, s and ms - a strict superset of the SCXML schema's five units - so every ISO component has an exact counterpart and the translation is a rename rather than an arithmetic conversion:

ISOdelay
P1Y1y
P1M (before T)1mo
P1W / P1D1w / 1d
PT1H / PT1M / PT1S1h / 1m / 1s

This is the vocabulary's one such table, and the emitters cite it rather than carrying their own. Nothing here decides how many days a month is, because nothing here has to: the ambiguity stays where the author wrote it and is resolved by the one duration vocabulary the platform shares.

iex> StatifierBlocks.Core.Duration.to_delay("PT2H")
"2h"

iex> StatifierBlocks.Core.Duration.to_delay("P1DT6H")
"1d6h"

to_iso(value)

@spec to_iso(term()) :: {:ok, String.t()} | :error

Canonicalises a stored duration to ISO-8601.

An ISO value passes through unchanged - byte for byte, so a document that stored P1DT6H compiles from exactly those bytes. A predicator string is compiled through Predicator.Duration.parse/1. Anything else is :error, and the caller turns that into a finding rather than a raise.

iex> StatifierBlocks.Core.Duration.to_iso("PT2H")
{:ok, "PT2H"}

iex> StatifierBlocks.Core.Duration.to_iso("1h30m")
{:ok, "PT1H30M"}

iex> StatifierBlocks.Core.Duration.to_iso("3d8h")
{:ok, "P3DT8H"}

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