Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dev fix 15 #17

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,11 @@ makes sense. This is done by defining an explicit order on
`ClosedIntervals.from/2` and `ClosedIntervals.get_interval/2`:

iex> import ClosedIntervals
iex> points = [%{idx: 1, data: :hello}, %{idx: 5, data: :world}]
iex> points = [%Indexed{idx: 1, data: :hello}, %Indexed{idx: 5, data: :world}]
iex> order = fn a, b -> a.idx <= b.idx end
iex> closed_intervals = from(points, order: order)
iex> get_interval(closed_intervals, %{idx: 3})
{%{idx: 1, data: :hello}, %{idx: 5, data: :world}}
iex> get_interval(closed_intervals, %Indexed{idx: 3})
{%Indexed{idx: 1, data: :hello}, %Indexed{idx: 5, data: :world}}

`ClosedIntervals` can also handle non-unique indices. This is useful when defining
a function step-wise. Note that in such a case, the intervals for a value should be retrieved
Expand All @@ -70,17 +70,17 @@ we must define an equality function as well. Usually, this function compares the
the order function.

iex> import ClosedIntervals
iex> points = [%{idx: 1, data: :hello}, %{idx: 1, data: :between}, %{idx: 5, data: :world}]
iex> points = [%Indexed{idx: 1, data: :hello}, %Indexed{idx: 1, data: :between}, %Indexed{idx: 5, data: :world}]
iex> order = fn a, b -> a.idx <= b.idx end
iex> eq = fn a, b -> a.idx == b.idx end
iex> closed_intervals = from(points, order: order, eq: eq)
iex> get_all_intervals(closed_intervals, %{idx: 3})
[{%{idx: 1, data: :between}, %{idx: 5, data: :world}}]
iex> get_all_intervals(closed_intervals, %{idx: 1})
iex> get_all_intervals(closed_intervals, %Indexed{idx: 3})
[{%Indexed{idx: 1, data: :between}, %Indexed{idx: 5, data: :world}}]
iex> get_all_intervals(closed_intervals, %Indexed{idx: 1})
[
{:"-inf", %{idx: 1, data: :hello}},
{%{idx: 1, data: :hello}, %{idx: 1, data: :between}},
{%{idx: 1, data: :between}, %{idx: 5, data: :world}}
{:"-inf", %Indexed{idx: 1, data: :hello}},
{%Indexed{idx: 1, data: :hello}, %Indexed{idx: 1, data: :between}},
{%Indexed{idx: 1, data: :between}, %Indexed{idx: 5, data: :world}}
]

## Inspect
Expand Down
79 changes: 32 additions & 47 deletions lib/closed_intervals.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ defmodule ClosedIntervals do
alias ClosedIntervals.Tree
require Tree

@enforce_keys [:tree, :order, :eq]
@enforce_keys [:tree]
defstruct @enforce_keys

@type t(data) :: %__MODULE__{
tree: Tree.t(data),
order: (data, data -> boolean()),
eq: (data, data -> boolean()) | nil
tree: Tree.t(data)
}

@type interval(data) :: {data, data} | {:"-inf", data} | {data, :"+inf"}
Expand Down Expand Up @@ -54,48 +52,29 @@ defmodule ClosedIntervals do

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}}]
iex> points = [%Indexed{idx: 3}, %Indexed{idx: 7}, %Indexed{idx: 1}]
iex> points |> from() |> leaf_intervals()
[{%Indexed{idx: 1}, %Indexed{idx: 3}}, {%Indexed{idx: 3}, %Indexed{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
@spec from(Enum.t()) :: t(term())
def from(enum) do
case Enum.sort(enum, Compare) do
points = [_, _ | _] ->
%__MODULE__{
tree: Tree.construct(points),
order: order,
eq: eq
tree: Tree.construct(points)
}

_ ->
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`.

Expand All @@ -116,18 +95,14 @@ defmodule ClosedIntervals do
true

"""
def from_leaf_intervals(leaf_intervals = [_ | _], args \\ []) do
def from_leaf_intervals(leaf_intervals = [_ | _]) 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
tree: tree
}
end

Expand Down Expand Up @@ -164,7 +139,8 @@ defmodule ClosedIntervals do
when data: var
def get_interval(closed_intervals = %__MODULE__{}, value) do
case get_all_intervals(closed_intervals, value) do
[interval] ->
# FIXME Hack to work around https://github.com/evnu/closed_intervals/issues/16
[interval | _] ->
interval

[inf = {:"-inf", _} | _] ->
Expand All @@ -186,35 +162,44 @@ defmodule ClosedIntervals do
"""
@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

def get_all_intervals(%__MODULE__{tree: tree}, value) do
left_bound = Tree.tree(tree, :left_bound)
right_bound = Tree.tree(tree, :right_bound)

IO.inspect(
value: value,
tree: tree,
left_bound: left_bound,
right_bound: right_bound
)

cond do
order.(value, left_bound) ->
Compare.compare(value, left_bound) in [:lt, :eq] ->
neg_inf = [{:"-inf", Tree.tree(tree, :left_bound)}]

if eq.(value, left_bound) do
neg_inf ++ Tree.get_all_intervals(tree, value, eq, order)
if Compare.compare(value, left_bound) == :eq do
neg_inf ++ Tree.get_all_intervals(tree, value)
else
neg_inf
end
|> IO.inspect(label: "first cond")

order.(right_bound, value) ->
Compare.compare(right_bound, value) in [:lt, :eq] ->
pos_inf = [{Tree.tree(tree, :right_bound), :"+inf"}]

if eq.(value, right_bound) do
pos_inf ++ Tree.get_all_intervals(tree, value, eq, order)
if Compare.compare(right_bound, value) == :eq do
pos_inf ++ Tree.get_all_intervals(tree, value)
else
pos_inf
end
|> IO.inspect(label: "second cond")

true ->
Tree.get_all_intervals(tree, value, eq, order)
Tree.get_all_intervals(tree, value)
|> IO.inspect(label: "third cond")
end
|> List.flatten()
|> IO.inspect(label: "got all intervals")
end

@doc """
Expand Down
14 changes: 3 additions & 11 deletions lib/closed_intervals/tree.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ defmodule ClosedIntervals.Tree do
@doc """
Construct a tree from a sorted list of data.

See `ClosedIntervals.from/2`.
See `ClosedIntervals.from/1`.
"""
def construct([x, y]) do
tree(
Expand Down Expand Up @@ -134,16 +134,8 @@ defmodule ClosedIntervals.Tree do
@doc """
See `ClosedIntervals.get_all_intervals/2`.
"""
def get_all_intervals(tree, value, eq, order) do
get_all_intervals_by(tree, &mk_compare(value, &1, eq, order))
end

defp mk_compare(data1, data2, eq, order) do
cond do
eq.(data1, data2) -> :eq
order.(data1, data2) -> :lt
true -> :gt
end
def get_all_intervals(tree, value) do
get_all_intervals_by(tree, &Compare.compare(value, &1))
end

@doc """
Expand Down
25 changes: 25 additions & 0 deletions lib/compare.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defprotocol Compare do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can implement a protocol only once per type, correct? That might be a problem when trying to have closed intervals with different order in the same VM.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is actually something I use: I have a project with different ord implementations on the same type.

@spec compare(t, t) :: :eq | :lt | :gt when t: any()
def compare(lhs, rhs)
end

defimpl Compare, for: Integer do
def compare(lhs, rhs) when is_number(rhs) do
cond do
lhs < rhs -> :lt
lhs == rhs -> :eq
lhs > rhs -> :gt
end
# |> IO.inspect(label: "comparing integers #{lhs} #{rhs}")
end
end

defimpl Compare, for: Float do
def compare(lhs, rhs) when is_number(rhs) do
cond do
lhs < rhs -> :lt
lhs == rhs -> :eq
lhs > rhs -> :gt
end
end
end
9 changes: 9 additions & 0 deletions lib/indexed.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
defmodule Indexed do
defstruct [:idx, :data]
end

defimpl Compare, for: Indexed do
def compare(lhs, rhs) do
Compare.compare(lhs.idx, rhs.idx)
end
end
57 changes: 24 additions & 33 deletions test/closed_intervals_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ defmodule ClosedIntervalsTest do
assert_raise ArgumentError, fn -> from([1]) end

assert from([1, 2]) == %ClosedIntervals{
tree: Tree.tree(left: nil, right: nil, left_bound: 1, right_bound: 2, cut: nil),
order: &<=/2,
eq: nil
tree: Tree.tree(left: nil, right: nil, left_bound: 1, right_bound: 2, cut: nil)
}

assert from([1, 2, 3]) == %ClosedIntervals{
Expand All @@ -40,9 +38,7 @@ defmodule ClosedIntervalsTest do
left_bound: 1,
right_bound: 3,
cut: 2
),
order: &<=/2,
eq: nil
)
}

assert %{tree: Tree.tree()} = from([1, 2, 3, 4, 5])
Expand All @@ -66,49 +62,44 @@ defmodule ClosedIntervalsTest do

describe "custom order" do
test "get_interval" do
order = fn a, b -> a.idx < b.idx end

points =
[a, b, c, _d] = [
%{idx: 1, data: :a},
%{idx: 2, data: :b},
%{idx: 3, data: :c},
%{idx: 4, data: :d}
%Indexed{idx: 1, data: :a},
%Indexed{idx: 2, data: :b},
%Indexed{idx: 3, data: :c},
%Indexed{idx: 4, data: :d}
]

tree = from(points, order: order)
assert {a, b} == get_interval(tree, %{idx: 1})
assert {b, c} == get_interval(tree, %{idx: 2})
assert {a, b} == get_interval(tree, %{idx: 1.5})
tree = from(points)
assert {a, b} == get_interval(tree, %Indexed{idx: 1})
assert {b, c} == get_interval(tree, %Indexed{idx: 2})
assert {a, b} == get_interval(tree, %Indexed{idx: 1.5})
end
end

test "get_all_intervals" do
order = fn a, b -> a.idx <= b.idx end
eq = fn a, b -> a.idx == b.idx end

points =
[a, b, c, d, e] = [
%{idx: 1, data: :a},
%{idx: 2, data: :b},
%{idx: 3, data: :c},
%{idx: 3, data: :x},
%{idx: 4, data: :d}
%Indexed{idx: 1, data: :a},
%Indexed{idx: 2, data: :b},
%Indexed{idx: 3, data: :c},
%Indexed{idx: 3, data: :x},
%Indexed{idx: 4, data: :d}
]

tree = from(points, order: order, eq: eq)
tree = from(points)

assert [{:"-inf", a}] == get_all_intervals(tree, %{idx: 0})
assert [{:"-inf", a}, {a, b}] == get_all_intervals(tree, %{idx: 1})
assert [{a, b}, {b, c}] == get_all_intervals(tree, %{idx: 2})
assert [{a, b}] == get_all_intervals(tree, %{idx: 1.5})
assert [{:"-inf", a}] == get_all_intervals(tree, %Indexed{idx: 0})
assert [{:"-inf", a}, {a, b}] == get_all_intervals(tree, %Indexed{idx: 1})
assert [{a, b}, {b, c}] == get_all_intervals(tree, %Indexed{idx: 2})
assert [{a, b}] == get_all_intervals(tree, %Indexed{idx: 1.5})

assert [{e, :"+inf"}] == get_all_intervals(tree, %{idx: 5})
assert [{e, :"+inf"}, {d, e}] == get_all_intervals(tree, %{idx: 4})
assert [{e, :"+inf"}] == get_all_intervals(tree, %Indexed{idx: 5})
assert [{e, :"+inf"}, {d, e}] == get_all_intervals(tree, %Indexed{idx: 4})

# non-unique idx
assert [{b, c}, {c, d}, {d, e}] == get_all_intervals(tree, %{idx: 3})
assert [{d, e}] == get_all_intervals(tree, %{idx: 3.5})
assert [{b, c}, {c, d}, {d, e}] == get_all_intervals(tree, %Indexed{idx: 3})
assert [{d, e}] == get_all_intervals(tree, %Indexed{idx: 3.5})
end

property "can reconstruct ClosedIntervals with from_leaf_intervals/1,2" do
Expand Down