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 8764c92
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 0 deletions.
36 changes: 36 additions & 0 deletions lib/interval/interval.ex
Expand Up @@ -382,6 +382,42 @@ 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 of the overlap between two intervals.
"""
@spec overlap(__MODULE__.t(), __MODULE__.t()) :: 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
case Timex.before?(a.from, b.from) do
true -> {b.from, b.left_open}
false -> {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
case Timex.before?(a.until, b.until) do
true -> {a.until, a.right_open}
false -> {b.until, b.right_open}
end
end

defimpl Enumerable do
alias Timex.Interval

Expand Down
93 changes: 93 additions & 0 deletions test/interval_test.exs
Expand Up @@ -196,6 +196,99 @@ 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)
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)
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)
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)
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)
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)
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)
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)
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)
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)
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 8764c92

Please sign in to comment.