StatifierBlocks.Core.Duration (StatifierBlocks v0.2.0)

Copy Markdown View Source

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 what core.wait has always held and what ADR-0001 decision 6's no-floats rule permits;
  • a predicator duration string - 1h30m, 2d, 3d8h - which is the form sb-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

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

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