Skip to content

Commit

Permalink
feat: Add :cycled option to Timex.between?/4 (bitwalker#726)
Browse files Browse the repository at this point in the history
Co-authored-by: Paul Schoenfelder <[email protected]>
  • Loading branch information
jrogov and bitwalker authored Jan 25, 2023
1 parent 25dfac9 commit 59f5416
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 81 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
### Added/Changed

- Changed `Timex.Duration.Parse` to be 2x faster
- Added `cycled` option for `Timex.between?/4` to support time-range checks that pass through midnight
- Add Croatian translation

### Fixed
Expand Down
43 changes: 37 additions & 6 deletions lib/timex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -904,12 +904,19 @@ defmodule Timex do
`inclusive: true` option.
To set just one of the bounds as inclusive, use the
`inclusive: :start` or `inclusive: :end` option.
Also, by default, for `Time.t`, if `start` and `end`
are on different sides of midnight,
it doesn't count as a continous period. Hence, `23:00 < 00:00, 01:00` would return `false`.
To use cycled time, use option `cycled: true`.
"""

@type between_options :: [
inclusive:
boolean
| :start
| :end
| :end,
cycled: boolean
]
@spec between?(
Time.t() | Comparable.comparable(),
Expand All @@ -926,17 +933,41 @@ defmodule Timex do
_ -> {1, 1}
end

in_bounds?(compare(a, start), compare(ending, a), start_test, ending_test)
passes_midnight? =
case Keyword.get(options, :cycled, false) do
true ->
case {a, start, ending} do
{%Time{}, %Time{}, %Time{}} ->
between?(ending, ~T[00:00:00], start, inclusive: :start)

_ ->
raise ArgumentError,
message:
"cycled: true was passed, but one of arguments is not Time.t: #{inspect({a, start, ending})}"
end

false ->
false
end

in_bounds?(compare(a, start), compare(ending, a), start_test, ending_test, passes_midnight?)
end

defp in_bounds?({:error, reason}, _, _, _),
defp in_bounds?({:error, reason}, _, _, _, _),
do: raise(ArgumentError, message: "#{inspect(reason)}")

defp in_bounds?(_, {:error, reason}, _, _),
defp in_bounds?(_, {:error, reason}, _, _, _),
do: raise(ArgumentError, message: "#{inspect(reason)}")

defp in_bounds?(start_comparison, ending_comparison, start_test, ending_test) do
start_comparison >= start_test && ending_comparison >= ending_test
defp in_bounds?(start_comparison, ending_comparison, start_test, ending_test, passes_midnight?) do
after_start? = start_comparison >= start_test
before_end? = ending_comparison >= ending_test

if passes_midnight? do
after_start? || before_end?
else
after_start? && before_end?
end
end

@doc """
Expand Down
176 changes: 101 additions & 75 deletions test/timex_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -355,112 +355,138 @@ defmodule TimexTests do
refute Timex.after?(~T[09:00:00], ~T[12:00:00])
end

test "between?" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})
describe "between?" do
test "w/o options" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})

assert true == Timex.between?(date2, date1, date3)
assert true == Timex.between?(date2, date1, date3)

assert false == Timex.between?(date1, date2, date3)
assert false == Timex.between?(date3, date1, date2)
assert false == Timex.between?(date1, date1, date3)
assert false == Timex.between?(date3, date1, date3)
assert false == Timex.between?(date1, date2, date3)
assert false == Timex.between?(date3, date1, date2)
assert false == Timex.between?(date1, date1, date3)
assert false == Timex.between?(date3, date1, date3)

assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}})
end
assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}})
end

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}})
end
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}})
end

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {})
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {})
end

assert Timex.between?(~T[12:00:00], ~T[09:00:00], ~T[17:00:00])
refute Timex.between?(~T[07:00:00], ~T[09:00:00], ~T[17:00:00])
end

assert Timex.between?(~T[12:00:00], ~T[09:00:00], ~T[17:00:00])
refute Timex.between?(~T[07:00:00], ~T[09:00:00], ~T[17:00:00])
end
test "inclusive: true" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})

test "between? inclusive" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})
options = [inclusive: true]

options = [inclusive: true]
assert true == Timex.between?(date2, date1, date3, options)
assert true == Timex.between?(date1, date1, date3, options)
assert true == Timex.between?(date3, date1, date3, options)

assert true == Timex.between?(date2, date1, date3, options)
assert true == Timex.between?(date1, date1, date3, options)
assert true == Timex.between?(date3, date1, date3, options)
assert false == Timex.between?(date1, date2, date3, options)
assert false == Timex.between?(date3, date1, date2, options)

assert false == Timex.between?(date1, date2, date3, options)
assert false == Timex.between?(date3, date1, date2, options)
assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, options)
end

assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, options)
end
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}}, options)
end

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}}, options)
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {}, options)
end
end

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {}, options)
end
end
test "inclusive: :start" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})

options = [inclusive: :start]

test "between? inclusive_start" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})
assert true == Timex.between?(date2, date1, date3, options)
assert true == Timex.between?(date1, date1, date3, options)
assert false == Timex.between?(date3, date1, date3, options)

options = [inclusive: :start]
assert false == Timex.between?(date1, date2, date3, options)
assert false == Timex.between?(date3, date1, date2, options)

assert true == Timex.between?(date2, date1, date3, options)
assert true == Timex.between?(date1, date1, date3, options)
assert false == Timex.between?(date3, date1, date3, options)
assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, options)
end

assert false == Timex.between?(date1, date2, date3, options)
assert false == Timex.between?(date3, date1, date2, options)
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}}, options)
end

assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, options)
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {}, options)
end
end

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}}, options)
end
test "inclusive: :end" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {}, options)
end
end
options = [inclusive: :end]

test "between? inclusive_end" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})
assert true == Timex.between?(date2, date1, date3, options)
assert false == Timex.between?(date1, date1, date3, options)
assert true == Timex.between?(date3, date1, date3, options)

options = [inclusive: :end]
assert false == Timex.between?(date1, date2, date3, options)
assert false == Timex.between?(date3, date1, date2, options)

assert true == Timex.between?(date2, date1, date3, options)
assert false == Timex.between?(date1, date1, date3, options)
assert true == Timex.between?(date3, date1, date3, options)
assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, options)
end

assert false == Timex.between?(date1, date2, date3, options)
assert false == Timex.between?(date3, date1, date2, options)
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}}, options)
end

assert_raise ArgumentError, fn ->
Timex.between?({}, {{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, options)
assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {}, options)
end
end

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {}, {{2013, 1, 1}, {1, 1, 2}}, options)
end
test "cycled: true" do
date1 = Timex.to_datetime({{2013, 1, 1}, {0, 0, 0}})
date2 = Timex.to_datetime({{2013, 1, 5}, {0, 0, 0}})
date3 = Timex.to_datetime({{2013, 1, 9}, {0, 0, 0}})

assert_raise ArgumentError, fn ->
Timex.between?({{2013, 1, 1}, {1, 1, 2}}, {{2013, 1, 1}, {1, 1, 2}}, {}, options)
assert_raise ArgumentError, fn ->
assert true == Timex.between?(date2, date1, date3, cycled: true)
end

assert Timex.between?(~T[01:00:00], ~T[00:00:00], ~T[02:00:00], cycled: true)
refute Timex.between?(~T[01:00:00], ~T[23:00:00], ~T[02:00:00], cycled: false)
assert Timex.between?(~T[01:00:00], ~T[23:00:00], ~T[02:00:00], cycled: true)

assert Timex.between?(~T[00:00:00], ~T[23:00:00], ~T[00:00:00],
cycled: true,
inclusive: :end
)

assert Timex.between?(~T[23:00:00], ~T[23:00:00], ~T[00:00:00],
cycled: true,
inclusive: :start
)
end
end

Expand Down

0 comments on commit 59f5416

Please sign in to comment.