Skip to content

Commit

Permalink
add overlap functionality to Timex.Interval
Browse files Browse the repository at this point in the history
* Find the overlap Interval between two intervals.
* Return {:error, :no_overlap_interval} if intervals
  do not overlap, (including overlapping at one instant)
  • Loading branch information
aaronglasenapp committed Dec 10, 2018
1 parent 5107429 commit 1b5a171
Show file tree
Hide file tree
Showing 2 changed files with 168 additions and 0 deletions.
65 changes: 65 additions & 0 deletions lib/interval/interval.ex
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,71 @@ defmodule Timex.Interval do
def max(%__MODULE__{until: until, right_open: false}), do: until
def max(%__MODULE__{until: until}), do: Timex.shift(until, microseconds: -1)

@doc """
Returns an Interval representing the intersection between two intervals.
If the intervals do not overlap, reutrn {:error, :no_overlap_interval}.
If the intervals overlap at a single instant (regardless of open/closed
bounds), also return {:error, :no_overlap_interval}
"""
@spec overlap(__MODULE__.t(), __MODULE__.t()) :: __MODULE__.t() | {:error, :no_overlap_interval}
def overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
{from, left_open} = start_of_overlap(a, b)
{until, right_open} = end_of_overlap(a, b)

case new(from: from, until: until, left_open: left_open, right_open: right_open) do
{:error, _} -> {:error, :no_overlap_interval}
interval -> interval
end
end

@doc """
Take the later start time of the two overlapping intervals,
and the left_open value of that interval.
"""
defp start_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
cond do
Timex.equal?(a.from, b.from) -> {a.from, determine_bound(a.left_open, b.left_open)}
Timex.before?(a.from, b.from) -> {b.from, b.left_open}
true -> {a.from, a.left_open}
end
end

@doc """
Take the earlier end time of the 2 overlapping intervals,
and the right_open value of that interval.
"""
defp end_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
cond do
Timex.equal?(a.until, b.until) -> {a.until, determine_bound(a.right_open, b.right_open)}
Timex.before?(a.until, b.until) -> {a.until, a.right_open}
true -> {b.until, b.right_open}
end
end

@doc """
When calculating overlap, if two intervals share a `from` (or `until`), the overlap
interval should have a bound matching the "inner" interval (eg: if either inteval has
an open bound, the overlap should have an open bound).
## Example:
(----) <- Interval a
[-------] <- Interval b
(----) <- overlap interval (left_open: true)
Interval a and b have the same `from` value.
Interval a has `left_open: true`
Interval b has `left_open: false`
The resulting overlap interval should have `left_open: true`
To determine the appropriate bound, if both intervals have a 'closed' bound on the matching
`from` or `until`, then the resulting overlap interval should have a 'closed' bound. In all
other cases, the overlap interval should have an 'open' bound.
"""
defp determine_bound(false, false), do: false
defp determine_bound(_, _), do: true

defimpl Enumerable do
alias Timex.Interval

Expand Down
103 changes: 103 additions & 0 deletions test/interval_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,109 @@ defmodule IntervalTests do
end
end

describe "overlap" do
test "non-overlapping intervals" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
b = Interval.new(from: ~N[2017-01-02 15:30:00], until: ~N[2017-01-02 15:45:00])

assert {:error, _} = Interval.overlap(a, b)
end

test "non-overlapping back-to-back intervals" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: true)
b = Interval.new(from: ~N[2017-01-02 15:15:00], until: ~N[2017-01-02 15:30:00])

assert {:error, _} = Interval.overlap(a, b)
end

test "overlapping at single instant with closed bounds" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
b = Interval.new(from: ~N[2017-01-02 15:15:00], until: ~N[2017-01-02 15:30:00], left_open: false)

assert {:error, _} = Interval.overlap(a, b)
end

test "first subset of second" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:45:00])
b = Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00])

assert Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00]) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00]) == Interval.overlap(b, a)
end

test "partially overlapping" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00])

assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(b, a)
end

test "overlapping across hours" do
a = Interval.new(from: ~N[2017-01-02 14:50:00], until: ~N[2017-01-02 15:15:00])
b = Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:30:00])

assert Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(b, a)
end

test "overlapping across days" do
a = Interval.new(from: ~N[2017-01-15 23:40:00], until: ~N[2017-01-16 00:10:00])
b = Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:20:00])

assert Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:10:00]) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:10:00]) == Interval.overlap(b, a)
end

test "overlapping across months" do
a = Interval.new(from: ~N[2017-06-30 23:40:00], until: ~N[2017-07-01 00:10:00])
b = Interval.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:20:00])

assert Interval.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:10:00]) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:10:00]) == Interval.overlap(b, a)
end

test "overlapping across years" do
a = Interval.new(from: ~N[2016-12-31 23:30:00], until: ~N[2017-01-01 00:30:00])
b = Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00])

assert Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00]) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00]) == Interval.overlap(b, a)
end

test "shared from/until with different openness" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], left_open: true, right_open: false)
b = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], left_open: false, right_open: true)

assert Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: true, left_open: true) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: true, left_open: true) == Interval.overlap(b, a)
end

test "left_open: true, right_open: true" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: true)
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: true)

assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00], right_open: true, left_open: true) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00], right_open: true, left_open: true) == Interval.overlap(b, a)
end

test "left_open: true, right_open: false" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: true)

assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00], right_open: false, left_open: true) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00], right_open: false, left_open: true) == Interval.overlap(b, a)
end

test "left_open: false, right_open: false" do
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: false)

assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00], right_open: false, left_open: false) == Interval.overlap(a, b)
assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00], right_open: false, left_open: false) == Interval.overlap(b, a)
end
end

describe "contains?/2" do
test "non-overlapping" do
earlier = Interval.new(from: ~D[2018-01-01], until: ~D[2018-01-04])
Expand Down

0 comments on commit 1b5a171

Please sign in to comment.