Current section
Files
Jump to
Current section
Files
lib/closed_intervals.ex
defmodule ClosedIntervals do
@moduledoc """
A ClosedIntervals datastructure.
`ClosedIntervals` represents a set of closed intervals and provides functions to
retrieve the interval to which a given value belongs to. `ClosedIntervals` can
handle arbitrary data, as long as it can be ordered in a sensible way. Users
can either use the default term order `&<=/2` if that suits their needs, or
provide an explicit order function.
"""
alias ClosedIntervals.Tree
require Tree
@enforce_keys [:tree, :order, :eq]
defstruct @enforce_keys
@type t(data) :: %__MODULE__{
tree: Tree.t(data),
order: (data, data -> boolean()),
eq: (data, data -> boolean()) | nil
}
@type interval(data) :: {data, data} | {:"-inf", data} | {data, :"+inf"}
@doc """
Create a new `ClosedIntervals` from points.
This function creates a new `ClosedIntervals` from an `Enum` of points. The points
can be of any form, as long as they can be ordered sensibly. For types where the
term order does not order them in a way such that the resulting order represents
a linear ordering along the interval range, a custom order can be applied using the
`order` parameter. `order` defaults to `&<=/2`. Note that a custom order should return
true for equal points, if the resulting order has to be stable.
Additionally, an explicit equality function can be provided which is used in
`ClosedIntervals.get_interval/2` and `ClosedIntervals.get_all_intervals/2`.
## Errors
The function expects that the `enum` contains at least two points. If that is not the case,
an `ArgumentError` is raised.
iex> from([1])
** (ArgumentError) Need at least two points to construct a ClosedIntervals
## Examples
`from/1,2` can handle plain types:
iex> from([1, 2, 3]) |> leaf_intervals()
[{1, 2}, {2, 3}]
It can also handle nested types, if a suitable `order` is defined:
iex> points = [%{idx: 3}, %{idx: 7}, %{idx: 1}]
iex> points |> from(order: &(&1.idx <= &2.idx)) |> leaf_intervals()
[{%{idx: 1}, %{idx: 3}}, {%{idx: 3}, %{idx: 7}}]
## Arguments
* `:order`: A custom order defined on the points used to construct the `ClosedIntervals`
* `:eq`: A custom equality defined on the points used to construct the `ClosedIntervals`
"""
@spec from(Enum.t(), Keyword.t()) :: t(term())
def from(enum, args \\ []) do
{order, eq} = parse_args!(args)
case Enum.sort(enum, order) do
points = [_, _ | _] ->
%__MODULE__{
tree: Tree.construct(points),
order: order,
eq: eq
}
_ ->
raise ArgumentError, "Need at least two points to construct a ClosedIntervals"
end
end
defp parse_args!(args) do
order = Keyword.get(args, :order, &<=/2)
eq = Keyword.get(args, :eq)
if !is_function(order, 2) do
raise ArgumentError, "Expecting :order to be a function of arity 2"
end
if eq && !is_function(eq, 2) do
raise ArgumentError, "Expecting :eq to be a function of arity 2"
end
{order, eq}
end
@doc """
Reconstruct a `ClosedInterval` from the output of `leaf_intervals/1`.
Note that the `args` must match the arguments used when originally constructing the
`ClosedInterval` with `from/1,2`.
## Errors
If the least of leaf intervals is not the result of `leaf_intervals/1`, this can result
in an `ArgumentError`.
## Example
iex> closed_intervals = from([1, 2, 3])
iex> leaf_intervals = leaf_intervals(closed_intervals)
iex> from_leaf_intervals(leaf_intervals)
iex> closed_intervals == from_leaf_intervals(leaf_intervals)
true
"""
def from_leaf_intervals(leaf_intervals = [_ | _], args \\ []) do
tree =
leaf_intervals
|> Enum.map(&Tree.from_bounds/1)
|> Tree.from_leaf_intervals()
{order, eq} = parse_args!(args)
%__MODULE__{
tree: tree,
order: order,
eq: eq
}
end
@doc """
Retrieve a list of all leaf intervals.
A leaf interval is an interval which has been constructed from two adjacent
points. It does not expand to `:"-inf"` or `:"+inf"`.
See `from_leaf_intervals/1,2`. We can reconstruct the original `ClosedInterval`
from a list of leaf intervals.
## Example
iex> from([1, 2, 3]) |> leaf_intervals()
[{1, 2}, {2, 3}]
"""
@spec leaf_intervals(t(data)) :: [{data, data}] when data: var
def leaf_intervals(%__MODULE__{tree: tree}) do
tree |> Tree.leaf_intervals()
end
@doc """
Get the interval to which a value belongs to.
## Example
iex> closed_intervals = from([1, 2, 5])
iex> get_interval(closed_intervals, 3)
{2, 5}
"""
@spec get_interval(t(data), data) :: interval(data)
when data: var
def get_interval(closed_intervals = %__MODULE__{}, value) do
case get_all_intervals(closed_intervals, value) do
[interval] ->
interval
[inf = {:"-inf", _} | _] ->
inf
[inf = {_, :"+inf"} | _] ->
inf
end
end
@doc """
Retrieve all intervals which cover `value`.
This function is useful if the index points used to define the `ClosedIntervals` are not
unique. For example, when defining a step-function, it might make sense to use the same
point multiple times but with different data in order to represent a sharp step. Values
which are placed right at the interval bounds can then belong to multiple closed intervals.
"""
@spec get_all_intervals(t(data), data) :: [interval(data)]
when data: var
def get_all_intervals(%__MODULE__{tree: tree, eq: eq, order: order}, value) do
eq = eq || fn _, _ -> false end
left_bound = Tree.tree(tree, :left_bound)
right_bound = Tree.tree(tree, :right_bound)
cond do
order.(value, left_bound) ->
neg_inf = [{:"-inf", Tree.tree(tree, :left_bound)}]
if eq.(value, left_bound) do
neg_inf ++ Tree.get_all_intervals(tree, value, eq, order)
else
neg_inf
end
order.(right_bound, value) ->
pos_inf = [{Tree.tree(tree, :right_bound), :"+inf"}]
if eq.(value, right_bound) do
pos_inf ++ Tree.get_all_intervals(tree, value, eq, order)
else
pos_inf
end
true ->
Tree.get_all_intervals(tree, value, eq, order)
end
|> List.flatten()
end
@doc """
Serialize `ClosedIntervals` into a list.
## Example
iex> closed_intervals = from([1, 2, 3])
iex> to_list(closed_intervals)
[1, 2, 3]
We can construct the original `ClosedInterval` from a list
generated by `to_list/1`:
iex> closed_intervals = from([1, 2, 3])
iex> to_list(closed_intervals)
iex> closed_intervals == closed_intervals |> to_list() |> from()
true
"""
@spec to_list(t(data)) :: [data] when data: var
def to_list(closed_intervals = %__MODULE__{}) do
Tree.to_list(closed_intervals.tree)
end
@doc """
Map a function over all intervals.
## Example
iex> closed_intervals = from([1, 2, 3])
iex> map(closed_intervals, & &1 + 1) |> to_list()
[2, 3, 4]
"""
@spec map(t(data), (data -> data)) :: t(data) when data: var
def map(closed_intervals = %__MODULE__{}, mapper) when is_function(mapper, 1) do
%__MODULE__{closed_intervals | tree: Tree.map(closed_intervals.tree, mapper)}
end
@doc """
Retrieve the left bound of a `ClosedIntervals`.
## Example
iex> [1, 2, 3] |> from() |> left_bound()
1
"""
def left_bound(%__MODULE__{tree: tree}) do
Tree.left_bound(tree)
end
@doc """
Retrieve the right bound of a `ClosedIntervals`.
## Example
iex> [1, 2, 3] |> from() |> right_bound()
3
"""
def right_bound(%__MODULE__{tree: tree}) do
Tree.right_bound(tree)
end
defimpl Inspect, for: ClosedIntervals do
import Inspect.Algebra
def inspect(closed_intervals, opts) do
concat([
"#ClosedIntervals<",
to_doc(ClosedIntervals.leaf_intervals(closed_intervals), opts),
">"
])
end
end
end