From aaeb55c1c9645201947cb801c2c85a9f9261b2b7 Mon Sep 17 00:00:00 2001 From: nseaSeb Date: Fri, 10 Jul 2026 16:55:14 +0200 Subject: [PATCH] Fix `NaiveDateTime.diff/3` over-counting incomplete units `NaiveDateTime.diff/3` truncated each operand to the requested unit and only then subtracted, i.e. it computed `floor(t1) - floor(t2)` instead of `floor(t1 - t2)`. When the subtrahend's sub-second fraction exceeded the minuend's, this rounded the result *up*, contradicting the documented guarantee that "Fractional results are [...] truncated" and the example that it "rounds incomplete days to zero". For instance an elapsed span of 86399.5s (less than one day) returned `1` for `:day`, and a 0.5s span returned `1` for `:second`. The sibling `DateTime.diff/3`, which carries the identical doc line, returned `0` for the same wall-clock instants because it computes the difference at the `:microsecond` base first and truncates once via `System.convert_time_unit/3`. Align `NaiveDateTime.diff/3` with `DateTime.diff/3` so both agree on identical instants. All existing doctests are preserved. Assisted-by: Claude Code:claude-opus-4-8 --- lib/elixir/lib/calendar/naive_datetime.ex | 8 ++++--- .../elixir/calendar/naive_datetime_test.exs | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index df37473ee04..0c7af910141 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -570,9 +570,11 @@ defmodule NaiveDateTime do "unsupported time unit. Expected :day, :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" end - units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit) - units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit) - units1 - units2 + diff_microsecond = + (naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) - + (naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(:microsecond)) + + System.convert_time_unit(diff_microsecond, :microsecond, unit) end @doc """ diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 14e724dad3c..7c2f02f21aa 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -221,6 +221,29 @@ defmodule NaiveDateTimeTest do assert NaiveDateTime.diff(%{dt | second: 57}, dt, :second) == 50 end + + test "truncates the elapsed difference, not each operand (matches DateTime.diff)" do + # The true elapsed span is 0.5s (< 1s) and 86399.5s (< 1 day). Sub-second + # fractions must not inflate the count: per the "rounds incomplete days to + # zero" guarantee, and consistent with the sibling DateTime.diff/3. + assert NaiveDateTime.diff( + ~N[2000-01-01 00:00:01.200000], + ~N[2000-01-01 00:00:00.700000], + :second + ) == 0 + + assert NaiveDateTime.diff( + ~N[2000-01-02 00:00:00.200000], + ~N[2000-01-01 00:00:00.700000], + :day + ) == 0 + + assert NaiveDateTime.diff( + ~N[2000-01-01 00:00:00.001200], + ~N[2000-01-01 00:00:00.000700], + :millisecond + ) == 0 + end end test "convert/2" do