Flicker.Facet.Range (Flicker v0.1.0)

Copy Markdown View Source

The cast value of every range-typed facet (Spec 018) — created:2026-06-01..2026-06-30, price:10..50, created:last-30-days.

Fields

  • :from — the lower endpoint, cast per the facet's :scalar. nil is an open lower bound (..2026-06-30 means "up to and including June 30th, from whenever").
  • :to — the upper endpoint, same casting. nil is an open upper bound.
  • :preset — the id of the semantic preset this range came from (:last_30_days), or nil for an explicitly typed range.

Why :preset is carried

A resolved range and the preset that produced it are not the same fact. A pill rendering %Range{from: ~D[2026-06-29], to: ~D[2026-07-28]} can only say "Jun 29 – Jul 28"; one that also knows preset: :last_30_days can say "Last 30 days", which is what the user actually chose. It is also why presets serialise back to their token rather than their endpoints (ADR-011): a saved or shared query stays relative instead of freezing to the day it was made.

Both endpoints are nil only for the all-time preset, which is a deliberate no-op filter rather than an empty one — see unbounded?/1.

Summary

Types

t()

A cast range value.

Functions

Whether both endpoints are set and equal — a single-day date range, or a numeric range collapsed to one value.

Whether this range constrains nothing — both endpoints open.

Types

t()

@type t() :: %Flicker.Facet.Range{
  from: term() | nil,
  preset: atom() | nil,
  to: term() | nil
}

A cast range value.

Functions

single?(range)

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

Whether both endpoints are set and equal — a single-day date range, or a numeric range collapsed to one value.

Examples

iex> Flicker.Facet.Range.single?(%Flicker.Facet.Range{
...>   from: ~D[2026-07-01],
...>   to: ~D[2026-07-01]
...> })
true

iex> Flicker.Facet.Range.single?(%Flicker.Facet.Range{from: 10, to: 50})
false

iex> Flicker.Facet.Range.single?(%Flicker.Facet.Range{from: 10})
false

unbounded?(range)

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

Whether this range constrains nothing — both endpoints open.

Flicker.Query.to_filter/2 contributes no clause at all for such a range, which is what makes all-time mean "don't filter by date" rather than "match nothing".

Examples

iex> Flicker.Facet.Range.unbounded?(%Flicker.Facet.Range{})
true

iex> Flicker.Facet.Range.unbounded?(%Flicker.Facet.Range{preset: :all_time})
true

iex> Flicker.Facet.Range.unbounded?(%Flicker.Facet.Range{from: 10})
false