Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions lib/elixir/lib/calendar/iso.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 """
Expand Down Expand Up @@ -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,
Expand All @@ -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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nesting in parentheses here allows the compiler to better optimize

microsecond
}
end

defp shift_time_options(%Duration{
Expand Down
307 changes: 164 additions & 143 deletions lib/elixir/test/elixir/calendar/iso_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -484,149 +484,170 @@ 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}}
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

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}}
end
end
end
Comment thread
preciz marked this conversation as resolved.

test "shift_time/2" do
Expand Down
Loading