Skip to content

Commit 46a2268

Browse files
arflbitwalker
authored andcommitted
Fixes wrong interval bounds interpretation (#483)
Open bounds are excluded, closed bounds are included
1 parent 933588a commit 46a2268

File tree

2 files changed

+140
-85
lines changed

2 files changed

+140
-85
lines changed

lib/interval/interval.ex

Lines changed: 98 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ defmodule Timex.Interval do
66
77
iex> use Timex
88
...> Interval.new(from: ~D[2016-03-03], until: [days: 3])
9-
%#{__MODULE__}{from: ~N[2016-03-03 00:00:00], left_open: true, right_open: false, step: [days: 1], until: ~N[2016-03-06 00:00:00]}
9+
%#{__MODULE__}{from: ~N[2016-03-03 00:00:00], left_open: false, right_open: true, step: [days: 1], until: ~N[2016-03-06 00:00:00]}
1010
1111
iex> use Timex
1212
...> Interval.new(from: ~D[2016-03-03], until: ~N[2016-03-10 01:23:45])
13-
%Timex.Interval{from: ~N[2016-03-03 00:00:00], left_open: true, right_open: false, step: [days: 1], until: ~N[2016-03-10 01:23:45]}
13+
%Timex.Interval{from: ~N[2016-03-03 00:00:00], left_open: false, right_open: true, step: [days: 1], until: ~N[2016-03-10 01:23:45]}
1414
1515
iex> use Timex
1616
...> ~N[2016-03-04 12:34:56] in Interval.new(from: ~D[2016-03-03], until: [days: 3])
@@ -36,30 +36,31 @@ defmodule Timex.Interval do
3636
"""
3737
defexception message: "Unable to format interval!"
3838

39-
def exception([message: message]) do
39+
def exception(message: message) do
4040
%FormatError{message: message}
4141
end
4242
end
4343

4444
@type t :: %__MODULE__{}
45-
@type valid_step_unit :: :microseconds
46-
| :milliseconds
47-
| :seconds
48-
| :minutes
49-
| :hours
50-
| :days
51-
| :weeks
52-
| :months
53-
| :years
45+
@type valid_step_unit ::
46+
:microseconds
47+
| :milliseconds
48+
| :seconds
49+
| :minutes
50+
| :hours
51+
| :days
52+
| :weeks
53+
| :months
54+
| :years
5455
@type valid_interval_step :: {valid_step_unit, integer}
5556
@type valid_interval_steps :: [valid_interval_step]
5657

5758
@enforce_keys [:from, :until]
58-
defstruct from: nil,
59-
until: nil,
60-
left_open: false,
59+
defstruct from: nil,
60+
until: nil,
61+
left_open: false,
6162
right_open: true,
62-
step: [days: 1]
63+
step: [days: 1]
6364

6465
@valid_step_units [
6566
:microseconds,
@@ -76,7 +77,7 @@ defmodule Timex.Interval do
7677
@doc """
7778
Create a new Interval struct.
7879
79-
**Note:** By default intervals are left open, i.e. they include the `from` date/time,
80+
**Note:** By default intervals are left closed, i.e. they include the `from` date/time,
8081
and exclude the `until` date/time. Put another way, `from <= x < until`. This behavior
8182
matches that of other popular date/time libraries, such as Joda Time, as well as the SQL
8283
behavior of the `overlaps` keyword.
@@ -94,9 +95,9 @@ defmodule Timex.Interval do
9495
can see more detail on the theory [on Wikipedia](https://en.wikipedia.org/wiki/Interval_(mathematics)),
9596
but it can be more intuitively thought of like so:
9697
97-
- An "open" bound is inclusive, and a "closed" bound is exclusive
98-
- So a left-open interval includes the `from` value, and a left-closed interval does not.
99-
- Likewise, a right-open interval includes the `until` value, and a right-closed interval does not.
98+
- An "open" bound is exclusive, and a "closed" bound is inclusive
99+
- So a left-closed interval includes the `from` value, and a left-open interval does not.
100+
- Likewise, a right-closed interval includes the `until` value, and a right-open interval does not.
100101
- An open interval is both left and right open, conversely, a closed interval is both left and right closed.
101102
102103
**Note:** `until` shifts delegate to `Timex.shift`, so the options provided should match its valid options.
@@ -106,58 +107,70 @@ defmodule Timex.Interval do
106107
iex> use Timex
107108
...> Interval.new(from: ~D[2014-09-22], until: ~D[2014-09-29])
108109
...> |> Interval.format!("%Y-%m-%d", :strftime)
109-
"(2014-09-22, 2014-09-29]"
110+
"[2014-09-22, 2014-09-29)"
110111
111112
iex> use Timex
112113
...> Interval.new(from: ~D[2014-09-22], until: [days: 7])
113114
...> |> Interval.format!("%Y-%m-%d", :strftime)
114-
"(2014-09-22, 2014-09-29]"
115+
"[2014-09-22, 2014-09-29)"
115116
116117
iex> use Timex
117-
...> Interval.new(from: ~D[2014-09-22], until: [days: 7], left_open: false, right_open: true)
118+
...> Interval.new(from: ~D[2014-09-22], until: [days: 7], left_open: true, right_open: false)
118119
...> |> Interval.format!("%Y-%m-%d", :strftime)
119-
"[2014-09-22, 2014-09-29)"
120+
"(2014-09-22, 2014-09-29]"
120121
121122
iex> use Timex
122-
...> Interval.new(from: ~N[2014-09-22T15:30:00], until: [minutes: 20], left_open: false)
123+
...> Interval.new(from: ~N[2014-09-22T15:30:00], until: [minutes: 20], right_open: false)
123124
...> |> Interval.format!("%H:%M", :strftime)
124125
"[15:30, 15:50]"
125126
126127
"""
127-
@spec new(Keyword.t) :: t
128-
| {:error, :invalid_until}
129-
| {:error, :invalid_step}
128+
@spec new(Keyword.t()) ::
129+
t
130+
| {:error, :invalid_until}
131+
| {:error, :invalid_step}
130132
def new(options \\ []) do
131133
from =
132134
case Keyword.get(options, :from) do
133135
nil ->
134136
Timex.Protocol.NaiveDateTime.now()
137+
135138
%NaiveDateTime{} = d ->
136139
d
140+
137141
d ->
138142
Timex.to_naive_datetime(d)
139143
end
140-
left_open = Keyword.get(options, :left_open, true)
141-
right_open = Keyword.get(options, :right_open, false)
142-
step = Keyword.get(options, :step, [days: 1])
144+
145+
left_open = Keyword.get(options, :left_open, false)
146+
right_open = Keyword.get(options, :right_open, true)
147+
step = Keyword.get(options, :step, days: 1)
148+
143149
until =
144-
case Keyword.get(options, :until, [days: 1]) do
150+
case Keyword.get(options, :until, days: 1) do
145151
{:error, _} = err ->
146152
err
153+
147154
x when is_list(x) ->
148155
Timex.shift(from, x)
156+
149157
%NaiveDateTime{} = d ->
150158
d
159+
151160
d ->
152161
Timex.to_naive_datetime(d)
153162
end
163+
154164
cond do
155165
invalid_step?(step) ->
156166
{:error, :invalid_step}
167+
157168
invalid_until?(until) ->
158169
{:error, :invalid_until}
170+
159171
Timex.compare(until, from) <= 0 ->
160172
{:error, :invalid_until}
173+
161174
:else ->
162175
%__MODULE__{
163176
from: from,
@@ -173,9 +186,11 @@ defmodule Timex.Interval do
173186
defp invalid_until?(_), do: false
174187

175188
defp invalid_step?([]), do: false
189+
176190
defp invalid_step?([{unit, n} | rest]) when unit in @valid_step_units and is_integer(n) do
177191
invalid_step?(rest)
178192
end
193+
179194
defp invalid_step?(_), do: true
180195

181196
@doc """
@@ -199,8 +214,9 @@ defmodule Timex.Interval do
199214
200215
"""
201216
def duration(%__MODULE__{until: until, from: from}, :duration) do
202-
Timex.diff(until, from, :microseconds) |> Duration.from_microseconds
217+
Timex.diff(until, from, :microseconds) |> Duration.from_microseconds()
203218
end
219+
204220
def duration(%__MODULE__{until: until, from: from}, unit) do
205221
Timex.diff(until, from, unit)
206222
end
@@ -213,17 +229,22 @@ defmodule Timex.Interval do
213229
## Examples
214230
215231
iex> use Timex
216-
...> Interval.new(from: ~D[2014-09-22], until: [days: 3])
232+
...> Interval.new(from: ~D[2014-09-22], until: [days: 3], right_open: true)
217233
...> |> Interval.with_step([days: 1]) |> Enum.map(&Timex.format!(&1, "%Y-%m-%d", :strftime))
218234
["2014-09-22", "2014-09-23", "2014-09-24"]
219235
236+
iex> use Timex
237+
...> Interval.new(from: ~D[2014-09-22], until: [days: 3], right_open: false)
238+
...> |> Interval.with_step([days: 1]) |> Enum.map(&Timex.format!(&1, "%Y-%m-%d", :strftime))
239+
["2014-09-22", "2014-09-23", "2014-09-24", "2014-09-25"]
240+
220241
iex> use Timex
221242
...> Interval.new(from: ~D[2014-09-22], until: [days: 3], right_open: false)
222243
...> |> Interval.with_step([days: 2]) |> Enum.map(&Timex.format!(&1, "%Y-%m-%d", :strftime))
223244
["2014-09-22", "2014-09-24"]
224245
225246
iex> use Timex
226-
...> Interval.new(from: ~D[2014-09-22], until: [days: 3], right_open: true)
247+
...> Interval.new(from: ~D[2014-09-22], until: [days: 3], right_open: false)
227248
...> |> Interval.with_step([days: 3]) |> Enum.map(&Timex.format!(&1, "%Y-%m-%d", :strftime))
228249
["2014-09-22", "2014-09-25"]
229250
@@ -245,21 +266,23 @@ defmodule Timex.Interval do
245266
iex> use Timex
246267
...> Interval.new(from: ~D[2014-09-22], until: [days: 3])
247268
...> |> Interval.format!("%Y-%m-%d %H:%M", :strftime)
248-
"(2014-09-22 00:00, 2014-09-25 00:00]"
269+
"[2014-09-22 00:00, 2014-09-25 00:00)"
249270
250271
iex> use Timex
251272
...> Interval.new(from: ~D[2014-09-22], until: [days: 3])
252273
...> |> Interval.format!("%Y-%m-%d", :strftime)
253-
"(2014-09-22, 2014-09-25]"
274+
"[2014-09-22, 2014-09-25)"
254275
"""
255276
def format(%__MODULE__{} = interval, format, formatter \\ nil) do
256277
case Timex.format(interval.from, format, formatter) do
257278
{:error, _} = err ->
258279
err
280+
259281
{:ok, from} ->
260282
case Timex.format(interval.until, format, formatter) do
261283
{:error, _} = err ->
262284
err
285+
263286
{:ok, until} ->
264287
lopen = if interval.left_open, do: "(", else: "["
265288
ropen = if interval.right_open, do: ")", else: "]"
@@ -275,8 +298,9 @@ defmodule Timex.Interval do
275298
case format(interval, format, formatter) do
276299
{:ok, str} ->
277300
str
301+
278302
{:error, e} ->
279-
raise FormatError, message: "#{inspect e}"
303+
raise FormatError, message: "#{inspect(e)}"
280304
end
281305
end
282306

@@ -310,21 +334,27 @@ defmodule Timex.Interval do
310334
311335
## Examples
312336
313-
iex> #{__MODULE__}.overlaps?(#{__MODULE__}.new(from: ~D[2016-03-04], until: [days: 1]), #{__MODULE__}.new(from: ~D[2016-03-03], until: [days: 3]))
337+
iex> #{__MODULE__}.overlaps?(#{__MODULE__}.new(from: ~D[2016-03-04], until: [days: 1]), #{
338+
__MODULE__
339+
}.new(from: ~D[2016-03-03], until: [days: 3]))
314340
true
315341
316-
iex> #{__MODULE__}.overlaps?(#{__MODULE__}.new(from: ~D[2016-03-07], until: [days: 1]), #{__MODULE__}.new(from: ~D[2016-03-03], until: [days: 3]))
342+
iex> #{__MODULE__}.overlaps?(#{__MODULE__}.new(from: ~D[2016-03-07], until: [days: 1]), #{
343+
__MODULE__
344+
}.new(from: ~D[2016-03-03], until: [days: 3]))
317345
false
318346
"""
319-
@spec overlaps?(__MODULE__.t, __MODULE__.t) :: boolean()
347+
@spec overlaps?(__MODULE__.t(), __MODULE__.t()) :: boolean()
320348
def overlaps?(%__MODULE__{} = a, %__MODULE__{} = b) do
321349
cond do
322350
Timex.compare(max(a), min(b)) < 0 ->
323351
# a is completely before b
324352
false
353+
325354
Timex.compare(max(b), min(a)) < 0 ->
326355
# b is completely before a
327356
false
357+
328358
:else ->
329359
# a and b have overlapping elements
330360
true
@@ -334,11 +364,13 @@ defmodule Timex.Interval do
334364
@doc false
335365
def min(interval)
336366

337-
def min(%__MODULE__{from: from, left_open: true}), do: from
367+
def min(%__MODULE__{from: from, left_open: false}), do: from
368+
338369
def min(%__MODULE__{from: from, step: step}) do
339370
case Timex.shift(from, step) do
340371
{:error, {:unknown_shift_unit, unit}} ->
341-
raise FormatError, message: "Invalid step unit for interval: #{inspect unit}"
372+
raise FormatError, message: "Invalid step unit for interval: #{inspect(unit)}"
373+
342374
d ->
343375
d
344376
end
@@ -347,8 +379,8 @@ defmodule Timex.Interval do
347379
@doc false
348380
def max(interval)
349381

350-
def max(%__MODULE__{until: until, right_open: true}), do: until
351-
def max(%__MODULE__{until: until}), do: Timex.shift(until, [microseconds: -1])
382+
def max(%__MODULE__{until: until, right_open: false}), do: until
383+
def max(%__MODULE__{until: until}), do: Timex.shift(until, microseconds: -1)
352384

353385
defimpl Enumerable do
354386
alias Timex.Interval
@@ -357,9 +389,10 @@ defmodule Timex.Interval do
357389
do_reduce({Interval.min(i), until, open?, step}, acc, fun)
358390
end
359391

360-
defp do_reduce(_state, {:halt, acc}, _fun),
392+
defp do_reduce(_state, {:halt, acc}, _fun),
361393
do: {:halted, acc}
362-
defp do_reduce( state, {:suspend, acc}, fun),
394+
395+
defp do_reduce(state, {:suspend, acc}, fun),
363396
do: {:suspended, acc, &do_reduce(state, &1, fun)}
364397

365398
defp do_reduce({current_date, end_date, right_open, step}, {:cont, acc}, fun) do
@@ -368,42 +401,51 @@ defmodule Timex.Interval do
368401
else
369402
case Timex.shift(current_date, step) do
370403
{:error, {:unknown_shift_unit, unit}} ->
371-
raise FormatError, message: "Invalid step unit for interval: #{inspect unit}"
404+
raise FormatError, message: "Invalid step unit for interval: #{inspect(unit)}"
405+
372406
{:error, err} ->
373-
raise FormatError, message: "Failed to shift to next element in interval: #{inspect err}"
407+
raise FormatError,
408+
message: "Failed to shift to next element in interval: #{inspect(err)}"
409+
374410
next_date ->
375411
do_reduce({next_date, end_date, right_open, step}, fun.(current_date, acc), fun)
376412
end
377413
end
378414
end
379415

380-
defp has_interval_ended?(current_date, end_date, true),
381-
do: Timex.compare(current_date, end_date) > 0
382-
defp has_interval_ended?(current_date, end_date, false),
416+
defp has_interval_ended?(current_date, end_date, _right_open = true),
383417
do: Timex.compare(current_date, end_date) >= 0
384418

419+
defp has_interval_ended?(current_date, end_date, _right_open = false),
420+
do: Timex.compare(current_date, end_date) > 0
421+
385422
def member?(%Interval{} = interval, value) do
386423
result =
387424
cond do
388425
before?(interval, value) ->
389426
false
427+
390428
after?(interval, value) ->
391429
false
430+
392431
:else ->
393432
true
394433
end
434+
395435
{:ok, result}
396436
end
397437

398438
defp before?(%Interval{from: from, left_open: true}, value),
399-
do: Timex.compare(value, from) < 0
400-
defp before?(%Interval{from: from, left_open: false}, value),
401439
do: Timex.compare(value, from) <= 0
402440

441+
defp before?(%Interval{from: from, left_open: false}, value),
442+
do: Timex.compare(value, from) < 0
443+
403444
defp after?(%Interval{until: until, right_open: true}, value),
404-
do: Timex.compare(until, value) < 0
445+
do: Timex.compare(value, until) >= 0
446+
405447
defp after?(%Interval{until: until, right_open: false}, value),
406-
do: Timex.compare(until, value) <= 0
448+
do: Timex.compare(value, until) > 0
407449

408450
def count(_interval) do
409451
{:error, __MODULE__}

0 commit comments

Comments
 (0)