Skip to content

Commit 8764c92

Browse files
add overlap functionality to Timex.Interval
* Find the overlap Interval between two intervals. * Return {:error, :no_overlap_interval} if intervals do not overlap, (including overlapping at one instant)
1 parent 5107429 commit 8764c92

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

lib/interval/interval.ex

+36
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,42 @@ 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 an Interval of the overlap between two intervals.
387+
"""
388+
@spec overlap(__MODULE__.t(), __MODULE__.t()) :: t() | {:error, :no_overlap_interval}
389+
def overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
390+
{from, left_open} = start_of_overlap(a, b)
391+
{until, right_open} = end_of_overlap(a, b)
392+
393+
case new(from: from, until: until, left_open: left_open, right_open: right_open) do
394+
{:error, _} -> {:error, :no_overlap_interval}
395+
interval -> interval
396+
end
397+
end
398+
399+
@doc """
400+
Take the later start time of the two overlapping intervals,
401+
and the left_open value of that interval.
402+
"""
403+
defp start_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
404+
case Timex.before?(a.from, b.from) do
405+
true -> {b.from, b.left_open}
406+
false -> {a.from, a.left_open}
407+
end
408+
end
409+
410+
@doc """
411+
Take the earlier end time of the 2 overlapping intervals,
412+
and the right_open value of that interval.
413+
"""
414+
defp end_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
415+
case Timex.before?(a.until, b.until) do
416+
true -> {a.until, a.right_open}
417+
false -> {b.until, b.right_open}
418+
end
419+
end
420+
385421
defimpl Enumerable do
386422
alias Timex.Interval
387423

test/interval_test.exs

+93
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,99 @@ 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 {:error, _} = Interval.overlap(a, b)
205+
end
206+
207+
test "non-overlapping back-to-back intervals" do
208+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: true)
209+
b = Interval.new(from: ~N[2017-01-02 15:15:00], until: ~N[2017-01-02 15:30:00])
210+
211+
assert {:error, _} = Interval.overlap(a, b)
212+
end
213+
214+
test "overlapping at single instant with closed bounds" do
215+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
216+
b = Interval.new(from: ~N[2017-01-02 15:15:00], until: ~N[2017-01-02 15:30:00], left_open: false)
217+
218+
assert {:error, _} = Interval.overlap(a, b)
219+
end
220+
221+
test "first subset of second" do
222+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:45:00])
223+
b = Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00])
224+
225+
assert Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00]) == Interval.overlap(a, b)
226+
end
227+
228+
test "partially overlapping" do
229+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
230+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00])
231+
232+
assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(a, b)
233+
end
234+
235+
test "overlapping across hours" do
236+
a = Interval.new(from: ~N[2017-01-02 14:50:00], until: ~N[2017-01-02 15:15:00])
237+
b = Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:30:00])
238+
239+
assert Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(a, b)
240+
end
241+
242+
test "overlapping across days" do
243+
a = Interval.new(from: ~N[2017-01-15 23:40:00], until: ~N[2017-01-16 00:10:00])
244+
b = Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:20:00])
245+
246+
assert Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:10:00]) == Interval.overlap(a, b)
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.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:10:00]) == Interval.overlap(a, b)
254+
end
255+
256+
test "overlapping across years" do
257+
a = Interval.new(from: ~N[2016-12-31 23:30:00], until: ~N[2017-01-01 00:30:00])
258+
b = Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00])
259+
260+
assert Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00]) == Interval.overlap(a, b)
261+
end
262+
263+
test "shared from/until with different openness" do
264+
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)
265+
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)
266+
267+
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)
268+
end
269+
270+
test "left_open: true, right_open: true" do
271+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: true)
272+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: true)
273+
274+
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)
275+
end
276+
277+
test "left_open: true, right_open: false" do
278+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
279+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: true)
280+
281+
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)
282+
end
283+
284+
test "left_open: false, right_open: false" do
285+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
286+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: false)
287+
288+
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)
289+
end
290+
end
291+
199292
describe "contains?/2" do
200293
test "non-overlapping" do
201294
earlier = Interval.new(from: ~D[2018-01-01], until: ~D[2018-01-04])

0 commit comments

Comments
 (0)