Skip to content

Commit e10c8fa

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 e10c8fa

File tree

2 files changed

+168
-0
lines changed

2 files changed

+168
-0
lines changed

lib/interval/interval.ex

+65
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,71 @@ 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 representing the intersection between two intervals.
387+
If the intervals do not overlap, return {:error, :no_overlap_interval}.
388+
If the intervals overlap at a single instant (regardless of open/closed
389+
bounds), also return {:error, :no_overlap_interval}
390+
"""
391+
@spec overlap(__MODULE__.t(), __MODULE__.t()) :: __MODULE__.t() | {:error, :no_overlap_interval}
392+
def overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
393+
{from, left_open} = start_of_overlap(a, b)
394+
{until, right_open} = end_of_overlap(a, b)
395+
396+
case new(from: from, until: until, left_open: left_open, right_open: right_open) do
397+
{:error, _} -> {:error, :no_overlap_interval}
398+
interval -> interval
399+
end
400+
end
401+
402+
@doc """
403+
Take the later start time of the two overlapping intervals,
404+
and the left_open value of that interval.
405+
"""
406+
defp start_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
407+
cond do
408+
Timex.equal?(a.from, b.from) -> {a.from, determine_bound(a.left_open, b.left_open)}
409+
Timex.before?(a.from, b.from) -> {b.from, b.left_open}
410+
true -> {a.from, a.left_open}
411+
end
412+
end
413+
414+
@doc """
415+
Take the earlier end time of the 2 overlapping intervals,
416+
and the right_open value of that interval.
417+
"""
418+
defp end_of_overlap(%__MODULE__{} = a, %__MODULE__{} = b) do
419+
cond do
420+
Timex.equal?(a.until, b.until) -> {a.until, determine_bound(a.right_open, b.right_open)}
421+
Timex.before?(a.until, b.until) -> {a.until, a.right_open}
422+
true -> {b.until, b.right_open}
423+
end
424+
end
425+
426+
@doc """
427+
When calculating overlap, if two intervals share a `from` (or `until`), the overlap
428+
interval should have a bound matching the "inner" interval (eg: if either interval has
429+
an open bound, the overlap should have an open bound).
430+
431+
## Example:
432+
433+
[----) <- Interval a
434+
(-------] <- Interval b
435+
(----) <- overlap interval (left_open: true)
436+
437+
Interval a and b have the same `from` value.
438+
Interval a has `left_open: false
439+
Interval b has `left_open: true`
440+
441+
The resulting overlap interval should have `left_open: true`
442+
443+
To determine the appropriate bound, if both intervals have a 'closed' bound on the matching
444+
`from` or `until`, then the resulting overlap interval should have a 'closed' bound. In all
445+
other cases, the overlap interval should have an 'open' bound.
446+
"""
447+
defp determine_bound(false, false), do: false
448+
defp determine_bound(_, _), do: true
449+
385450
defimpl Enumerable do
386451
alias Timex.Interval
387452

test/interval_test.exs

+103
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,109 @@ 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+
assert Interval.new(from: ~N[2017-01-02 15:20:00], until: ~N[2017-01-02 15:30:00]) == Interval.overlap(b, a)
227+
end
228+
229+
test "partially overlapping" do
230+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00])
231+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00])
232+
233+
assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(a, b)
234+
assert Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(b, a)
235+
end
236+
237+
test "overlapping across hours" do
238+
a = Interval.new(from: ~N[2017-01-02 14:50:00], until: ~N[2017-01-02 15:15:00])
239+
b = Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:30:00])
240+
241+
assert Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(a, b)
242+
assert Interval.new(from: ~N[2017-01-02 14:55:00], until: ~N[2017-01-02 15:15:00]) == Interval.overlap(b, a)
243+
end
244+
245+
test "overlapping across days" do
246+
a = Interval.new(from: ~N[2017-01-15 23:40:00], until: ~N[2017-01-16 00:10:00])
247+
b = Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:20:00])
248+
249+
assert Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:10:00]) == Interval.overlap(a, b)
250+
assert Interval.new(from: ~N[2017-01-15 23:50:00], until: ~N[2017-01-16 00:10:00]) == Interval.overlap(b, a)
251+
end
252+
253+
test "overlapping across months" do
254+
a = Interval.new(from: ~N[2017-06-30 23:40:00], until: ~N[2017-07-01 00:10:00])
255+
b = Interval.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:20:00])
256+
257+
assert Interval.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:10:00]) == Interval.overlap(a, b)
258+
assert Interval.new(from: ~N[2017-06-30 23:50:00], until: ~N[2017-07-01 00:10:00]) == Interval.overlap(b, a)
259+
end
260+
261+
test "overlapping across years" do
262+
a = Interval.new(from: ~N[2016-12-31 23:30:00], until: ~N[2017-01-01 00:30:00])
263+
b = Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00])
264+
265+
assert Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00]) == Interval.overlap(a, b)
266+
assert Interval.new(from: ~N[2016-12-31 23:45:00], until: ~N[2017-01-01 00:15:00]) == Interval.overlap(b, a)
267+
end
268+
269+
test "shared from/until with different openness" do
270+
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)
271+
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)
272+
273+
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)
274+
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(b, a)
275+
end
276+
277+
test "left_open: true, right_open: true" do
278+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: true)
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: true, left_open: true) == Interval.overlap(a, b)
282+
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(b, a)
283+
end
284+
285+
test "left_open: true, right_open: false" do
286+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
287+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: true)
288+
289+
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)
290+
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(b, a)
291+
end
292+
293+
test "left_open: false, right_open: false" do
294+
a = Interval.new(from: ~N[2017-01-02 15:00:00], until: ~N[2017-01-02 15:15:00], right_open: false)
295+
b = Interval.new(from: ~N[2017-01-02 15:10:00], until: ~N[2017-01-02 15:30:00], left_open: false)
296+
297+
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)
298+
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(b, a)
299+
end
300+
end
301+
199302
describe "contains?/2" do
200303
test "non-overlapping" do
201304
earlier = Interval.new(from: ~D[2018-01-01], until: ~D[2018-01-04])

0 commit comments

Comments
 (0)