Ash.Range (ash v3.32.2)

Copy Markdown View Source

A continuous range of values of some inner type, with inclusive/exclusive bounds.

The value representation for Ash.Type.Range. bounds follows Postgres range notation: the first character is the lower bound, the second the upper — [ / ] inclusive, ( / ) exclusive. A nil lower/upper is an unbounded (infinite) end. The default :"[)" (lower-inclusive, upper-exclusive) is the convention that lets adjacent ranges tile a timeline without overlap.

A range containing no points is empty, and every empty range is the same range. Ash.Type.Range casts any such range to empty/0, whose bounds are dropped — as Postgres does — so that empty ranges compare equal and survive storage in a data layer that keeps no bounds for them.

Summary

Types

One of Allen's thirteen interval relations.

t()

Functions

Whether two ranges are adjacent: one ends exactly where the other begins, with no point between them and none shared.

Compares two ranges as Postgres orders them: empty first, then by lower bound, then by upper, an unbounded end as -∞/+∞, and the earlier boundary first where two bounds name the same value ([1 before (1, 5) before 5]).

Whether the range holds value, which may be a point or another range.

The empty range: the one range containing no points.

Whether the range contains no points.

Whether two ranges share any point.

Whether the range's lower bound includes the point it names.

Which of Allen's thirteen relations left bears to right, or nil if either is empty.

Every relation relation/2 can answer, in Allen's canonical order.

Whether the range's upper bound includes the point it names.

Whether the given atom is a valid bounds specifier.

Types

allen()

@type allen() ::
  :precedes
  | :meets
  | :overlaps
  | :finished_by
  | :contains
  | :starts
  | :equals
  | :started_by
  | :during
  | :finishes
  | :overlapped_by
  | :met_by
  | :preceded_by

One of Allen's thirteen interval relations.

bounds()

@type bounds() :: :"[)" | :"[]" | :"()" | :"(]"

t()

@type t() :: %Ash.Range{
  bounds: bounds(),
  empty?: boolean(),
  lower: term() | nil,
  upper: term() | nil
}

Functions

adjacent?(left, right)

@spec adjacent?(t(), t()) :: boolean()

Whether two ranges are adjacent: one ends exactly where the other begins, with no point between them and none shared.

The seam counts only when exactly one side includes it, so [1,5) is adjacent to [5,9), where [1,5] overlaps it and (5,9) leaves a gap. Symmetric, unlike Allen's meets, which is directional. An empty range is adjacent to nothing.

Adjacency is what lets a series of ranges tile: each meets the next, covering everything between the first lower bound and the last upper without overlapping.

For a discrete inner type this is a question about the canonical form — [1,4] and [5,9) are adjacent as integers but not as decimals — so compare values that have been through Ash.Type.Range, which canonicalises them. Matches Postgres -|-.

compare(left, right)

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

Compares two ranges as Postgres orders them: empty first, then by lower bound, then by upper, an unbounded end as -∞/+∞, and the earlier boundary first where two bounds name the same value ([1 before (1, 5) before 5]).

A sort order rather than containment: [1,10) sorting before [3,5) says nothing about one holding the other.

contains?(range, value)

@spec contains?(t(), term()) :: boolean()

Whether the range holds value, which may be a point or another range.

An unbounded end holds everything beyond it, and an empty range holds no point. Each bound is compared with Comp, so an inner type behaves inside a range as it does outside one, and a bound that excludes its own value (( or )) is not held.

A range holds another when the second lies within the first, sharing an endpoint or being equal included. Every range holds the empty range, as Postgres @> does.

empty()

@spec empty() :: t()

The empty range: the one range containing no points.

empty?(range)

@spec empty?(t()) :: boolean()

Whether the range contains no points.

True for empty/0, and for a bounded range whose bounds admit nothing: a lower above its upper, or bounds that meet without both including the point they meet at. An unbounded end is never empty.

intersects?(left, right)

@spec intersects?(t(), t()) :: boolean()

Whether two ranges share any point.

An empty range intersects nothing, not even itself. Each range must start at or before the other ends, and a boundary the two ranges share counts only when both sides include it — so [1,3) and [3,5) do not intersect, where [1,3] and [3,5) do. Bounds are compared with Comp, as everywhere else here.

Named for what it answers rather than for the operator it backs. Postgres calls && "overlap" and range_overlaps/2 keeps that name, but Allen's overlaps is the narrower relation where two ranges cross with neither containing the other — under which [1,10) and [3,5) do not overlap. This returns true for them.

lower_inclusive?(bounds)

@spec lower_inclusive?(bounds()) :: boolean()

Whether the range's lower bound includes the point it names.

relation(left, right)

@spec relation(t(), t()) :: allen() | nil

Which of Allen's thirteen relations left bears to right, or nil if either is empty.

Exactly one holds for any pair of non-empty ranges, so the answer classifies rather than tests. An empty range has no relation to anything: it precedes nothing and is during nothing, having no points to be positioned by.

Two ranges meet when one ends where the other begins and exactly one of them includes that point — [1,5) meets [5,9), where [1,5] overlaps it and (5,9) merely follows it. Matches Postgres -|-.

relations()

@spec relations() :: [allen()]

Every relation relation/2 can answer, in Allen's canonical order.

Sorted by how far left begins before right, then by how far it ends before, with equals at the centre and each relation the converse of its mirror. A set of relations is conventionally a thirteen-bit mask, so these are the bit positions.

upper_inclusive?(bounds)

@spec upper_inclusive?(bounds()) :: boolean()

Whether the range's upper bound includes the point it names.

valid_bounds?(bounds)

@spec valid_bounds?(term()) :: boolean()

Whether the given atom is a valid bounds specifier.