Zocam.Intervals (Zocam v0.1.0)

Copy Markdown View Source

The linear kernel: sets of concrete intervals on one axis, with union, intersection, complement, and difference.

An interval is a plain map with four keys:

%{from: a, until: b, left: :closed, right: :open}

 from                       until
  < span >
  
  left: :closed                right: :open
  (a is inside)                (b is outside)

A nil endpoint means "unbounded on this side": %{from: x, until: nil, ...} is the ray from x onward. A set of intervals is the %Zocam.Intervals{} struct; compress/1 keeps its list sorted and free of overlaps, and the set operations both produce and accept the empty list (the empty set).

Endpoints are Time (a daily wall window) or DateTime (a concrete window). The two kinds do not mix inside one interval. Abstract calendar values ("a Saturday", "May") do not live here: they belong to Zocam.Point, and Zocam.Span.ground/3 turns them into the concrete intervals of this module.

This module is the bottom layer of the library:

Zocam.Point  Zocam.Span  Zocam.Intervals  (this module)

Summary

Functions

Normalize any accepted shape (a struct, an interval map, option lists, a mixed list, {left, right} pairs, nil, []) into a plain list of interval maps. This is the funnel every set operation pours its input through, so each operation handles ONE shape.

Check one interval option list: from/until/left/right at most once each, and at least one of from/until present. Returns the options unchanged; raises ArgumentError otherwise.

Check the options for new!/1. Two forms are legal: a root-level interval (from:/until: directly in the list) or one or more interval: entries. Mixing the two forms raises, and every interval: entry is checked with check_interval_opts!/1.

Everything outside the operand. One bounded interval complements to two rays (a {left_ray, right_ray} pair); a half-unbounded one to a single ray; the whole timeline to nil; the empty set to the whole timeline. Each boundary closing flips (see opposite_closing/1): the instants [a, b] covers are exactly the instants (-inf, a) and (b, +inf) miss.

Is lhs entirely before rhs, with no shared instant? Touching endpoints count as "before" only when at least one side is :open (the shared instant then belongs to at most one of them).

Bring a set into its normal form: fuse everything that overlaps or touches, then sort by the left endpoint (nil first: an unbounded left side starts before everything). Two sets with the same content compress to the same list, which makes normal forms comparable with ==.

Set difference: the instants of this that are not in other. Computed as this intersected with the complement of other, so the three operations stay consistent by construction. Subtracting a middle slice cuts one interval into two; on sets the subtrahends subtract one after the other from what remains.

The instants shared by both operands. On two single intervals the result is one interval or nil; on sets it is the compressed set of all pairwise intersections. The tighter bound wins on each side, closing included: [a, b] meets (a, c) in (a, b].

Build the interval map from an option list. A missing side stays nil (unbounded). The default closings are :closed on the left and :open on the right, the half-open convention that lets adjacent intervals tile without overlap; an absent side gets nil.

Build a compressed interval set from options

Do the two operands share at least one instant? Both operands may be single intervals or whole sets; two sets overlap when any pair of their members does.

Union a list of intervals into a minimal list: overlapping and touching members fuse into one spanning interval (union/1), or add one interval into an existing list (union/2). The result is not sorted; compress/1 also sorts.

Types

at_least_one_valid()

@type at_least_one_valid() :: valid_interval() | valid_intervals()

both()

@type both() :: [
  from: timables(),
  until: timables(),
  left: closing(),
  right: closing()
]

closing()

@type closing() :: :open | :closed

interval()

@type interval() :: %{
  from: timables() | nil,
  until: timables() | nil,
  left: closing() | nil,
  right: closing() | nil
}

interval_opt()

@type interval_opt() :: {:interval, interval_opts()}

interval_opts()

@type interval_opts() :: only_from() | only_until() | both()

new_opts()

@type new_opts() :: interval_opt()

only_from()

@type only_from() :: [from: timables(), left: closing(), right: closing()]

only_until()

@type only_until() :: [until: timables(), left: closing(), right: closing()]

t()

@type t() :: %Zocam.Intervals{intervals: [interval(), ...]}

timables()

@type timables() :: Time.t() | DateTime.t()

valid_interval()

@type valid_interval() :: interval_opts() | interval()

valid_intervals()

@type valid_intervals() :: [interval_opt() | interval(), ...] | t()

Functions

check_and_extract_as_list!(unknown)

@spec check_and_extract_as_list!(valid_interval() | valid_intervals()) :: [interval()]

Normalize any accepted shape (a struct, an interval map, option lists, a mixed list, {left, right} pairs, nil, []) into a plain list of interval maps. This is the funnel every set operation pours its input through, so each operation handles ONE shape.

check_interval_opts!(opts)

@spec check_interval_opts!(interval_opts()) :: interval_opts()

Check one interval option list: from/until/left/right at most once each, and at least one of from/until present. Returns the options unchanged; raises ArgumentError otherwise.

check_new_opts!(opts)

@spec check_new_opts!([new_opts()] | interval_opts()) ::
  [new_opts()] | interval_opts()

Check the options for new!/1. Two forms are legal: a root-level interval (from:/until: directly in the list) or one or more interval: entries. Mixing the two forms raises, and every interval: entry is checked with check_interval_opts!/1.

complement(interval)

@spec complement(interval()) :: interval() | {interval(), interval()} | nil
@spec complement(t()) :: t()
@spec complement(at_least_one_valid()) :: [interval()]

Everything outside the operand. One bounded interval complements to two rays (a {left_ray, right_ray} pair); a half-unbounded one to a single ray; the whole timeline to nil; the empty set to the whole timeline. Each boundary closing flips (see opposite_closing/1): the instants [a, b] covers are exactly the instants (-inf, a) and (b, +inf) miss.

completely_before?(lhs, rhs)

@spec completely_before?(interval(), interval()) :: boolean()

Is lhs entirely before rhs, with no shared instant? Touching endpoints count as "before" only when at least one side is :open (the shared instant then belongs to at most one of them).

compress(intervals)

@spec compress(t()) :: t()
@spec compress([interval()]) :: [interval()]

Bring a set into its normal form: fuse everything that overlaps or touches, then sort by the left endpoint (nil first: an unbounded left side starts before everything). Two sets with the same content compress to the same list, which makes normal forms comparable with ==.

diff(this, other)

@spec diff(interval(), interval()) :: interval() | {interval(), interval()} | nil
@spec diff(t(), at_least_one_valid()) :: t()
@spec diff(at_least_one_valid(), at_least_one_valid()) :: [interval()]

Set difference: the instants of this that are not in other. Computed as this intersected with the complement of other, so the three operations stay consistent by construction. Subtracting a middle slice cuts one interval into two; on sets the subtrahends subtract one after the other from what remains.

intersect(this, other)

@spec intersect(interval(), interval()) :: interval() | nil
@spec intersect(t(), at_least_one_valid()) :: t()
@spec intersect(at_least_one_valid(), at_least_one_valid()) :: [interval()]

The instants shared by both operands. On two single intervals the result is one interval or nil; on sets it is the compressed set of all pairwise intersections. The tighter bound wins on each side, closing included: [a, b] meets (a, c) in (a, b].

interval_from_opts(opts)

@spec interval_from_opts(interval_opts()) :: interval()

Build the interval map from an option list. A missing side stays nil (unbounded). The default closings are :closed on the left and :open on the right, the half-open convention that lets adjacent intervals tile without overlap; an absent side gets nil.

is_interval(i)

(macro)

new!(opts)

@spec new!([new_opts()] | interval_opts()) :: t()

Build a compressed interval set from options:

new!(from: ~T[09:00:00], until: ~T[17:00:00])
new!(interval: [from: a, until: b], interval: [from: c, until: d])

Raises ArgumentError on an empty or mixed specification. The result is already in normal form (see compress/1).

overlaps?(lhs, rhs)

@spec overlaps?(interval(), interval()) :: boolean()
@spec overlaps?(valid_interval(), valid_interval() | valid_intervals()) :: boolean()

Do the two operands share at least one instant? Both operands may be single intervals or whole sets; two sets overlap when any pair of their members does.

union(ivals)

@spec union([interval()]) :: [interval()]

Union a list of intervals into a minimal list: overlapping and touching members fuse into one spanning interval (union/1), or add one interval into an existing list (union/2). The result is not sorted; compress/1 also sorts.

union(ivals, new_ival)

@spec union([interval()], interval()) :: [interval()]