Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions lib/interval/interval.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
67 changes: 67 additions & 0 deletions test/interval_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down