From a17a5652d29169d35c18870cf199968899079131 Mon Sep 17 00:00:00 2001 From: preciz Date: Mon, 21 Sep 2026 17:05:13 +0200 Subject: [PATCH] Avoid repeated conversions when shifting Calendar.ISO times Replace duration options and reduction with tuple dispatch. Fold whole seconds into the time tuple and apply microseconds in a single shift, preserving precision and a fast path for zero shifts. Assisted-by: Codex:GPT-6 --- lib/elixir/lib/calendar/iso.ex | 28 +++---- lib/elixir/test/elixir/calendar/iso_test.exs | 77 ++++++++++++-------- 2 files changed, 56 insertions(+), 49 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 794fe93771..8bc7644b89 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1923,18 +1923,14 @@ defmodule Calendar.ISO do @spec shift_time(hour, minute, second, microsecond, Duration.t()) :: {hour, minute, second, microsecond} def shift_time(hour, minute, second, microsecond, duration) do - shift_options = shift_time_options(duration) + case duration_to_seconds_and_microseconds(duration) do + {0, {0, _}} -> + {hour, minute, second, microsecond} - Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn - {:microsecond, {0, _}}, time -> - time - - {_, 0}, time -> - time - - {time_unit, value}, time -> - shift_time_unit(time, value, time_unit) - end) + {seconds, shift_microsecond} -> + time = {hour, minute, second + seconds, microsecond} + shift_time_unit(time, shift_microsecond, :microsecond) + end end @doc false @@ -2039,7 +2035,8 @@ defmodule Calendar.ISO do } end - defp shift_time_options(%Duration{ + @compile {:inline, duration_to_seconds_and_microseconds: 1} + defp duration_to_seconds_and_microseconds(%Duration{ year: 0, month: 0, week: 0, @@ -2049,13 +2046,10 @@ defmodule Calendar.ISO do second: second, microsecond: microsecond }) do - [ - second: hour * 3600 + minute * 60 + second, - microsecond: microsecond - ] + {(hour * 60 + minute) * 60 + second, microsecond} end - defp shift_time_options(_duration) do + defp duration_to_seconds_and_microseconds(_duration) do raise ArgumentError, "cannot shift time by date scale unit. Expected :hour, :minute, :second, :microsecond" end diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 9a0009671c..964c9f3d9b 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -650,49 +650,62 @@ defmodule Calendar.ISOTest do 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}} + describe "shift_time/5" do + test "shifts by duration" 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}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(minute: 30)) == - {0, 30, 0, {0, 0}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(minute: 30)) == + {0, 30, 0, {0, 0}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(minute: -30)) == - {23, 30, 0, {0, 0}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(minute: -30)) == + {23, 30, 0, {0, 0}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(second: 30)) == - {0, 0, 30, {0, 0}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(second: 30)) == + {0, 0, 30, {0, 0}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(second: -30)) == - {23, 59, 30, {0, 0}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(second: -30)) == + {23, 59, 30, {0, 0}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6})) == - {0, 0, 0, {100, 6}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6})) == + {0, 0, 0, {100, 6}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {-100, 6})) == - {23, 59, 59, {999_900, 6}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {-100, 6})) == + {23, 59, 59, {999_900, 6}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {2000, 4})) == - {0, 0, 0, {2000, 4}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {2000, 4})) == + {0, 0, 0, {2000, 4}} - assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {-2000, 4})) == - {23, 59, 59, {998_000, 4}} + assert Calendar.ISO.shift_time(0, 0, 0, {0, 0}, Duration.new!(microsecond: {-2000, 4})) == + {23, 59, 59, {998_000, 4}} - assert Calendar.ISO.shift_time(0, 0, 0, {3500, 6}, Duration.new!(microsecond: {-2000, 4})) == - {0, 0, 0, {1500, 4}} + assert Calendar.ISO.shift_time(0, 0, 0, {3500, 6}, Duration.new!(microsecond: {-2000, 4})) == + {0, 0, 0, {1500, 4}} - assert Calendar.ISO.shift_time(0, 0, 0, {3500, 4}, Duration.new!(minute: 5)) == - {0, 5, 0, {3500, 4}} + assert Calendar.ISO.shift_time(0, 0, 0, {3500, 4}, Duration.new!(minute: 5)) == + {0, 5, 0, {3500, 4}} - assert Calendar.ISO.shift_time(0, 0, 0, {3500, 6}, Duration.new!(hour: 4)) == - {4, 0, 0, {3500, 6}} + assert Calendar.ISO.shift_time(0, 0, 0, {3500, 6}, Duration.new!(hour: 4)) == + {4, 0, 0, {3500, 6}} - assert Calendar.ISO.shift_time( - 23, - 59, - 59, - {999_900, 6}, - Duration.new!(hour: 4, microsecond: {100, 6}) - ) == {4, 0, 0, {0, 6}} + assert Calendar.ISO.shift_time( + 23, + 59, + 59, + {999_900, 6}, + Duration.new!(hour: 4, microsecond: {100, 6}) + ) == {4, 0, 0, {0, 6}} + end + + test "updates precision when time shifts cancel" do + for second <- [-1, 1], precision <- [2, 6] do + duration = Duration.new!(second: second, microsecond: {-second * 1_000_000, precision}) + + assert Calendar.ISO.shift_time(12, 0, 0, {120_000, 3}, duration) == + {12, 0, 0, {120_000, precision}} + end + end end end