The behaviour a Tempo.IntervalSet storage backend implements.
An interval set's representation is pluggable: the set struct carries a
:backend module and an opaque backend state (in its :intervals
field), and every Tempo.IntervalSet function reaches the members
through this contract rather than assuming a concrete data structure.
Tempo.IntervalSet.Backend.List — a sorted plain list — is the default
and the reference implementation.
The contract
The universal primitive is the ordered walk: walk/1 returns an
enumerable that yields the member intervals in time order, sorted by
from endpoint, disjoint, each half-open [from, to). A backend is
correct as soon as it can walk; the remaining callbacks exist so a
backend can answer common questions without a full walk, and default
to walk-derived implementations via use Tempo.IntervalSet.Backend.
bounded?/1 is the honesty bit: a backend over a finite member list
returns true; a lazy backend over a potentially unbounded generator
returns false, and aggregate operations (to_list/1, count/1,
set-wide duration) refuse rather than walk forever. Callers must
consult bounded?/1 before calling to_list/1 or count/1.
Implementing a backend
defmodule MyBackend do
use Tempo.IntervalSet.Backend
@impl true
def from_list(intervals, _options), do: build_my_state(intervals)
@impl true
def to_list(state), do: my_state_to_list(state)
@impl true
def walk(state), do: my_lazy_stream(state)
@impl true
def bounded?(_state), do: true
enduse provides overridable count/1, empty?/1, and first/1
derived from to_list/1 and walk/1; override them when the
representation can answer faster (a tree knows its size; any backend
can peek its first member without materialising).
Construct a set on a specific backend with
Tempo.IntervalSet.new(intervals, backend: MyBackend). Set operations
preserve the backend of member-preserving results; newly constructed
extents default to the list backend.
Summary
Types
The backend's opaque representation of the member intervals. For the list backend this is the member list itself; other backends choose their own shape.
Callbacks
Whether the member list is finite. Aggregate operations consult this
before materialising; false makes them refuse rather than hang.
The number of members. Only valid when bounded?/1 is true.
Whether the set has no members. Safe on every backend — peeking one element never requires a full walk.
The earliest member, or nil when empty. Safe on every backend.
Build backend state from a sorted, disjoint member list.
Candidate members whose extent may intersect the half-open UTC-second
range [lo, hi).
The members as a plain list in time order. Only valid when
bounded?/1 is true; an unbounded backend raises.
An enumerable yielding the members in time order. The universal primitive: safe on every backend, lazy where the backend is lazy.
Types
@type state() :: term()
The backend's opaque representation of the member intervals. For the list backend this is the member list itself; other backends choose their own shape.
Callbacks
Whether the member list is finite. Aggregate operations consult this
before materialising; false makes them refuse rather than hang.
@callback count(state()) :: non_neg_integer()
The number of members. Only valid when bounded?/1 is true.
Whether the set has no members. Safe on every backend — peeking one element never requires a full walk.
@callback first(state()) :: Tempo.Interval.t() | nil
The earliest member, or nil when empty. Safe on every backend.
@callback from_list( [Tempo.Interval.t()], keyword() ) :: state()
Build backend state from a sorted, disjoint member list.
Tempo.IntervalSet.new/2 validates, sorts, and (by default)
coalesces the members before calling this — the backend may assume
time order and disjointness.
@callback overlapping( state(), {number(), number()} ) :: [Tempo.Interval.t()]
Candidate members whose extent may intersect the half-open UTC-second
range [lo, hi).
A pruning callback, not a semantic one: the contract is to return
at least every member whose [from, to) intersects the range — a
backend may return extra members, or all of them (the default). The
caller applies the exact resolution-aware check to the candidates, so
a backend that prunes well (an interval tree) accelerates stabbing
and overlap queries without owning their semantics.
@callback to_list(state()) :: [Tempo.Interval.t()]
The members as a plain list in time order. Only valid when
bounded?/1 is true; an unbounded backend raises.
@callback walk(state()) :: Enumerable.t()
An enumerable yielding the members in time order. The universal primitive: safe on every backend, lazy where the backend is lazy.