Skip to content

Commit

Permalink
Separate Duration date and time unit specs (elixir-lang#13480)
Browse files Browse the repository at this point in the history
  • Loading branch information
tfiedlerdejanze authored Apr 7, 2024
1 parent f5d1aa2 commit 1b8a34a
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 95 deletions.
32 changes: 30 additions & 2 deletions lib/elixir/lib/calendar/date.ex
Original file line number Diff line number Diff line change
Expand Up @@ -794,13 +794,41 @@ defmodule Date do
"""
@doc since: "1.17.0"
@spec shift(Calendar.date(), Duration.duration()) :: t
@spec shift(Calendar.date(), Duration.t() | [unit_pair]) :: t
when unit_pair: {:year, integer} | {:month, integer} | {:week, integer} | {:day, integer}
def shift(%{calendar: calendar} = date, duration) do
%{year: year, month: month, day: day} = date
{year, month, day} = calendar.shift_date(year, month, day, Duration.new!(duration))
{year, month, day} = calendar.shift_date(year, month, day, new_duration!(duration))
%Date{calendar: calendar, year: year, month: month, day: day}
end

defp new_duration!(%Duration{} = duration) do
duration
end

defp new_duration!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end

defp validate_duration_unit!({unit, _value})
when unit in [:hour, :minute, :second, :microsecond] do
raise ArgumentError, "unsupported unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end

defp validate_duration_unit!({unit, _value}) when unit not in [:year, :month, :week, :day] do
raise ArgumentError, "unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end

defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end

defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end

@doc false
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_iso_days(year, month, day), {0, 86_400_000_000}}
Expand Down
20 changes: 11 additions & 9 deletions lib/elixir/lib/calendar/duration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -76,31 +76,33 @@ defmodule Duration do
end

def new!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
Enum.each(unit_pairs, &validate_unit!/1)
struct!(Duration, unit_pairs)
end

defp validate_duration_unit!({:microsecond, {ms, precision}})
defp validate_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end

defp validate_duration_unit!({:microsecond, microsecond}) do
defp validate_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"expected a tuple {ms, precision} for microsecond where precision is an integer from 0 to 6, got #{inspect(microsecond)}"
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end

defp validate_duration_unit!({unit, _value})
defp validate_unit!({unit, _value})
when unit not in [:year, :month, :week, :day, :hour, :minute, :second] do
raise ArgumentError, "unexpected unit #{inspect(unit)}"
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond"
end

defp validate_duration_unit!({_unit, value}) when is_integer(value) do
defp validate_unit!({_unit, value}) when is_integer(value) do
:ok
end

defp validate_duration_unit!({unit, value}) do
raise ArgumentError, "expected an integer for #{inspect(unit)}, got #{inspect(value)}"
defp validate_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end

@doc """
Expand Down
6 changes: 4 additions & 2 deletions lib/elixir/lib/calendar/iso.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1637,7 +1637,8 @@ defmodule Calendar.ISO do
end

defp shift_date_options(_duration) do
raise ArgumentError, "cannot shift date by time units"
raise ArgumentError,
"cannot shift date by time scale unit. Expected :year, :month, :week, :day"
end

defp shift_datetime_options(%Duration{
Expand Down Expand Up @@ -1674,7 +1675,8 @@ defmodule Calendar.ISO do
end

defp shift_time_options(_duration) do
raise ArgumentError, "cannot shift time by date units"
raise ArgumentError,
"cannot shift time by date scale unit. Expected :hour, :minute, :second, :microsecond"
end

## Helpers
Expand Down
48 changes: 46 additions & 2 deletions lib/elixir/lib/calendar/time.ex
Original file line number Diff line number Diff line change
Expand Up @@ -571,12 +571,17 @@ defmodule Time do
"""
@doc since: "1.17.0"
@spec shift(Calendar.time(), Duration.duration()) :: t
@spec shift(Calendar.time(), Duration.t() | [unit_pair]) :: t
when unit_pair:
{:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
def shift(%{calendar: calendar} = time, duration) do
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time

{hour, minute, second, microsecond} =
calendar.shift_time(hour, minute, second, microsecond, Duration.new!(duration))
calendar.shift_time(hour, minute, second, microsecond, new_duration!(duration))

%Time{
calendar: calendar,
Expand All @@ -587,6 +592,45 @@ defmodule Time do
}
end

defp new_duration!(%Duration{} = duration) do
duration
end

defp new_duration!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end

defp validate_duration_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end

defp validate_duration_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end

defp validate_duration_unit!({unit, _value}) when unit in [:year, :month, :week, :day] do
raise ArgumentError,
"unsupported unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end

defp validate_duration_unit!({unit, _value})
when unit not in [:hour, :minute, :second, :microsecond] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end

defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end

defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end

@doc """
Compares two time structs.
Expand Down
16 changes: 10 additions & 6 deletions lib/elixir/test/elixir/calendar/date_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -194,13 +194,17 @@ defmodule DateTest do
assert Date.shift(~D[2000-01-01], month: 12) == ~D[2001-01-01]
assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == ~D[0004-02-03]

assert_raise ArgumentError, "cannot shift date by time units", fn ->
Date.shift(~D[2012-02-29], second: 86400)
end
assert_raise ArgumentError,
"unsupported unit :second. Expected :year, :month, :week, :day",
fn -> Date.shift(~D[2012-02-29], second: 86400) end

assert_raise ArgumentError, "unexpected unit :months", fn ->
Date.shift(~D[2012-01-01], months: 12)
end
assert_raise ArgumentError,
"unknown unit :months. Expected :year, :month, :week, :day",
fn -> Date.shift(~D[2012-01-01], months: 12) end

assert_raise ArgumentError,
"cannot shift date by time scale unit. Expected :year, :month, :week, :day",
fn -> Date.shift(~D[2012-02-29], %Duration{second: 86400}) end

# Implements calendar callback
assert_raise RuntimeError, "shift_date/4 not implemented", fn ->
Expand Down
6 changes: 3 additions & 3 deletions lib/elixir/test/elixir/calendar/datetime_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1151,8 +1151,8 @@ defmodule DateTimeTest do
zone_abbr: "CEST"
}

assert_raise ArgumentError, "unexpected unit :months", fn ->
DateTime.shift(~U[2012-01-01 00:00:00Z], months: 12)
end
assert_raise ArgumentError,
"unknown unit :months. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond",
fn -> DateTime.shift(~U[2012-01-01 00:00:00Z], months: 12) end
end
end
8 changes: 4 additions & 4 deletions lib/elixir/test/elixir/calendar/duration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@ defmodule DurationTest do
assert ^duration = Duration.new!(duration)

assert_raise ArgumentError,
"expected an integer for :month, got nil",
"unsupported value nil for :month. Expected an integer",
fn -> Duration.new!(month: nil) end

assert_raise ArgumentError,
"unexpected unit :years",
"unknown unit :years. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond",
fn -> Duration.new!(years: 1) end

assert_raise ArgumentError,
~s/expected a tuple {ms, precision} for microsecond where precision is an integer from 0 to 6, got {1, 2, 3}/,
"unsupported value {1, 2, 3} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6",
fn -> Duration.new!(microsecond: {1, 2, 3}) end

assert_raise ArgumentError,
~s/expected a tuple {ms, precision} for microsecond where precision is an integer from 0 to 6, got {100, 7}/,
"unsupported value {100, 7} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6",
fn -> Duration.new!(microsecond: {100, 7}) end
end

Expand Down
118 changes: 57 additions & 61 deletions lib/elixir/test/elixir/calendar/naive_datetime_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -392,67 +392,63 @@ defmodule NaiveDateTimeTest do
end
end

describe "shift/2" do
test "shifts with valid arguments" do
naive_datetime = ~N[2000-01-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, year: 1) == ~N[2001-01-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, month: 1) == ~N[2000-02-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, week: 3) == ~N[2000-01-22 00:00:00]
assert NaiveDateTime.shift(naive_datetime, day: 2) == ~N[2000-01-03 00:00:00]
assert NaiveDateTime.shift(naive_datetime, hour: 6) == ~N[2000-01-01 06:00:00]
assert NaiveDateTime.shift(naive_datetime, minute: 30) == ~N[2000-01-01 00:30:00]
assert NaiveDateTime.shift(naive_datetime, second: 45) == ~N[2000-01-01 00:00:45]
assert NaiveDateTime.shift(naive_datetime, year: -1) == ~N[1999-01-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, month: -1) == ~N[1999-12-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, week: -1) == ~N[1999-12-25 00:00:00]
assert NaiveDateTime.shift(naive_datetime, day: -1) == ~N[1999-12-31 00:00:00]
assert NaiveDateTime.shift(naive_datetime, hour: -12) == ~N[1999-12-31 12:00:00]
assert NaiveDateTime.shift(naive_datetime, minute: -45) == ~N[1999-12-31 23:15:00]
assert NaiveDateTime.shift(naive_datetime, second: -30) == ~N[1999-12-31 23:59:30]
assert NaiveDateTime.shift(naive_datetime, year: 1, month: 2) == ~N[2001-03-01 00:00:00]

assert NaiveDateTime.shift(naive_datetime, microsecond: {-500, 6}) ==
~N[1999-12-31 23:59:59.999500]

assert NaiveDateTime.shift(naive_datetime, microsecond: {500, 6}) ==
~N[2000-01-01 00:00:00.000500]

assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 6}) ==
~N[2000-01-01 00:00:00.000100]

assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 4}) ==
~N[2000-01-01 00:00:00.0001]

assert NaiveDateTime.shift(naive_datetime, month: 2, day: 3, hour: 6, minute: 15) ==
~N[2000-03-04 06:15:00]

assert NaiveDateTime.shift(naive_datetime,
year: 1,
month: 2,
week: 3,
day: 4,
hour: 5,
minute: 6,
second: 7,
microsecond: {8, 6}
) == ~N[2001-03-26 05:06:07.000008]

assert NaiveDateTime.shift(naive_datetime,
year: -1,
month: -2,
week: -3,
day: -4,
hour: -5,
minute: -6,
second: -7,
microsecond: {-8, 6}
) == ~N[1998-10-06 18:53:52.999992]
end
test "shift/2" do
naive_datetime = ~N[2000-01-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, year: 1) == ~N[2001-01-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, month: 1) == ~N[2000-02-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, week: 3) == ~N[2000-01-22 00:00:00]
assert NaiveDateTime.shift(naive_datetime, day: 2) == ~N[2000-01-03 00:00:00]
assert NaiveDateTime.shift(naive_datetime, hour: 6) == ~N[2000-01-01 06:00:00]
assert NaiveDateTime.shift(naive_datetime, minute: 30) == ~N[2000-01-01 00:30:00]
assert NaiveDateTime.shift(naive_datetime, second: 45) == ~N[2000-01-01 00:00:45]
assert NaiveDateTime.shift(naive_datetime, year: -1) == ~N[1999-01-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, month: -1) == ~N[1999-12-01 00:00:00]
assert NaiveDateTime.shift(naive_datetime, week: -1) == ~N[1999-12-25 00:00:00]
assert NaiveDateTime.shift(naive_datetime, day: -1) == ~N[1999-12-31 00:00:00]
assert NaiveDateTime.shift(naive_datetime, hour: -12) == ~N[1999-12-31 12:00:00]
assert NaiveDateTime.shift(naive_datetime, minute: -45) == ~N[1999-12-31 23:15:00]
assert NaiveDateTime.shift(naive_datetime, second: -30) == ~N[1999-12-31 23:59:30]
assert NaiveDateTime.shift(naive_datetime, year: 1, month: 2) == ~N[2001-03-01 00:00:00]

assert NaiveDateTime.shift(naive_datetime, microsecond: {-500, 6}) ==
~N[1999-12-31 23:59:59.999500]

assert NaiveDateTime.shift(naive_datetime, microsecond: {500, 6}) ==
~N[2000-01-01 00:00:00.000500]

assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 6}) ==
~N[2000-01-01 00:00:00.000100]

assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 4}) ==
~N[2000-01-01 00:00:00.0001]

assert NaiveDateTime.shift(naive_datetime, month: 2, day: 3, hour: 6, minute: 15) ==
~N[2000-03-04 06:15:00]

assert NaiveDateTime.shift(naive_datetime,
year: 1,
month: 2,
week: 3,
day: 4,
hour: 5,
minute: 6,
second: 7,
microsecond: {8, 6}
) == ~N[2001-03-26 05:06:07.000008]

assert NaiveDateTime.shift(naive_datetime,
year: -1,
month: -2,
week: -3,
day: -4,
hour: -5,
minute: -6,
second: -7,
microsecond: {-8, 6}
) == ~N[1998-10-06 18:53:52.999992]

test "fails with invalid unit" do
assert_raise ArgumentError, "unexpected unit :months", fn ->
NaiveDateTime.shift(~N[2000-01-01 00:00:00], months: 12)
end
end
assert_raise ArgumentError,
"unknown unit :months. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond",
fn -> NaiveDateTime.shift(~N[2000-01-01 00:00:00], months: 12) end
end
end
16 changes: 10 additions & 6 deletions lib/elixir/test/elixir/calendar/time_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,16 @@ defmodule TimeTest do
assert Time.shift(time, microsecond: {1000, 4}) == ~T[00:00:00.0010]
assert Time.shift(time, hour: 2, minute: 65, second: 5) == ~T[03:05:05.0]

assert_raise ArgumentError, "cannot shift time by date units", fn ->
Time.shift(time, day: 1)
end
assert_raise ArgumentError,
"unsupported unit :day. Expected :hour, :minute, :second, :microsecond",
fn -> Time.shift(time, day: 1) end

assert_raise ArgumentError, "unexpected unit :hours", fn ->
Time.shift(time, hours: 12)
end
assert_raise ArgumentError,
"unknown unit :hours. Expected :hour, :minute, :second, :microsecond",
fn -> Time.shift(time, hours: 12) end

assert_raise ArgumentError,
"cannot shift time by date scale unit. Expected :hour, :minute, :second, :microsecond",
fn -> Time.shift(time, %Duration{day: 1}) end
end
end

0 comments on commit 1b8a34a

Please sign in to comment.