ReactiveDag.Calendar (reactive_dag v0.17.0-rc)

Copy Markdown View Source

Calendar bucketing as an ASH CALCULATION — the Ash-native answer to the classic "date-marked records → time-bucketed aggregate". The bucket is declared on the resource that OWNS the date (where derived values live in Ash), and a rollup node just groups by the calculation:

# on the data's resource
calculations do
  calculate :month, :string, {ReactiveDag.Calendar, bucket: :month, of: :observed_on}
end

# on the rollup node
reduce over: :readings,
       group_by: [:month],
       into: [sum: [value: :total], count: :n]

Buckets: :day ("2026-08-11"), :week (ISO — "2026-W33"), :month ("2026-08"), :quarter ("2026-Q3"), :year ("2026"). Labels sort lexicographically in chronological order, and the derived cell key IS the label. Accepts Date, DateTime, NaiveDateTime; nil stays nil.

This module computes in the BEAM after the read, so it works on every data layer (Ets included). A Postgres host wanting datastore pushdown declares an expr calculation instead — the rollup neither knows nor cares:

calculate :month, :string, expr(fragment("to_char(?, 'YYYY-MM')", observed_on))

Summary

Functions

The kind bucket a child KEY belongs to, by PURE string work: the key's leading |-segment is parsed as a date or a finer bucket label, and relabeled — bucket_of_key(:month, "2026-08-11|r4")"2026-08"; bucket_of_key(:month, "2026-08-11")"2026-08". :error when the leading segment isn't date-shaped (the key rule then degrades to :all).

The supported bucket kinds.

The bucket label for a date-ish value (nil-safe).

Parse a bucket LABEL back to {kind, first_date} — the pure inverse of label/2 ("2026-08"{:month, ~D[2026-08-01]}). :error for anything that isn't a bucket label. This is what makes bucket keys self-describing: a key rule can relabel a child's key without consulting any data.

The half-open date range a bucket label covers: {first, next_first}range(:month, "2026-08"){~D[2026-08-01], ~D[2026-09-01]}. :error when the label isn't a kind label. What a host (or the library's automatic bucket scoping) filters the date attribute by.

Functions

bucket_of_key(kind, key)

@spec bucket_of_key(atom(), String.t()) :: String.t() | :error

The kind bucket a child KEY belongs to, by PURE string work: the key's leading |-segment is parsed as a date or a finer bucket label, and relabeled — bucket_of_key(:month, "2026-08-11|r4")"2026-08"; bucket_of_key(:month, "2026-08-11")"2026-08". :error when the leading segment isn't date-shaped (the key rule then degrades to :all).

Deletion-safe: a vanished entry's key still parses to the bucket it left. Nesting is "the bucket containing the child bucket's START date" — exact for day→month/quarter/year and month→quarter/year; a :week child only nests deterministically into :year (weeks straddle months).

buckets()

@spec buckets() :: [atom()]

The supported bucket kinds.

describe(opts)

Callback implementation for Ash.Resource.Calculation.describe/1.

has_calculate?()

has_expression?()

Callback implementation for Ash.Resource.Calculation.has_expression?/0.

label(kind, dt)

The bucket label for a date-ish value (nil-safe).

parse(label)

@spec parse(String.t()) :: {atom(), Date.t()} | :error

Parse a bucket LABEL back to {kind, first_date} — the pure inverse of label/2 ("2026-08"{:month, ~D[2026-08-01]}). :error for anything that isn't a bucket label. This is what makes bucket keys self-describing: a key rule can relabel a child's key without consulting any data.

range(kind, label)

@spec range(atom(), String.t()) :: {Date.t(), Date.t()} | :error

The half-open date range a bucket label covers: {first, next_first}range(:month, "2026-08"){~D[2026-08-01], ~D[2026-09-01]}. :error when the label isn't a kind label. What a host (or the library's automatic bucket scoping) filters the date attribute by.

strict_loads?()

Callback implementation for Ash.Resource.Calculation.strict_loads?/0.