Skip to content
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## 2.0.0-alpha.2

### Added

- `partition/2` now also accepts an interval as it's second argument.

### Fixed

- `adjacent?/2` is now exposed on the interval module via `defdelegate` in the using macro.
Expand Down
71 changes: 52 additions & 19 deletions lib/interval.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1125,40 +1125,73 @@ defmodule Interval do
@doc """
Partition an interval `a` into 3 intervals using `x`:

- The interval with all points from `a` < `x`
- The interval with just `x`
- The interval with all points from `a` > `x`
- The interval with all points from `a` where `a` < `x`
- The interval with `x`
- The interval with all points from `a` where `a` > `x`

If `x` is not in `a` this function returns an empty list.

Note: Since 2.0.0, `x` can be a point _or_ an interval.
When `x` is a point, the middle interval will be an interval such that `[x,x]`.

If there are no points in a to the left of `x`, an empty interval is returned for the left side.
The same of course applies to the right side of `x`.

## Examples

iex> partition(new(module: Interval.IntegerInterval, left: 1, right: 5, bounds: "[]"), 3)
iex> partition(Interval.IntegerInterval.new(1, 5, "[]"), 3)
[
new(module: Interval.IntegerInterval, left: 1, right: 3, bounds: "[)"),
new(module: Interval.IntegerInterval, left: 3, right: 3, bounds: "[]"),
new(module: Interval.IntegerInterval, left: 3, right: 5, bounds: "(]")
Interval.IntegerInterval.new(1, 3, "[)"),
Interval.IntegerInterval.new(3, 3, "[]"),
Interval.IntegerInterval.new(3, 5, "(]")
]

iex> partition(new(module: Interval.IntegerInterval, left: 1, right: 5), -10)
iex> partition(Interval.IntegerInterval.new(1, 5), -10)
[]

iex> partition(Interval.IntegerInterval.new(1, 6), Interval.IntegerInterval.new(3, 4))
[
Interval.IntegerInterval.new(1, 3, "[)"),
Interval.IntegerInterval.new(3, 4, "[)"),
Interval.IntegerInterval.new(4, 6, "[)")
]

iex> partition(Interval.FloatInterval.new(1.0, 6.0), Interval.FloatInterval.new(1.0, 3.0, "[]"))
[
Interval.FloatInterval.new(1.0, 1.0, "[)"),
Interval.FloatInterval.new(1.0, 3.0, "[]"),
Interval.FloatInterval.new(3.0, 6.0, "()")
]
"""
@doc since: "0.1.4"
@spec partition(t(), point()) :: [t()] | []
def partition(%module{} = a, x) do
case contains_point?(a, x) do
false ->
[]
@spec partition(t(), point() | t()) :: [t()] | []
def partition(%module{} = a, %module{} = x) do
if contains?(a, x) and not empty?(x) do
# x might be unbounded, in which case the left/right side of x will be the empty interval.
left_of =
if unbounded_left?(x) do
new_empty(module)
else
from_endpoints(module, a.left, inverted_bound(x.left))
end

true ->
[
from_endpoints(module, a.left, {:exclusive, x}),
from_endpoints(module, {:inclusive, x}, {:inclusive, x}),
from_endpoints(module, {:exclusive, x}, a.right)
]
right_of =
if unbounded_right?(x) do
new_empty(module)
else
from_endpoints(module, inverted_bound(x.right), a.right)
end

[left_of, x, right_of]
else
[]
end
end

def partition(%module{} = a, x) do
partition(a, new(module: module, left: x, right: x, bounds: "[]"))
end

@doc """
Compare the left/right side of `a` with the left/right side of `b`

Expand Down
29 changes: 29 additions & 0 deletions test/continuous_interval_property_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,35 @@ defmodule ContinuousIntervalPropertyTest do
end
end

property "partition/2", %{impl: impl} do
check all(
a <- Helper.interval(impl),
b <- Helper.interval(impl)
) do
if Interval.contains?(a, b) and not Interval.empty?(b) do
[p1, p2, p3] = Interval.partition(a, b)
# the middle partition is b (when b is an interval)
assert p2 == b
# a contains p1,p2,p3 (since we sliced up a in 3 parts)
assert Interval.contains?(a, p1)
assert Interval.contains?(a, p3)
# the union of p1,p2,3 is a
a_prime = p1 |> Interval.union(p2) |> Interval.union(p3)
assert a_prime == a

if Interval.unbounded_left?(b) do
assert Interval.empty?(p1)
end

if Interval.unbounded_right?(b) do
assert Interval.empty?(p3)
end
else
assert [] == Interval.partition(a, b)
end
end
end

property "strictly_left_of?/2 and strictly_right_of?/2 relationship", %{impl: impl} do
check all(
a <- Helper.interval(impl, empty: false),
Expand Down
29 changes: 29 additions & 0 deletions test/discrete_interval_property_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,33 @@ defmodule DiscreteIntervalPropertyTest do
end
end
end

property "partition/2", %{impl: impl} do
check all(
a <- Helper.interval(impl),
b <- Helper.interval(impl)
) do
if Interval.contains?(a, b) and not Interval.empty?(b) do
[p1, p2, p3] = Interval.partition(a, b)
# the middle partition is b (when b is an interval)
assert p2 == b
# a contains p1,p2,p3 (since we sliced up a in 3 parts)
assert Interval.contains?(a, p1)
assert Interval.contains?(a, p3)
# the union of p1,p2,3 is a
a_prime = p1 |> Interval.union(p2) |> Interval.union(p3)
assert a_prime == a

if Interval.unbounded_left?(b) do
assert Interval.empty?(p1)
end

if Interval.unbounded_right?(b) do
assert Interval.empty?(p3)
end
else
assert [] == Interval.partition(a, b)
end
end
end
end
43 changes: 37 additions & 6 deletions test/interval_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ defmodule Interval.IntervalTest do
assert Interval.contains?(floati(1.0, 2.0, "[]"), floati(1.0, 2.0, "()"))
end

test "partition/2" do
test "partition/2 around a point" do
assert Interval.partition(inti(1, 4), 2) === [inti(1, 2), inti(2), inti(3)]
assert Interval.partition(inti(1, 4), 1) === [inti(0, 0), inti(1, 2), inti(2, 4)]
assert Interval.partition(inti(1, 4), 3) === [inti(1, 3), inti(3), inti(0, 0)]
Expand All @@ -507,16 +507,47 @@ defmodule Interval.IntervalTest do
floati(2.0, 2.0, "[]"),
floati(2.0, nil, "()")
]

assert Interval.partition(inti(1, 4), 4) === []
end

test "partition/2's result unioned together is it's input interval" do
test "partition/2 around an interval" do
a = inti(1, 4)
b = inti(2, 2, "[]")
assert Interval.partition(a, b) === [inti(1, 2), inti(2, 2, "[]"), inti(3)]

a = inti(1, 4)
b = inti(nil, nil)
assert Interval.partition(a, b) === []

b =
a
|> Interval.partition(2)
|> Enum.reduce(&Interval.union/2)
a = inti(nil, 5)
b = inti(2, 3, "[]")
assert Interval.partition(a, b) === [inti(nil, 2), inti(2, 3, "[]"), inti(3, 5, "()")]

a = inti(1, nil)
b = inti(2, 3, "[]")
assert Interval.partition(a, b) === [inti(1, 2, "[)"), inti(2, 3, "[]"), inti(3, nil, "()")]

a = inti(nil, nil)
b = inti(1, 2)
assert Interval.partition(a, b) === [inti(nil, 1), inti(1, 2), inti(2, nil)]

a = inti(nil, nil)
b = inti(nil, nil)
assert Interval.partition(a, b) === [inti(:empty), inti(nil, nil), inti(:empty)]

a = inti(nil, nil)
b = inti(nil, 2)
assert Interval.partition(a, b) === [inti(:empty), inti(nil, 2), inti(2, nil)]

a = inti(nil, nil)
b = inti(2, nil)
assert Interval.partition(a, b) === [inti(nil, 2), inti(2, nil), inti(:empty)]
end

test "partition/2's result unioned together is it's input interval" do
a = inti(1, 4)
b = a |> Interval.partition(2) |> Enum.reduce(&Interval.union/2)
assert a === b
end

Expand Down
19 changes: 19 additions & 0 deletions test/regression_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,23 @@ defmodule Interval.RegressionTest do
right: {:exclusive, Decimal.new("1")}
}
end

test "partition/2 regression 2025-03-11" do
a = %IntegerInterval{left: {:inclusive, 2}, right: :unbounded}
b = %IntegerInterval{left: {:inclusive, -1}, right: :unbounded}
assert [] = Interval.partition(a, b)

a = %IntegerInterval{left: :unbounded, right: :unbounded}
b = %IntegerInterval{left: :unbounded, right: :unbounded}

assert [
%IntegerInterval{left: :empty, right: :empty},
%IntegerInterval{left: :unbounded, right: :unbounded},
%IntegerInterval{left: :empty, right: :empty}
] = Interval.partition(a, b)

a = %Interval.IntegerInterval{left: {:inclusive, 1}, right: :unbounded}
b = %Interval.IntegerInterval{left: {:inclusive, -2}, right: :unbounded}
assert Interval.partition(a, b) == []
end
end