From 5e6ba32d0b58f6f79559a3e2710be8383649b1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Mon, 14 Nov 2022 15:55:44 +0100 Subject: [PATCH 1/5] WIP --- lib/closed_intervals.ex | 72 +++++++++++++--------------------- lib/closed_intervals/tree.ex | 14 ++----- test/closed_intervals_test.exs | 12 ++---- 3 files changed, 35 insertions(+), 63 deletions(-) diff --git a/lib/closed_intervals.ex b/lib/closed_intervals.ex index 8b81f82..f9c3268 100644 --- a/lib/closed_intervals.ex +++ b/lib/closed_intervals.ex @@ -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"} @@ -55,7 +53,7 @@ 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() + iex> points |> from() |> leaf_intervals() [{%{idx: 1}, %{idx: 3}}, {%{idx: 3}, %{idx: 7}}] ## Arguments @@ -64,16 +62,12 @@ defmodule ClosedIntervals do * `: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) } _ -> @@ -81,21 +75,6 @@ defmodule ClosedIntervals do 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`. @@ -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 @@ -186,35 +161,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 """ diff --git a/lib/closed_intervals/tree.ex b/lib/closed_intervals/tree.ex index 7f110e4..f0e306e 100644 --- a/lib/closed_intervals/tree.ex +++ b/lib/closed_intervals/tree.ex @@ -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( @@ -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 """ diff --git a/test/closed_intervals_test.exs b/test/closed_intervals_test.exs index 36f1c90..027c03e 100644 --- a/test/closed_intervals_test.exs +++ b/test/closed_intervals_test.exs @@ -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{ @@ -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]) @@ -76,7 +72,7 @@ defmodule ClosedIntervalsTest do %{idx: 4, data: :d} ] - tree = from(points, order: order) + tree = from(points) 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}) @@ -96,7 +92,7 @@ defmodule ClosedIntervalsTest do %{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}) From f167fe8152c2e962d5688f3ea57592a5a5afdfba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Mon, 14 Nov 2022 16:10:32 +0100 Subject: [PATCH 2/5] WIP custom thing --- test/closed_intervals_test.exs | 45 +++++++++++++++------------------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/test/closed_intervals_test.exs b/test/closed_intervals_test.exs index 027c03e..0f15799 100644 --- a/test/closed_intervals_test.exs +++ b/test/closed_intervals_test.exs @@ -62,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) - 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}) + 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) - 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 From 2f1a6b9148a815847b5a52e6249e8f3eaab0b1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Mon, 14 Nov 2022 16:02:01 +0100 Subject: [PATCH 3/5] FIXME hack --- lib/closed_intervals.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/closed_intervals.ex b/lib/closed_intervals.ex index f9c3268..0e32cc0 100644 --- a/lib/closed_intervals.ex +++ b/lib/closed_intervals.ex @@ -139,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", _} | _] -> From 0cc1846f04911b11ef4c1a88050a8b621cf9a819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Mon, 14 Nov 2022 16:11:47 +0100 Subject: [PATCH 4/5] wip custom thing --- README.md | 20 ++++++++++---------- lib/closed_intervals.ex | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 1b75c92..d63993a 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/lib/closed_intervals.ex b/lib/closed_intervals.ex index 0e32cc0..d407e02 100644 --- a/lib/closed_intervals.ex +++ b/lib/closed_intervals.ex @@ -52,9 +52,9 @@ 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 = [%Indexed{idx: 3}, %Indexed{idx: 7}, %Indexed{idx: 1}] iex> points |> from() |> leaf_intervals() - [{%{idx: 1}, %{idx: 3}}, {%{idx: 3}, %{idx: 7}}] + [{%Indexed{idx: 1}, %Indexed{idx: 3}}, {%Indexed{idx: 3}, %Indexed{idx: 7}}] ## Arguments From 2485cb1a182dd0ab49e20ef19683fc9321e83c78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20B=C3=A4renz?= Date: Mon, 14 Nov 2022 16:19:37 +0100 Subject: [PATCH 5/5] WIP add missing files --- lib/compare.ex | 25 +++++++++++++++++++++++++ lib/indexed.ex | 9 +++++++++ 2 files changed, 34 insertions(+) create mode 100644 lib/compare.ex create mode 100644 lib/indexed.ex diff --git a/lib/compare.ex b/lib/compare.ex new file mode 100644 index 0000000..cec1052 --- /dev/null +++ b/lib/compare.ex @@ -0,0 +1,25 @@ +defprotocol Compare do + @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 diff --git a/lib/indexed.ex b/lib/indexed.ex new file mode 100644 index 0000000..074367e --- /dev/null +++ b/lib/indexed.ex @@ -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