The two duration spellings a :duration field may hold, and the one
form the engine reads.
RECORDED PROPOSAL - no ADR says this yet
The two-spelling stored form is campaign 014's D4 proposal, filed as
sb-709 and implemented in the spike's duration control. ADR-0002
decision 7's :duration field type is unchanged and no ADR text was
touched. Only core.send reads this module today; core.wait still
accepts ISO-8601 alone, which is the seam sb-709's control has
already crossed and this package has not.
The three forms, and why there are three
A :duration field may be stored two ways:
- ISO-8601 with integer components -
PT2H,P1DT6H- which is whatcore.waithas always held and what ADR-0001 decision 6's no-floats rule permits; - a predicator duration string -
1h30m,2d,3d8h- which is the formsb-709's control makes primary because it is the form a person types.
Neither 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 core.wait translates too.
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 and the
one core.wait validates against - 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
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.
Component-wise and lossless, for the reason StatifierBlocks.Core.Wait
writes out at length: every ISO component has an exact counterpart in
the predicator unit vocabulary, so the translation is a rename and
nothing here decides how many days a month is.
iex> StatifierBlocks.Core.Duration.to_delay("PT2H")
"2h"
iex> StatifierBlocks.Core.Duration.to_delay("P1DT6H")
"1d6h"
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