From 9c93b6a54d07de1ab7f8f20eb840ed20473e4337 Mon Sep 17 00:00:00 2001 From: preciz Date: Mon, 21 Sep 2026 08:39:32 +0200 Subject: [PATCH 1/2] Avoid repeated conversions when shifting Calendar.ISO datetimes Combine seconds and microseconds into one shift after applying months, preserving existing precision semantics. Inline duration arithmetic and set precision upfront to tail-call the shift helper. Assisted-by: Codex:GPT-6 --- lib/elixir/lib/calendar/iso.ex | 48 ++++++++++++-------- lib/elixir/test/elixir/calendar/iso_test.exs | 31 +++++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index d10c621ec5c..794fe93771d 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1881,23 +1881,32 @@ defmodule Calendar.ISO do microsecond, Duration.t() ) :: {year, month, day, hour, minute, second, microsecond} - def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do - shift_options = shift_datetime_options(duration) + def shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + {value, _} = microsecond, + duration + ) do + {months, seconds, shift_microsecond} = shift_datetime_units(duration) - Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn - {:microsecond, {0, _}}, naive_datetime -> - naive_datetime + {year, month, day} = + if months == 0, do: {year, month, day}, else: shift_months({year, month, day}, months) - {_, 0}, naive_datetime -> - naive_datetime + datetime = {year, month, day, hour, minute, second, microsecond} + {microseconds, precision} = shift_time_unit_values(shift_microsecond, microsecond) - {:month, value}, {year, month, day, hour, minute, second, microsecond} -> - {new_year, new_month, new_day} = shift_months({year, month, day}, value) - {new_year, new_month, new_day, hour, minute, second, microsecond} + if seconds == 0 and microseconds == 0 do + datetime + else + total_microseconds = seconds * @microseconds_per_second + microseconds - {time_unit, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, time_unit) - end) + datetime = put_elem(datetime, 6, {value, precision}) + shift_time_unit(datetime, total_microseconds, :microsecond) + end end @doc """ @@ -2012,7 +2021,8 @@ defmodule Calendar.ISO do "cannot shift date by time scale unit. Expected :year, :month, :week, :day" end - defp shift_datetime_options(%Duration{ + @compile {:inline, shift_datetime_units: 1} + defp shift_datetime_units(%Duration{ year: year, month: month, week: week, @@ -2022,11 +2032,11 @@ defmodule Calendar.ISO do second: second, microsecond: microsecond }) do - [ - month: year * 12 + month, - second: week * 7 * 86_400 + day * 86_400 + hour * 3600 + minute * 60 + second, - microsecond: microsecond - ] + { + year * 12 + month, + (((week * @days_per_week + day) * 24 + hour) * 60 + minute) * 60 + second, + microsecond + } end defp shift_time_options(%Duration{ diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 1eda3d81bb3..19789fbc079 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -629,6 +629,37 @@ defmodule Calendar.ISOTest do ) == {1999, 12, 31, 22, 55, 0, {0, 0}} end + test "shift_naive_datetime/8 applies months before combined time shifts" do + duration = Duration.new!(month: 1, second: 1, microsecond: {200_000, 6}) + + assert Calendar.ISO.shift_naive_datetime(2024, 1, 30, 23, 59, 59, {900_000, 3}, duration) == + {2024, 3, 1, 0, 0, 1, {100_000, 6}} + + duration = Duration.new!(month: -1, second: -1, microsecond: {-200_000, 6}) + + assert Calendar.ISO.shift_naive_datetime(2024, 3, 31, 0, 0, 0, {100_000, 3}, duration) == + {2024, 2, 28, 23, 59, 58, {900_000, 6}} + end + + test "shift_naive_datetime/8 preserves precision for zero microsecond shifts" do + for second <- [-1, 0, 1], precision <- [0, 6] do + duration = Duration.new!(second: second, microsecond: {0, precision}) + + assert Calendar.ISO.shift_naive_datetime(2024, 1, 31, 12, 0, 30, {120_000, 3}, duration) == + {2024, 1, 31, 12, 0, 30 + second, {120_000, 3}} + end + end + + test "shift_naive_datetime/8 updates precision when time shifts cancel" do + for second <- [-1, 1], precision <- [2, 6] do + duration = + Duration.new!(month: 1, second: second, microsecond: {-second * 1_000_000, precision}) + + assert Calendar.ISO.shift_naive_datetime(2024, 1, 31, 12, 0, 0, {120_000, 3}, duration) == + {2024, 2, 29, 12, 0, 0, {120_000, precision}} + end + end + test "shift_time/2" do assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(hour: 1)) == {1, 0, 0, {0, 0}} assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(hour: -1)) == {23, 0, 0, {0, 0}} From a56c2c573cd0fba237971990e6f541e471b60264 Mon Sep 17 00:00:00 2001 From: preciz Date: Mon, 21 Sep 2026 13:26:44 +0200 Subject: [PATCH 2/2] Group Calendar.ISO datetime shift tests Remove redundant month-ordering coverage and retain precision loop tests. Assisted-by: Codex:GPT-6 --- lib/elixir/test/elixir/calendar/iso_test.exs | 330 +++++++++---------- 1 file changed, 160 insertions(+), 170 deletions(-) diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 19789fbc079..9a0009671c1 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -484,179 +484,169 @@ defmodule Calendar.ISOTest do assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 9)) == {2024, 10, 31} end - test "shift_naive_datetime/2" do - assert Calendar.ISO.shift_naive_datetime( - 2024, - 3, - 2, - 0, - 0, - 0, - {0, 0}, - Duration.new!([]) - ) == {2024, 3, 2, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 1, - 1, - 0, - 0, - 0, - {0, 0}, - Duration.new!(year: 1) - ) == {2001, 1, 1, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 1, - 1, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: 1) - ) == {2000, 2, 1, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 1, - 1, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: 1, day: 28) - ) == {2000, 2, 29, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 1, - 1, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: 1, day: 30) - ) == {2000, 3, 2, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 1, - 1, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: 2, day: 29) - ) == {2000, 3, 30, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 2, - 29, - 0, - 0, - 0, - {0, 0}, - Duration.new!(year: -1) - ) == {1999, 2, 28, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 2, - 29, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: -1) - ) == {2000, 1, 29, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 2, - 29, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: -1, day: -28) - ) == {2000, 1, 1, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 2, - 29, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: -1, day: -30) - ) == {1999, 12, 30, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 2, - 29, - 0, - 0, - 0, - {0, 0}, - Duration.new!(month: -1, day: -29) - ) == {1999, 12, 31, 0, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 1, - 1, - 0, - 0, - 0, - {0, 0}, - Duration.new!(hour: 12) - ) == {2000, 1, 1, 12, 0, 0, {0, 0}} - - assert Calendar.ISO.shift_naive_datetime( - 2000, - 1, - 1, - 0, - 0, - 0, - {0, 0}, - Duration.new!(minute: -65) - ) == {1999, 12, 31, 22, 55, 0, {0, 0}} - end - - test "shift_naive_datetime/8 applies months before combined time shifts" do - duration = Duration.new!(month: 1, second: 1, microsecond: {200_000, 6}) - - assert Calendar.ISO.shift_naive_datetime(2024, 1, 30, 23, 59, 59, {900_000, 3}, duration) == - {2024, 3, 1, 0, 0, 1, {100_000, 6}} - - duration = Duration.new!(month: -1, second: -1, microsecond: {-200_000, 6}) - - assert Calendar.ISO.shift_naive_datetime(2024, 3, 31, 0, 0, 0, {100_000, 3}, duration) == - {2024, 2, 28, 23, 59, 58, {900_000, 6}} - end - - test "shift_naive_datetime/8 preserves precision for zero microsecond shifts" do - for second <- [-1, 0, 1], precision <- [0, 6] do - duration = Duration.new!(second: second, microsecond: {0, precision}) - - assert Calendar.ISO.shift_naive_datetime(2024, 1, 31, 12, 0, 30, {120_000, 3}, duration) == - {2024, 1, 31, 12, 0, 30 + second, {120_000, 3}} + describe "shift_naive_datetime/8" do + test "shifts by duration" do + assert Calendar.ISO.shift_naive_datetime( + 2024, + 3, + 2, + 0, + 0, + 0, + {0, 0}, + Duration.new!([]) + ) == {2024, 3, 2, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Duration.new!(year: 1) + ) == {2001, 1, 1, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: 1) + ) == {2000, 2, 1, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: 1, day: 28) + ) == {2000, 2, 29, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: 1, day: 30) + ) == {2000, 3, 2, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: 2, day: 29) + ) == {2000, 3, 30, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 2, + 29, + 0, + 0, + 0, + {0, 0}, + Duration.new!(year: -1) + ) == {1999, 2, 28, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 2, + 29, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: -1) + ) == {2000, 1, 29, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 2, + 29, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: -1, day: -28) + ) == {2000, 1, 1, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 2, + 29, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: -1, day: -30) + ) == {1999, 12, 30, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 2, + 29, + 0, + 0, + 0, + {0, 0}, + Duration.new!(month: -1, day: -29) + ) == {1999, 12, 31, 0, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Duration.new!(hour: 12) + ) == {2000, 1, 1, 12, 0, 0, {0, 0}} + + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Duration.new!(minute: -65) + ) == {1999, 12, 31, 22, 55, 0, {0, 0}} + end + + test "preserves precision for zero microsecond shifts" do + for second <- [-1, 0, 1], precision <- [0, 6] do + duration = Duration.new!(second: second, microsecond: {0, precision}) + + assert Calendar.ISO.shift_naive_datetime(2024, 1, 31, 12, 0, 30, {120_000, 3}, duration) == + {2024, 1, 31, 12, 0, 30 + second, {120_000, 3}} + end end - end - test "shift_naive_datetime/8 updates precision when time shifts cancel" do - for second <- [-1, 1], precision <- [2, 6] do - duration = - Duration.new!(month: 1, second: second, microsecond: {-second * 1_000_000, precision}) + test "updates precision when time shifts cancel" do + for second <- [-1, 1], precision <- [2, 6] do + duration = + Duration.new!(month: 1, second: second, microsecond: {-second * 1_000_000, precision}) - assert Calendar.ISO.shift_naive_datetime(2024, 1, 31, 12, 0, 0, {120_000, 3}, duration) == - {2024, 2, 29, 12, 0, 0, {120_000, precision}} + assert Calendar.ISO.shift_naive_datetime(2024, 1, 31, 12, 0, 0, {120_000, 3}, duration) == + {2024, 2, 29, 12, 0, 0, {120_000, precision}} + end end end