Ash.Type.Duration (ash v3.35.0)

Copy Markdown View Source

Represents a Duration

A builtin type that can be referenced via :duration

Constraints

  • :signs - The signs the value may have, compared against zero by Ash.Type.Duration.compare/2. Any combination is permitted: :positive or [:positive] requires a positive duration, [:positive, :zero] a non-negative one, and [:positive, :negative] a non-zero one. Omit the constraint to allow any sign. This is the sign of the duration as a whole, not of each unit — %Duration{day: 1, hour: -5} is positive, as compare/2 counts it as nineteen hours. Only where units in different buckets carry opposite signs does the sign depend on compare/2's conventions of a 30-day month and a 24-hour day. Valid values are :positive, :negative, :zero

  • :units - The units the value may be expressed in. A duration is always re-expressed in the largest of these units that will hold it, on the way in and on the way out, so [:week, :minute] turns 14 days 2 hours into 2 weeks 120 minutes. Conversion only happens within a bucket: year/month, week/day, and hour down to microsecond. A month is not a fixed number of days and a day is not a fixed number of hours (it is 23 or 25 across a daylight-saving change, which is why PostgreSQL keeps days and microseconds in separate interval buckets), so a value that no combination of the permitted units expresses exactly is rejected, including anything that would have to cross a bucket. This applies on the way out as well as in: a stored duration the permitted units cannot express is refused rather than quietly rewritten. Either a single unit, an explicit list of them, or a shorthand: :year_month ([:year, :month]) or :day_time ([:week, :day, :hour, :minute, :second, :microsecond]), which covers both the week/day and the hour to microsecond buckets. Confining an attribute to one of these keeps its comparisons free of the 30-day month (see Ash.Type.Duration.compare/2). With no constraint every unit is permitted, so the same normalization applies and nothing is ever lost.

Summary

Functions

Compares two durations as a total order, matching how the AshPostgres data layer (PostgreSQL interval) compares them: a fixed conversion of month → 30 days and day → 24 hours (so year → 360 days, week → 7 days), down to microseconds.

Functions

compare(left, right)

@spec compare(Duration.t(), Duration.t()) :: :lt | :eq | :gt

Compares two durations as a total order, matching how the AshPostgres data layer (PostgreSQL interval) compares them: a fixed conversion of month → 30 days and day → 24 hours (so year → 360 days, week → 7 days), down to microseconds.

Duration is only partially ordered in general — a month is not a fixed number of days — which is why Elixir ships Duration without a compare/2, and why data layers disagree on cross-unit comparison: PostgreSQL uses 30-day months, Neo4j ~30.44-day months, and Elixir's to_timeout/1 refuses month/ year outright. This adopts PostgreSQL's convention so in-memory comparison stays aligned with the dominant data layer rather than raising or drifting. Within a single bucket (year/month, week/day, or hour down to microsecond) the result is exact and portable across those backends. Comparison across buckets depends on a convention: the 30-day month, or the 24-hour day. The 24-hour day matches PostgreSQL's interval comparison and Elixir's DateTime.shift/3, but not PostgreSQL's timestamptz arithmetic, which adds a day on the wall clock and so can differ from 24 hours across a daylight saving change.

Computed from the integer fields directly, so microsecond precision is kept (unlike to_timeout/1, which truncates to milliseconds).

This function is the single place the convention lives; if Elixir core later gains a Duration.compare/2, it can delegate here.

handle_change?()

prepare_change?()