From 09b30fcafa78fcd07a29bfa75060c78413ed366b Mon Sep 17 00:00:00 2001 From: Aaron Glasenapp Date: Thu, 6 Dec 2018 16:41:43 -0700 Subject: [PATCH] add overlap functionality to Timex.Interval * determine the overlap duration between two intervals. * Can specify a valid unit for Durations. * Will return a Duration struct or integer depending on the unit provided (default to :seconds) * Will return 0 (or eqivalent Duration struct) for intervals that do not overlap. --- lib/interval/interval.ex | 41 ++++++++++++++++++++++++ test/interval_test.exs | 67 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/lib/interval/interval.ex b/lib/interval/interval.ex index a8ead465..c889ee08 100644 --- a/lib/interval/interval.ex +++ b/lib/interval/interval.ex @@ -382,6 +382,47 @@ 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 the duration of the overlap between two intervals. defaults to seconds. + Like the `duration` function, if unit is :duration, returns a Timex.Duration.t(), + otherwise returns an integer in "unit". + """ + @spec overlap(__MODULE__.t(), __MODULE__.t(), atom()) :: Duration.t() | Integer.t() + def overlap(%__MODULE__{} = a, %__MODULE__{} = b, unit \\ :seconds) do + case {overlaps?(a, b), unit} do + {false, :duration} -> + Duration.from_seconds(0) + + {false, _} -> + 0 + + {true, unit} -> + new(from: start_of_overlap(a, b), until: end_of_overlap(a, b)) + |> duration(unit) + end + end + + @doc "Take the later start time of the two overlapping intervals." + defp start_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do + case Timex.before?(min(a), min(b)) do + true -> min(b) + false -> min(a) + end + end + + @doc """ + Take the earlier end time of the 2 overlapping intervals. + Force `right_open: false` because we don't want + the microsecond offset that results from + `right_open: true` when calculating overlap. + """ + defp end_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do + case Timex.before?(max(a), max(b)) do + true -> max(Map.merge(a, %{right_open: false})) + false -> max(Map.merge(b, %{right_open: false})) + end + end + defimpl Enumerable do alias Timex.Interval diff --git a/test/interval_test.exs b/test/interval_test.exs index 1a1f8f71..36fdedd4 100644 --- a/test/interval_test.exs +++ b/test/interval_test.exs @@ -196,6 +196,73 @@ 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]) + + %Duration{seconds: seconds} = Interval.overlap(a, b, :duration) + assert seconds == 0 + assert Interval.overlap(a, b) == 0 + end + + test "overlapping at single instant" 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:15:00], until: ~N[2017-01-02 15:30:00]) + + assert Interval.overlap(a, b) == 0 + 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.overlap(a, b, :minutes) == 10 + %Duration{seconds: seconds} = Interval.overlap(a, b, :duration) + assert seconds == 600 + 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.overlap(a, b) == 300 + assert Interval.overlap(a, b, :minutes) == 5 + 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 15:10:00], until: ~N[2017-01-02 15:30:00]) + + assert Interval.overlap(a, b) == 300 + assert Interval.overlap(a, b, :minutes) == 5 + 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.overlap(a, b) == 1200 + assert Interval.overlap(a, b, :minutes) == 20 + 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.overlap(a, b) == 1200 + assert Interval.overlap(a, b, :minutes) == 20 + 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.overlap(a, b) == 1800 + assert Interval.overlap(a, b, :minutes) == 30 + end + end + describe "contains?/2" do test "non-overlapping" do earlier = Interval.new(from: ~D[2018-01-01], until: ~D[2018-01-04])