diff --git a/lib/interval/interval.ex b/lib/interval/interval.ex index a8ead465..76a15482 100644 --- a/lib/interval/interval.ex +++ b/lib/interval/interval.ex @@ -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 diff --git a/test/interval_test.exs b/test/interval_test.exs index 1a1f8f71..1de4a765 100644 --- a/test/interval_test.exs +++ b/test/interval_test.exs @@ -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])