Skip to content

Commit bc21381

Browse files
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.
1 parent 5107429 commit bc21381

File tree

2 files changed

+75
-0
lines changed

2 files changed

+75
-0
lines changed

lib/interval/interval.ex

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,47 @@ defmodule Timex.Interval do
382382
def max(%__MODULE__{until: until, right_open: false}), do: until
383383
def max(%__MODULE__{until: until}), do: Timex.shift(until, microseconds: -1)
384384

385+
@doc """
386+
Returns the duration of the overlap between two intervals. defaults to seconds.
387+
Like the `duration` function, if unit is :duration, returns a Timex.Duration.t(),
388+
otherwise returns an integer in "unit".
389+
"""
390+
@spec overlap(__MODULE__.t(), __MODULE__.t(), atom()) :: Duration.t() | Integer.t()
391+
def overlap(%__MODULE__{} = a, %__MODULE__{} = b, unit \\ :seconds) do
392+
case {overlaps?(a, b), unit} do
393+
{false, :duration} ->
394+
Duration.from_seconds(0)
395+
396+
{false, _} ->
397+
0
398+
399+
{true, unit} ->
400+
new(from: start_of_overlap(a, b), until: end_of_overlap(a, b))
401+
|> duration(unit)
402+
end
403+
end
404+
405+
@doc "Take the later start time of the two overlapping intervals."
406+
defp start_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
407+
case Timex.before?(min(a), min(b)) do
408+
true -> min(b)
409+
false -> min(a)
410+
end
411+
end
412+
413+
@doc """
414+
Take the earlier end time of the 2 overlapping intervals.
415+
Force `right_open: false` because we don't want
416+
the microsecond offset that results from
417+
`right_open: true` when calculating overlap.
418+
"""
419+
defp end_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
420+
case Timex.before?(max(a), max(b)) do
421+
true -> max(Map.merge(a, %{right_open: false}))
422+
false -> max(Map.merge(b, %{right_open: false}))
423+
end
424+
end
425+
385426
defimpl Enumerable do
386427
alias Timex.Interval
387428

test/interval_test.exs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,40 @@ defmodule IntervalTests do
196196
end
197197
end
198198

199+
describe "overlap" do
200+
test "non-overlapping intervals" do
201+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
202+
b = Interval.new(from: ~N[2017-01-02 15:30:00], until: ~N[2017-01-02 15:45:00])
203+
204+
assert Interval.overlap(a, b) == 0
205+
end
206+
207+
test "overlapping at single instant" do
208+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
209+
b = Interval.new(from: ~N[2017-01-02 15:15:00], until: ~N[2017-01-02 15:30:00])
210+
211+
assert Interval.overlap(a, b) == 0
212+
end
213+
214+
test "partially overlapping" do
215+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
216+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00])
217+
218+
assert Interval.overlap(a, b) == 300
219+
assert Interval.overlap(a, b, :minutes) == 5
220+
221+
end
222+
223+
test "first subset of second" do
224+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:45:00])
225+
b = Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00])
226+
227+
assert Interval.overlap(a, b, :minutes) == 10
228+
%Duration{seconds: seconds} = Interval.overlap(a, b, :duration)
229+
assert seconds == 600
230+
end
231+
end
232+
199233
describe "contains?/2" do
200234
test "non-overlapping" do
201235
earlier = Interval.new(from: ~D[2018-01-01], until: ~D[2018-01-04])

0 commit comments

Comments
 (0)