Skip to content

Commit 09b30fc

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 09b30fc

File tree

2 files changed

+108
-0
lines changed

2 files changed

+108
-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: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,73 @@ 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+
%Duration{seconds: seconds} = Interval.overlap(a, b, :duration)
205+
assert seconds == 0
206+
assert Interval.overlap(a, b) == 0
207+
end
208+
209+
test "overlapping at single instant" do
210+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
211+
b = Interval.new(from: ~N[2017-01-02 15:15:00], until: ~N[2017-01-02 15:30:00])
212+
213+
assert Interval.overlap(a, b) == 0
214+
end
215+
216+
test "first subset of second" do
217+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:45:00])
218+
b = Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00])
219+
220+
assert Interval.overlap(a, b, :minutes) == 10
221+
%Duration{seconds: seconds} = Interval.overlap(a, b, :duration)
222+
assert seconds == 600
223+
end
224+
225+
test "partially overlapping" do
226+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
227+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00])
228+
229+
assert Interval.overlap(a, b) == 300
230+
assert Interval.overlap(a, b, :minutes) == 5
231+
end
232+
233+
test "overlapping across hours" do
234+
a = Interval.new(from: ~N[2017-01-02 14:50:00], until: ~N[2017-01-02 15:15:00])
235+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00])
236+
237+
assert Interval.overlap(a, b) == 300
238+
assert Interval.overlap(a, b, :minutes) == 5
239+
end
240+
241+
test "overlapping across days" do
242+
a = Interval.new(from: ~N[2017-01-15 23:40:00], until: ~N[2017-01-16 00:10:00])
243+
b = Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:20:00])
244+
245+
assert Interval.overlap(a, b) == 1200
246+
assert Interval.overlap(a, b, :minutes) == 20
247+
end
248+
249+
test "overlapping across months" do
250+
a = Interval.new(from: ~N[2017-06-30 23:40:00], until: ~N[2017-07-01 00:10:00])
251+
b = Interval.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:20:00])
252+
253+
assert Interval.overlap(a, b) == 1200
254+
assert Interval.overlap(a, b, :minutes) == 20
255+
end
256+
257+
test "overlapping across years" do
258+
a = Interval.new(from: ~N[2016-12-31 23:30:00], until: ~N[2017-01-01 00:30:00])
259+
b = Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00])
260+
261+
assert Interval.overlap(a, b) == 1800
262+
assert Interval.overlap(a, b, :minutes) == 30
263+
end
264+
end
265+
199266
describe "contains?/2" do
200267
test "non-overlapping" do
201268
earlier = Interval.new(from: ~D[2018-01-01], until: ~D[2018-01-04])

0 commit comments

Comments
 (0)