Explicitly handle reverse order DateTimeRange (#5336)

We're seeing warnings as follows:
```
 (plausible 0.0.1) lib/plausible/stats/legacy/legacy_query_builder.ex:32: Plausible.Stats.Legacy.QueryBuilder.from/4 |
 |  (plausible 0.0.1) lib/plausible/stats/query.ex:143: Plausible.Stats.Query.put_imported_opts/2 |
 |  (plausible 0.0.1) lib/plausible/stats/query.ex:163: Plausible.Stats.Query.get_imports_in_range/2 |
 |  (plausible 0.0.1) lib/plausible/imported.ex:98: Plausible.Imported.completed_imports_in_query_range/2 |
 |  (plausible 0.0.1) lib/plausible/stats/query.ex:65: Plausible.Stats.Query.date_range/2 |
 |  (elixir 1.17.3) lib/calendar/date.ex:111: Date.range/2 |
 |
warning: a negative range was inferred for Date.range/2, call Date.range/3 instead with -1 as third argument
```

As well as some Stats API queries with the date time range reversed.

This PR makes it explicit we support passing the range in reverse and
handles that without warnings. Along the way added some tests.

Ref: https://3.basecamp.com/5308029/buckets/36789884/card_tables/cards/8415153184
This commit is contained in:
Karl-Aksel Puulmann 2025-04-16 12:52:06 +03:00 committed by GitHub
parent 472b7bfb1d
commit 2fe154b169
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 1 deletions

View File

@ -42,7 +42,11 @@ defmodule Plausible.Stats.DateTimeRange do
first = DateTime.truncate(first, :second)
last = DateTime.truncate(last, :second)
%__MODULE__{first: first, last: last}
if DateTime.before?(first, last) do
%__MODULE__{first: first, last: last}
else
%__MODULE__{first: last, last: first}
end
end
def to_timezone(%__MODULE__{first: first, last: last}, timezone) do

View File

@ -0,0 +1,94 @@
defmodule Plausible.Stats.DateTimeRangeTest do
use Plausible.DataCase, async: true
alias Plausible.Stats.DateTimeRange
describe "new!/2" do
test "creates a range when datetimes are in correct order" do
first = DateTime.new!(~D[2023-01-01], ~T[10:00:00], "UTC")
last = DateTime.new!(~D[2023-01-02], ~T[10:00:00], "UTC")
range = DateTimeRange.new!(first, last)
assert range.first == first
assert range.last == last
end
test "swaps datetimes when in reverse order" do
first = DateTime.new!(~D[2023-02-01], ~T[10:00:00], "UTC")
last = DateTime.new!(~D[2023-01-01], ~T[10:00:00], "UTC")
range = DateTimeRange.new!(first, last)
assert range.first == last
assert range.last == first
end
test "truncates microseconds" do
first = DateTime.new!(~D[2023-01-01], ~T[10:00:00.123], "UTC")
last = DateTime.new!(~D[2023-01-02], ~T[10:00:00.456], "UTC")
range = DateTimeRange.new!(first, last)
assert range.first == DateTime.truncate(first, :second)
assert range.last == DateTime.truncate(last, :second)
end
end
describe "new!/3 with dates and timezone" do
test "creates range with start and end of day" do
first_date = ~D[2023-01-01]
last_date = ~D[2023-01-02]
range = DateTimeRange.new!(first_date, last_date, "UTC")
assert range.first == DateTime.new!(first_date, ~T[00:00:00], "UTC")
assert range.last == DateTime.new!(last_date, ~T[23:59:59], "UTC")
end
test "handles timezone gaps (spring forward)" do
# https://stackoverflow.com/questions/18489927/a-day-without-midnight
range = DateTimeRange.new!(~D[2020-03-29], ~D[2020-03-29], "Asia/Beirut")
assert range.first == DateTime.new!(~D[2020-03-29], ~T[01:00:00], "Asia/Beirut")
assert range.last == DateTime.new!(~D[2020-03-29], ~T[23:59:59], "Asia/Beirut")
end
end
describe "to_timezone/2" do
test "converts range to specified timezone" do
first = DateTime.new!(~D[2023-01-01], ~T[10:00:00], "UTC")
last = DateTime.new!(~D[2023-01-02], ~T[10:00:00], "UTC")
range = DateTimeRange.new!(first, last)
converted = DateTimeRange.to_timezone(range, "America/New_York")
assert converted.first == DateTime.shift_zone!(first, "America/New_York")
assert converted.last == DateTime.shift_zone!(last, "America/New_York")
end
end
describe "to_date_range/2" do
test "converts datetime range to date range" do
first = DateTime.new!(~D[2023-01-01], ~T[10:00:00], "UTC")
last = DateTime.new!(~D[2023-01-05], ~T[10:00:00], "UTC")
range = DateTimeRange.new!(first, last)
date_range = DateTimeRange.to_date_range(range, "UTC")
assert date_range.first == ~D[2023-01-01]
assert date_range.last == ~D[2023-01-05]
end
test "handles timezone conversions that cross date boundaries" do
first = DateTime.new!(~D[2023-01-01], ~T[23:00:00], "UTC")
last = DateTime.new!(~D[2023-01-02], ~T[04:59:59], "UTC")
range = DateTimeRange.new!(first, last)
date_range = DateTimeRange.to_date_range(range, "America/New_York")
assert date_range.first == ~D[2023-01-01]
assert date_range.last == ~D[2023-01-01]
end
end
end