From 0d335037c72a6a32429041d27e4bcc833881765f Mon Sep 17 00:00:00 2001 From: nseaSeb Date: Wed, 22 Jul 2026 12:19:47 +0200 Subject: [PATCH] Fix `diff/3` rounding for `:day`, `:hour` and `:minute` The `:day`, `:hour` and `:minute` clauses of `Time.diff/3`, `DateTime.diff/3` and `NaiveDateTime.diff/3` compute the difference in seconds first and then divide again, so the sub-second remainder is rounded twice. The intermediate conversion floors while the final `div/2` truncates, so the result follows neither rule: # elapsed is 23:59:59.999998, less than a full day Time.diff(~T[00:00:00.000001], ~T[23:59:59.999999], :hour) #=> -24, while the opposite direction returns 23 # elapsed is 86399.5s, and the docs promise incomplete days round to zero NaiveDateTime.diff(~N[2000-01-01 00:00:00.7], ~N[2000-01-02 00:00:00.2], :day) #=> -1 Compute the difference in microseconds and divide once instead. This keeps the documented truncation ("fractional results are not supported and are truncated", "it also rounds incomplete days to zero") in both directions. Positive results are unchanged, so the existing `DateTime.diff/3` "almost 7 days" test now gains its negative counterpart. Assisted-by: Claude Code:claude-opus-4-8 --- lib/elixir/lib/calendar/datetime.ex | 6 +++--- lib/elixir/lib/calendar/naive_datetime.ex | 6 +++--- lib/elixir/lib/calendar/time.ex | 4 ++-- lib/elixir/test/elixir/calendar/datetime_test.exs | 1 + lib/elixir/test/elixir/calendar/naive_datetime_test.exs | 6 ++++++ lib/elixir/test/elixir/calendar/time_test.exs | 6 ++++++ 6 files changed, 21 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index b8174743bc4..b75b15e6e10 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1576,15 +1576,15 @@ defmodule DateTime do def diff(datetime1, datetime2, unit \\ :second) def diff(datetime1, datetime2, :day) do - diff(datetime1, datetime2, :second) |> div(86400) + diff(datetime1, datetime2, :microsecond) |> div(86_400_000_000) end def diff(datetime1, datetime2, :hour) do - diff(datetime1, datetime2, :second) |> div(3600) + diff(datetime1, datetime2, :microsecond) |> div(3_600_000_000) end def diff(datetime1, datetime2, :minute) do - diff(datetime1, datetime2, :second) |> div(60) + diff(datetime1, datetime2, :microsecond) |> div(60_000_000) end def diff( diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 0c7af910141..2e037d4675c 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -541,15 +541,15 @@ defmodule NaiveDateTime do def diff(naive_datetime1, naive_datetime2, unit \\ :second) def diff(naive_datetime1, naive_datetime2, :day) do - diff(naive_datetime1, naive_datetime2, :second) |> div(86400) + diff(naive_datetime1, naive_datetime2, :microsecond) |> div(86_400_000_000) end def diff(naive_datetime1, naive_datetime2, :hour) do - diff(naive_datetime1, naive_datetime2, :second) |> div(3600) + diff(naive_datetime1, naive_datetime2, :microsecond) |> div(3_600_000_000) end def diff(naive_datetime1, naive_datetime2, :minute) do - diff(naive_datetime1, naive_datetime2, :second) |> div(60) + diff(naive_datetime1, naive_datetime2, :microsecond) |> div(60_000_000) end def diff( diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index fd2ef3a84ef..eaff3983638 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -901,11 +901,11 @@ defmodule Time do def diff(time1, time2, unit \\ :second) def diff(time1, time2, :hour) do - diff(time1, time2, :second) |> div(3600) + diff(time1, time2, :microsecond) |> div(3_600_000_000) end def diff(time1, time2, :minute) do - diff(time1, time2, :second) |> div(60) + diff(time1, time2, :microsecond) |> div(60_000_000) end def diff( diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index 4aa19afdc31..48605482f57 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -817,6 +817,7 @@ defmodule DateTimeTest do |> DateTime.add(-1, :microsecond) assert DateTime.diff(in_almost_7_days, datetime, :day) == 6 + assert DateTime.diff(datetime, in_almost_7_days, :day) == -6 end test "in microseconds" do diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 7c2f02f21aa..3644538cf35 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -238,6 +238,12 @@ defmodule NaiveDateTimeTest do :day ) == 0 + assert NaiveDateTime.diff( + ~N[2000-01-01 00:00:00.700000], + ~N[2000-01-02 00:00:00.200000], + :day + ) == 0 + assert NaiveDateTime.diff( ~N[2000-01-01 00:00:00.001200], ~N[2000-01-01 00:00:00.000700], diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 2818e755117..deb8c59f415 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -113,6 +113,12 @@ defmodule TimeTest do assert Time.diff(time1_holocene, time2, :second) == -17883 assert Time.diff(time1_holocene, time2, :millisecond) == -17_882_889 assert Time.diff(time1_holocene, time2, :microsecond) == -17_882_889_000 + + # Two times of day are always less than 24 hours apart, so neither + # direction can amount to a full day. + assert Time.diff(~T[23:59:59.999999], ~T[00:00:00.000001], :hour) == 23 + assert Time.diff(~T[00:00:00.000001], ~T[23:59:59.999999], :hour) == -23 + assert Time.diff(~T[00:00:00.000001], ~T[23:59:59.999999], :minute) == -1439 end test "truncate/2" do