From 2ea8b4434e307a636e096ba3c48669d1341968f5 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 09:12:10 +0100 Subject: [PATCH 01/97] Date.shift/2 for ISO dates --- lib/elixir/lib/calendar/date.ex | 83 ++++++++++++++++++- lib/elixir/test/elixir/calendar/date_test.exs | 44 ++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 173365a7976..9c0d89404f3 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -40,7 +40,7 @@ defmodule Date do ## Using epochs - The `add/2` and `diff/2` functions can be used for computing dates + The `add/2`, `diff/2` and `shift/2` functions can be used for computing dates or retrieving the number of days between instants. For example, if there is an interest in computing the number of days from the Unix epoch (1970-01-01): @@ -51,6 +51,9 @@ defmodule Date do iex> Date.add(~D[1970-01-01], 14716) ~D[2010-04-17] + iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 3) + ~D[2010-04-18] + Those functions are optimized to deal with common epochs, such as the Unix Epoch above or the Gregorian Epoch (0000-01-01). """ @@ -65,6 +68,15 @@ defmodule Date do calendar: Calendar.calendar() } + @typedoc """ + The shift units that can be applied when shifting dates. + """ + @type shift_unit :: + {:year, integer()} + | {:month, integer()} + | {:week, integer()} + | {:day, integer()} + @doc """ Returns a range of dates. @@ -757,6 +769,75 @@ defmodule Date do end end + @doc """ + Shifts a date by given units. Raises an `ArgumentError` if the + given date is not a valid Calendar.ISO date. + + Available shift units are: `:year, :month, :week, :day` and the shift + is always applied in that order. + + `Date.shift/2` defines a week as 7 days and a year as 12 months. + + When shifting by month: + - it will shift to the current day of a month + - if the current day does not exist in a month, it will shift to the last day of a month + + ## Examples + + iex> Date.shift(~D[2016-01-03], month: 2) + ~D[2016-03-03] + iex> Date.shift(~D[2016-02-29], month: 1) + ~D[2016-03-29] + iex> Date.shift(~D[2016-01-31], month: 1) + ~D[2016-02-29] + iex> Date.shift(~D[2016-01-31], year: 4, day: 1) + ~D[2020-02-01] + + """ + # @doc since: "1.5.0" + @spec shift(Calendar.date(), [shift_unit()]) :: t + def shift(%{calendar: Calendar.ISO} = date, shift_units) do + shift_units = Keyword.validate!(shift_units, year: 0, month: 0, week: 0, day: 0) + + ordered_units = [ + year: shift_units[:year], + month: shift_units[:month], + week: shift_units[:week], + day: shift_units[:day] + ] + + Enum.reduce(ordered_units, date, fn + {_opt, 0}, new_date -> new_date + {:year, value}, new_date -> shift_months(new_date, value * 12) + {:month, value}, new_date -> shift_months(new_date, value) + {:week, value}, new_date -> Date.add(new_date, value * 7) + {:day, value}, new_date -> Date.add(new_date, value) + end) + end + + def shift(_date, _opts) do + raise ArgumentError, "date invalid or not supported" + end + + @doc false + defp shift_months(date, 0), do: date + + defp shift_months(%Date{year: year, month: month, day: day}, months) do + total_months = year * 12 + month + months - 1 + new_year = Integer.floor_div(total_months, 12) + + new_month = + case rem(total_months, 12) + 1 do + 0 -> 12 + new_month -> new_month + end + + last_day_of_month = :calendar.last_day_of_the_month(abs(new_year), new_month) + new_day = min(day, last_day_of_month) + + Date.new!(new_year, new_month, new_day) + 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}} diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index abbd8b75e63..138065dea3e 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -179,4 +179,48 @@ defmodule DateTest do assert Date.diff(date1, date2) == -13 assert Date.diff(date2, date1) == 13 end + + test "shift/2" do + date1 = ~D[2012-02-29] + assert Date.shift(date1, day: -1) == ~D[2012-02-28] + assert Date.shift(date1, day: 1) == ~D[2012-03-01] + assert Date.shift(date1, month: -1) == ~D[2012-01-29] + assert Date.shift(date1, month: -2) == ~D[2011-12-29] + assert Date.shift(date1, week: -1) == ~D[2012-02-22] + assert Date.shift(date1, week: -9) == ~D[2011-12-28] + assert Date.shift(date1, month: 1) == ~D[2012-03-29] + assert Date.shift(date1, year: -1) == ~D[2011-02-28] + assert Date.shift(date1, year: 1) == ~D[2013-02-28] + assert Date.shift(date1, year: 4) == ~D[2016-02-29] + + assert Date.shift(date1, year: 1, day: 2) == ~D[2013-03-02] + assert Date.shift(date1, day: -2, year: -1, month: 24) == ~D[2013-02-26] + assert Date.shift(date1, day: 2, year: 4, month: 1) == ~D[2016-03-31] + assert Date.shift(date1, year: 1) == Date.shift(date1, month: 12) + + date2 = ~D[0000-01-01] + assert Date.shift(date2, day: -1) == ~D[-0001-12-31] + assert Date.shift(date2, day: 1) == ~D[0000-01-02] + assert Date.shift(date2, month: -1) == ~D[-0001-12-01] + assert Date.shift(date2, month: 1) == ~D[0000-02-01] + assert Date.shift(date2, year: -1) == ~D[-0001-01-01] + assert Date.shift(date2, year: 1) == ~D[0001-01-01] + assert Date.shift(date2, day: 2, year: 1, month: 37) == ~D[0004-02-03] + + date3 = ~D[2000-01-01] + assert Date.shift(date3, month: 12) == ~D[2001-01-01] + assert Date.shift(date3, month: 14) == ~D[2001-03-01] + assert Date.shift(date3, month: -32) == ~D[1997-05-01] + assert Date.shift(date3, day: 2, year: 4, month: 1) == ~D[2004-02-03] + + assert_raise ArgumentError, fn -> + date = ~D[2000-01-01] + Date.shift(date, months: 12) + end + + assert_raise ArgumentError, ~s/date invalid or not supported/, fn -> + date = Calendar.Holocene.date(12000, 01, 01) + Date.shift(date, month: 12) + end + end end From df03f59c1ec87e61ebd0f604f438da004310b12a Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 12:30:14 +0100 Subject: [PATCH 02/97] do not order shift options --- lib/elixir/lib/calendar/date.ex | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 9c0d89404f3..51980c5e3ae 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -773,14 +773,14 @@ defmodule Date do Shifts a date by given units. Raises an `ArgumentError` if the given date is not a valid Calendar.ISO date. - Available shift units are: `:year, :month, :week, :day` and the shift - is always applied in that order. + Available shift units are: `:year, :month, :week, :day` + The shift options are applied in the order they are given. `Date.shift/2` defines a week as 7 days and a year as 12 months. When shifting by month: - it will shift to the current day of a month - - if the current day does not exist in a month, it will shift to the last day of a month + - when the current day does not exist in a month, it will shift to the last day of a month ## Examples @@ -799,14 +799,7 @@ defmodule Date do def shift(%{calendar: Calendar.ISO} = date, shift_units) do shift_units = Keyword.validate!(shift_units, year: 0, month: 0, week: 0, day: 0) - ordered_units = [ - year: shift_units[:year], - month: shift_units[:month], - week: shift_units[:week], - day: shift_units[:day] - ] - - Enum.reduce(ordered_units, date, fn + Enum.reduce(shift_units, date, fn {_opt, 0}, new_date -> new_date {:year, value}, new_date -> shift_months(new_date, value * 12) {:month, value}, new_date -> shift_months(new_date, value) From 209dc137c18b8d9f39afdacf2af8fd2c31d1c0c8 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 12:35:21 +0100 Subject: [PATCH 03/97] use calendar.days_in_month/2 --- lib/elixir/lib/calendar/date.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 51980c5e3ae..3311e28dc99 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -815,7 +815,7 @@ defmodule Date do @doc false defp shift_months(date, 0), do: date - defp shift_months(%Date{year: year, month: month, day: day}, months) do + defp shift_months(%Date{calendar: calendar, year: year, month: month, day: day}, months) do total_months = year * 12 + month + months - 1 new_year = Integer.floor_div(total_months, 12) @@ -825,7 +825,7 @@ defmodule Date do new_month -> new_month end - last_day_of_month = :calendar.last_day_of_the_month(abs(new_year), new_month) + last_day_of_month = calendar.days_in_month(new_year, new_month) new_day = min(day, last_day_of_month) Date.new!(new_year, new_month, new_day) From d1e09e461a66b604142032c9891c983963f9741c Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 12:47:29 +0100 Subject: [PATCH 04/97] cleanup last day of month --- lib/elixir/lib/calendar/date.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 3311e28dc99..68453d4dc33 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -825,8 +825,7 @@ defmodule Date do new_month -> new_month end - last_day_of_month = calendar.days_in_month(new_year, new_month) - new_day = min(day, last_day_of_month) + new_day = min(day, calendar.days_in_month(new_year, new_month)) Date.new!(new_year, new_month, new_day) end From e275c4bd0a06c4facdf5612eb6a8a59a6898214c Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 13:05:43 +0100 Subject: [PATCH 05/97] return tuple from Date.shift/2 --- lib/elixir/lib/calendar/date.ex | 28 +++++------ lib/elixir/test/elixir/calendar/date_test.exs | 50 +++++++++---------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 68453d4dc33..6d2c7563f81 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -52,7 +52,7 @@ defmodule Date do ~D[2010-04-17] iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 3) - ~D[2010-04-18] + {:ok, ~D[2010-04-18]} Those functions are optimized to deal with common epochs, such as the Unix Epoch above or the Gregorian Epoch (0000-01-01). @@ -785,27 +785,27 @@ defmodule Date do ## Examples iex> Date.shift(~D[2016-01-03], month: 2) - ~D[2016-03-03] + {:ok, ~D[2016-03-03]} iex> Date.shift(~D[2016-02-29], month: 1) - ~D[2016-03-29] + {:ok, ~D[2016-03-29]} iex> Date.shift(~D[2016-01-31], month: 1) - ~D[2016-02-29] + {:ok, ~D[2016-02-29]} iex> Date.shift(~D[2016-01-31], year: 4, day: 1) - ~D[2020-02-01] + {:ok, ~D[2020-02-01]} """ - # @doc since: "1.5.0" - @spec shift(Calendar.date(), [shift_unit()]) :: t + @spec shift(Calendar.date(), [shift_unit()]) :: {:ok, t} def shift(%{calendar: Calendar.ISO} = date, shift_units) do shift_units = Keyword.validate!(shift_units, year: 0, month: 0, week: 0, day: 0) - Enum.reduce(shift_units, date, fn - {_opt, 0}, new_date -> new_date - {:year, value}, new_date -> shift_months(new_date, value * 12) - {:month, value}, new_date -> shift_months(new_date, value) - {:week, value}, new_date -> Date.add(new_date, value * 7) - {:day, value}, new_date -> Date.add(new_date, value) - end) + {:ok, + Enum.reduce(shift_units, date, fn + {_opt, 0}, new_date -> new_date + {:year, value}, new_date -> shift_months(new_date, value * 12) + {:month, value}, new_date -> shift_months(new_date, value) + {:week, value}, new_date -> Date.add(new_date, value * 7) + {:day, value}, new_date -> Date.add(new_date, value) + end)} end def shift(_date, _opts) do diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index 138065dea3e..b8e490e0191 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -182,36 +182,36 @@ defmodule DateTest do test "shift/2" do date1 = ~D[2012-02-29] - assert Date.shift(date1, day: -1) == ~D[2012-02-28] - assert Date.shift(date1, day: 1) == ~D[2012-03-01] - assert Date.shift(date1, month: -1) == ~D[2012-01-29] - assert Date.shift(date1, month: -2) == ~D[2011-12-29] - assert Date.shift(date1, week: -1) == ~D[2012-02-22] - assert Date.shift(date1, week: -9) == ~D[2011-12-28] - assert Date.shift(date1, month: 1) == ~D[2012-03-29] - assert Date.shift(date1, year: -1) == ~D[2011-02-28] - assert Date.shift(date1, year: 1) == ~D[2013-02-28] - assert Date.shift(date1, year: 4) == ~D[2016-02-29] - - assert Date.shift(date1, year: 1, day: 2) == ~D[2013-03-02] - assert Date.shift(date1, day: -2, year: -1, month: 24) == ~D[2013-02-26] - assert Date.shift(date1, day: 2, year: 4, month: 1) == ~D[2016-03-31] + assert Date.shift(date1, day: -1) == {:ok, ~D[2012-02-28]} + assert Date.shift(date1, day: 1) == {:ok, ~D[2012-03-01]} + assert Date.shift(date1, month: -1) == {:ok, ~D[2012-01-29]} + assert Date.shift(date1, month: -2) == {:ok, ~D[2011-12-29]} + assert Date.shift(date1, week: -1) == {:ok, ~D[2012-02-22]} + assert Date.shift(date1, week: -9) == {:ok, ~D[2011-12-28]} + assert Date.shift(date1, month: 1) == {:ok, ~D[2012-03-29]} + assert Date.shift(date1, year: -1) == {:ok, ~D[2011-02-28]} + assert Date.shift(date1, year: 1) == {:ok, ~D[2013-02-28]} + assert Date.shift(date1, year: 4) == {:ok, ~D[2016-02-29]} + + assert Date.shift(date1, year: 1, day: 2) == {:ok, ~D[2013-03-02]} + assert Date.shift(date1, day: -2, year: -1, month: 24) == {:ok, ~D[2013-02-26]} + assert Date.shift(date1, day: 2, year: 4, month: 1) == {:ok, ~D[2016-03-31]} assert Date.shift(date1, year: 1) == Date.shift(date1, month: 12) date2 = ~D[0000-01-01] - assert Date.shift(date2, day: -1) == ~D[-0001-12-31] - assert Date.shift(date2, day: 1) == ~D[0000-01-02] - assert Date.shift(date2, month: -1) == ~D[-0001-12-01] - assert Date.shift(date2, month: 1) == ~D[0000-02-01] - assert Date.shift(date2, year: -1) == ~D[-0001-01-01] - assert Date.shift(date2, year: 1) == ~D[0001-01-01] - assert Date.shift(date2, day: 2, year: 1, month: 37) == ~D[0004-02-03] + assert Date.shift(date2, day: -1) == {:ok, ~D[-0001-12-31]} + assert Date.shift(date2, day: 1) == {:ok, ~D[0000-01-02]} + assert Date.shift(date2, month: -1) == {:ok, ~D[-0001-12-01]} + assert Date.shift(date2, month: 1) == {:ok, ~D[0000-02-01]} + assert Date.shift(date2, year: -1) == {:ok, ~D[-0001-01-01]} + assert Date.shift(date2, year: 1) == {:ok, ~D[0001-01-01]} + assert Date.shift(date2, day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} date3 = ~D[2000-01-01] - assert Date.shift(date3, month: 12) == ~D[2001-01-01] - assert Date.shift(date3, month: 14) == ~D[2001-03-01] - assert Date.shift(date3, month: -32) == ~D[1997-05-01] - assert Date.shift(date3, day: 2, year: 4, month: 1) == ~D[2004-02-03] + assert Date.shift(date3, month: 12) == {:ok, ~D[2001-01-01]} + assert Date.shift(date3, month: 14) == {:ok, ~D[2001-03-01]} + assert Date.shift(date3, month: -32) == {:ok, ~D[1997-05-01]} + assert Date.shift(date3, day: 2, year: 4, month: 1) == {:ok, ~D[2004-02-03]} assert_raise ArgumentError, fn -> date = ~D[2000-01-01] From a07b738da0bcd088ea07a97494694ea5121c91bf Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 17:29:51 +0100 Subject: [PATCH 06/97] implement Calendar.ISO.shift_date/4 Calendar callback --- lib/elixir/lib/calendar.ex | 5 ++ lib/elixir/lib/calendar/date.ex | 60 ++------------ lib/elixir/lib/calendar/iso.ex | 82 +++++++++++++++++++ lib/elixir/test/elixir/calendar/date_test.exs | 2 +- 4 files changed, 94 insertions(+), 55 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 8bab6e2794b..98e4ec9e45f 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -338,6 +338,11 @@ defmodule Calendar do @doc since: "1.15.0" @callback iso_days_to_end_of_day(iso_days) :: iso_days + @doc """ + Shifts date by given duration according to its calendar. + """ + @callback shift_date(year(), month(), day(), keyword()) :: {year, month, day} + # General Helpers @doc """ diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 6d2c7563f81..88907f1fafc 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -68,15 +68,6 @@ defmodule Date do calendar: Calendar.calendar() } - @typedoc """ - The shift units that can be applied when shifting dates. - """ - @type shift_unit :: - {:year, integer()} - | {:month, integer()} - | {:week, integer()} - | {:day, integer()} - @doc """ Returns a range of dates. @@ -770,17 +761,7 @@ defmodule Date do end @doc """ - Shifts a date by given units. Raises an `ArgumentError` if the - given date is not a valid Calendar.ISO date. - - Available shift units are: `:year, :month, :week, :day` - The shift options are applied in the order they are given. - - `Date.shift/2` defines a week as 7 days and a year as 12 months. - - When shifting by month: - - it will shift to the current day of a month - - when the current day does not exist in a month, it will shift to the last day of a month + Shifts a date by given list of durations according to its calendar. ## Examples @@ -794,40 +775,11 @@ defmodule Date do {:ok, ~D[2020-02-01]} """ - @spec shift(Calendar.date(), [shift_unit()]) :: {:ok, t} - def shift(%{calendar: Calendar.ISO} = date, shift_units) do - shift_units = Keyword.validate!(shift_units, year: 0, month: 0, week: 0, day: 0) - - {:ok, - Enum.reduce(shift_units, date, fn - {_opt, 0}, new_date -> new_date - {:year, value}, new_date -> shift_months(new_date, value * 12) - {:month, value}, new_date -> shift_months(new_date, value) - {:week, value}, new_date -> Date.add(new_date, value * 7) - {:day, value}, new_date -> Date.add(new_date, value) - end)} - end - - def shift(_date, _opts) do - raise ArgumentError, "date invalid or not supported" - end - - @doc false - defp shift_months(date, 0), do: date - - defp shift_months(%Date{calendar: calendar, year: year, month: month, day: day}, months) do - total_months = year * 12 + month + months - 1 - new_year = Integer.floor_div(total_months, 12) - - new_month = - case rem(total_months, 12) + 1 do - 0 -> 12 - new_month -> new_month - end - - new_day = min(day, calendar.days_in_month(new_year, new_month)) - - Date.new!(new_year, new_month, new_day) + @spec shift(Calendar.date(), keyword()) :: {:ok, t} + def shift(%{calendar: calendar} = date, shift_units) do + %{year: year, month: month, day: day} = date + {year, month, day} = calendar.shift_date(year, month, day, shift_units) + {:ok, %Date{calendar: Calendar.ISO, year: year, month: month, day: day}} end @doc false diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 99c977c127c..101030de1a3 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -161,6 +161,15 @@ defmodule Calendar.ISO do @type utc_offset :: integer @type format :: :basic | :extended + @typedoc """ + The shift units that can be applied when shifting dates. + """ + @type shift_unit :: + {:year, integer()} + | {:month, integer()} + | {:week, integer()} + | {:day, integer()} + @typedoc """ Microseconds with stored precision. @@ -1455,6 +1464,79 @@ defmodule Calendar.ISO do {days, {@parts_per_day - 1, @parts_per_day}} end + @doc """ + Shifts the given date by a list of shift units. + + Available units are: `:year, :month, :week, :day` which + are applied in that order. + + When shifting by month: + - it will shift to the current day of a month + - when the current day does not exist in a month, it will shift to the last day of a month + + ## Examples + + iex> Calendar.ISO.shift_date(2016, 1, 3, month: 2) + {2016, 3, 3} + iex> Calendar.ISO.shift_date(2016, 2, 29, month: 1) + {2016, 3, 29} + iex> Calendar.ISO.shift_date(2016, 1, 31, month: 1) + {2016, 2, 29} + iex> Calendar.ISO.shift_date(2016, 1, 31, year: 4, day: 1) + {2020, 2, 1} + """ + @spec shift_date(year(), month(), day(), [shift_unit()]) :: {year, month, day} + @impl true + def shift_date(year, month, day, shift_units) do + shift_units = Keyword.validate!(shift_units, year: 0, month: 0, week: 0, day: 0) + + Enum.reduce(shift_units, {year, month, day}, fn + {_opt, 0}, new_date -> new_date + {:year, value}, new_date -> shift_years(new_date, value) + {:month, value}, new_date -> shift_months(new_date, value) + {:week, value}, new_date -> shift_weeks(new_date, value) + {:day, value}, new_date -> shift_days(new_date, value) + end) + end + + @doc false + defp shift_days({year, month, day}, days) do + date_to_iso_days(year, month, day) + |> Kernel.+(days) + |> date_from_iso_days() + end + + @doc false + defp shift_weeks({year, month, day}, days) do + date_to_iso_days(year, month, day) + |> Kernel.+(days * 7) + |> date_from_iso_days() + end + + @doc false + defp shift_months({year, month, day}, months) do + months_in_year = months_in_year(year) + + total_months = year * months_in_year + month + months - 1 + new_year = Integer.floor_div(total_months, months_in_year) + + new_month = + case rem(total_months, months_in_year) + 1 do + 0 -> months_in_year + new_month -> new_month + end + + new_day = min(day, days_in_month(new_year, new_month)) + + {new_year, new_month, new_day} + end + + @doc false + defp shift_years({year, _, _} = date, years) do + months_in_year = months_in_year(year) + shift_months(date, years * months_in_year) + end + ## Helpers @doc false diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index b8e490e0191..afaa1201808 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -218,7 +218,7 @@ defmodule DateTest do Date.shift(date, months: 12) end - assert_raise ArgumentError, ~s/date invalid or not supported/, fn -> + assert_raise UndefinedFunctionError, fn -> date = Calendar.Holocene.date(12000, 01, 01) Date.shift(date, month: 12) end From 8c1ff229d496e9c2af1a56e3dd1cae5449dd1c15 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 22:14:26 +0100 Subject: [PATCH 07/97] handle complex shifts around negative years --- lib/elixir/lib/calendar/date.ex | 4 +++- lib/elixir/lib/calendar/iso.ex | 33 +++++++++++++++++++-------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 88907f1fafc..5cc9a25dd18 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -763,6 +763,8 @@ defmodule Date do @doc """ Shifts a date by given list of durations according to its calendar. + Check `Calendar.ISO.shift_date/4` for more information. + ## Examples iex> Date.shift(~D[2016-01-03], month: 2) @@ -779,7 +781,7 @@ defmodule Date do def shift(%{calendar: calendar} = date, shift_units) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, shift_units) - {:ok, %Date{calendar: Calendar.ISO, year: year, month: month, day: day}} + {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} end @doc false diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 101030de1a3..abc824f97ae 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -162,9 +162,9 @@ defmodule Calendar.ISO do @type format :: :basic | :extended @typedoc """ - The shift units that can be applied when shifting dates. + The duration units that can be applied when shifting dates. """ - @type shift_unit :: + @type duration_unit :: {:year, integer()} | {:month, integer()} | {:week, integer()} @@ -1465,10 +1465,9 @@ defmodule Calendar.ISO do end @doc """ - Shifts the given date by a list of shift units. + Shifts the given date by a list of duration units and values. - Available units are: `:year, :month, :week, :day` which - are applied in that order. + Available units are: `:year, :month, :week, :day` which are applied in that order. When shifting by month: - it will shift to the current day of a month @@ -1485,12 +1484,19 @@ defmodule Calendar.ISO do iex> Calendar.ISO.shift_date(2016, 1, 31, year: 4, day: 1) {2020, 2, 1} """ - @spec shift_date(year(), month(), day(), [shift_unit()]) :: {year, month, day} + @spec shift_date(year(), month(), day(), [duration_unit()]) :: {year, month, day} @impl true - def shift_date(year, month, day, shift_units) do - shift_units = Keyword.validate!(shift_units, year: 0, month: 0, week: 0, day: 0) + def shift_date(year, month, day, duration) do + duration = Keyword.validate!(duration, year: 0, month: 0, week: 0, day: 0) - Enum.reduce(shift_units, {year, month, day}, fn + duration_sorted = [ + year: duration[:year], + month: duration[:month], + week: duration[:week], + day: duration[:day] + ] + + Enum.reduce(duration_sorted, {year, month, day}, fn {_opt, 0}, new_date -> new_date {:year, value}, new_date -> shift_years(new_date, value) {:month, value}, new_date -> shift_months(new_date, value) @@ -1507,22 +1513,21 @@ defmodule Calendar.ISO do end @doc false - defp shift_weeks({year, month, day}, days) do - date_to_iso_days(year, month, day) - |> Kernel.+(days * 7) - |> date_from_iso_days() + defp shift_weeks(date, days) do + shift_days(date, days * 7) end @doc false defp shift_months({year, month, day}, months) do months_in_year = months_in_year(year) - total_months = year * months_in_year + month + months - 1 + new_year = Integer.floor_div(total_months, months_in_year) new_month = case rem(total_months, months_in_year) + 1 do 0 -> months_in_year + new_month when new_month < 1 -> new_month + months_in_year new_month -> new_month end From 34803b92216a6f5efa65b505ecdd5b4b532e5327 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 2 Mar 2024 22:14:42 +0100 Subject: [PATCH 08/97] more tests --- lib/elixir/test/elixir/calendar/date_test.exs | 43 ++++++------------- lib/elixir/test/elixir/calendar/iso_test.exs | 36 ++++++++++++++++ 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index afaa1201808..f5b73dd108e 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -181,37 +181,18 @@ defmodule DateTest do end test "shift/2" do - date1 = ~D[2012-02-29] - assert Date.shift(date1, day: -1) == {:ok, ~D[2012-02-28]} - assert Date.shift(date1, day: 1) == {:ok, ~D[2012-03-01]} - assert Date.shift(date1, month: -1) == {:ok, ~D[2012-01-29]} - assert Date.shift(date1, month: -2) == {:ok, ~D[2011-12-29]} - assert Date.shift(date1, week: -1) == {:ok, ~D[2012-02-22]} - assert Date.shift(date1, week: -9) == {:ok, ~D[2011-12-28]} - assert Date.shift(date1, month: 1) == {:ok, ~D[2012-03-29]} - assert Date.shift(date1, year: -1) == {:ok, ~D[2011-02-28]} - assert Date.shift(date1, year: 1) == {:ok, ~D[2013-02-28]} - assert Date.shift(date1, year: 4) == {:ok, ~D[2016-02-29]} - - assert Date.shift(date1, year: 1, day: 2) == {:ok, ~D[2013-03-02]} - assert Date.shift(date1, day: -2, year: -1, month: 24) == {:ok, ~D[2013-02-26]} - assert Date.shift(date1, day: 2, year: 4, month: 1) == {:ok, ~D[2016-03-31]} - assert Date.shift(date1, year: 1) == Date.shift(date1, month: 12) - - date2 = ~D[0000-01-01] - assert Date.shift(date2, day: -1) == {:ok, ~D[-0001-12-31]} - assert Date.shift(date2, day: 1) == {:ok, ~D[0000-01-02]} - assert Date.shift(date2, month: -1) == {:ok, ~D[-0001-12-01]} - assert Date.shift(date2, month: 1) == {:ok, ~D[0000-02-01]} - assert Date.shift(date2, year: -1) == {:ok, ~D[-0001-01-01]} - assert Date.shift(date2, year: 1) == {:ok, ~D[0001-01-01]} - assert Date.shift(date2, day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} - - date3 = ~D[2000-01-01] - assert Date.shift(date3, month: 12) == {:ok, ~D[2001-01-01]} - assert Date.shift(date3, month: 14) == {:ok, ~D[2001-03-01]} - assert Date.shift(date3, month: -32) == {:ok, ~D[1997-05-01]} - assert Date.shift(date3, day: 2, year: 4, month: 1) == {:ok, ~D[2004-02-03]} + assert Date.shift(~D[2012-02-29], day: -1) == {:ok, ~D[2012-02-28]} + assert Date.shift(~D[2012-02-29], month: -1) == {:ok, ~D[2012-01-29]} + assert Date.shift(~D[2012-02-29], week: -9) == {:ok, ~D[2011-12-28]} + assert Date.shift(~D[2012-02-29], month: 1) == {:ok, ~D[2012-03-29]} + assert Date.shift(~D[2012-02-29], year: -1) == {:ok, ~D[2011-02-28]} + assert Date.shift(~D[2012-02-29], year: 4) == {:ok, ~D[2016-02-29]} + assert Date.shift(~D[0000-01-01], day: -1) == {:ok, ~D[-0001-12-31]} + assert Date.shift(~D[0000-01-01], month: -1) == {:ok, ~D[-0001-12-01]} + assert Date.shift(~D[0000-01-01], year: -1) == {:ok, ~D[-0001-01-01]} + assert Date.shift(~D[0000-01-01], year: -1) == {:ok, ~D[-0001-01-01]} + assert Date.shift(~D[2000-01-01], month: 12) == {:ok, ~D[2001-01-01]} + assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} assert_raise ArgumentError, fn -> date = ~D[2000-01-01] diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 87baebc7d25..ee6bea48bb5 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -428,4 +428,40 @@ defmodule Calendar.ISOTest do {:error, :invalid_format} end end + + describe "shift_date/2" do + test "regular use" do + assert Calendar.ISO.shift_date(2024, 3, 2, []) == {2024, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, year: 1) == {2025, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, month: 2) == {2024, 5, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, week: 3) == {2024, 3, 23} + assert Calendar.ISO.shift_date(2024, 3, 2, day: 5) == {2024, 3, 7} + assert Calendar.ISO.shift_date(0, 1, 1, month: 1) == {0, 2, 1} + assert Calendar.ISO.shift_date(0, 1, 1, year: 1) == {1, 1, 1} + assert Calendar.ISO.shift_date(0, 1, 1, year: -2, month: 2) == {-2, 3, 1} + assert Calendar.ISO.shift_date(-4, 1, 1, year: -1) == {-5, 1, 1} + + assert Calendar.ISO.shift_date(2024, 3, 2, year: 1, month: 2, week: 3, day: 5) == + {2025, 5, 28} + + assert Calendar.ISO.shift_date(2024, 3, 2, year: -1, month: -2, week: -3) == + {2022, 12, 12} + end + + test "leap year" do + assert Calendar.ISO.shift_date(2020, 2, 28, day: 1) == {2020, 2, 29} + assert Calendar.ISO.shift_date(2020, 2, 29, year: 1) == {2021, 2, 28} + assert Calendar.ISO.shift_date(2024, 3, 31, month: -1) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 3, 31, month: -2) == {2024, 1, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 1) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 2) == {2024, 3, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 3) == {2024, 4, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 4) == {2024, 5, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 5) == {2024, 6, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 6) == {2024, 7, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 7) == {2024, 8, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 8) == {2024, 9, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, month: 9) == {2024, 10, 31} + end + end end From f69cf9de5e3bcb29a2ee1d0e090f0f6dc5f60912 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 3 Mar 2024 22:39:47 +0100 Subject: [PATCH 09/97] Introduce bare Calendar.Duration --- lib/elixir/lib/calendar.ex | 10 +- lib/elixir/lib/calendar/date.ex | 10 +- lib/elixir/lib/calendar/duration.ex | 55 ++++++++ lib/elixir/lib/calendar/iso.ex | 117 ++++++++++++------ lib/elixir/lib/calendar/naive_datetime.ex | 60 +++++++++ lib/elixir/test/elixir/calendar/date_test.exs | 1 + lib/elixir/test/elixir/calendar/iso_test.exs | 6 + 7 files changed, 216 insertions(+), 43 deletions(-) create mode 100644 lib/elixir/lib/calendar/duration.ex diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 98e4ec9e45f..40857e01bd5 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -341,7 +341,15 @@ defmodule Calendar do @doc """ Shifts date by given duration according to its calendar. """ - @callback shift_date(year(), month(), day(), keyword()) :: {year, month, day} + @callback shift_date(year, month, day, [Calendar.Duration.duration_unit()]) :: + {year, month, day} + + @doc """ + Shifts naive datetime by given duration according to its calendar. + """ + @callback shift_naive_datetime(year, month, day, hour, minute, second, microsecond, [ + Calendar.Duration.duration_unit() + ]) :: {year, month, day, hour, minute, second, microsecond} # General Helpers diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 5cc9a25dd18..350e42713a0 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -761,7 +761,7 @@ defmodule Date do end @doc """ - Shifts a date by given list of durations according to its calendar. + Shifts a date by given Calendar.Duration according to its calendar. Check `Calendar.ISO.shift_date/4` for more information. @@ -777,10 +777,12 @@ defmodule Date do {:ok, ~D[2020-02-01]} """ - @spec shift(Calendar.date(), keyword()) :: {:ok, t} - def shift(%{calendar: calendar} = date, shift_units) do + @spec shift(Calendar.date(), [Calendar.Duration.duration_units()]) :: {:ok, t} + def shift(%{calendar: calendar} = date, duration_units) do + duration = Calendar.Duration.sorted!(duration_units) + %{year: year, month: month, day: day} = date - {year, month, day} = calendar.shift_date(year, month, day, shift_units) + {year, month, day} = calendar.shift_date(year, month, day, duration) {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} end diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex new file mode 100644 index 00000000000..f6d8daca1ec --- /dev/null +++ b/lib/elixir/lib/calendar/duration.ex @@ -0,0 +1,55 @@ +defmodule Calendar.Duration do + @moduledoc """ + The Duration type following ISO 8601. + + https://en.wikipedia.org/wiki/ISO_8601#Durations + + TODO: + - Implement parser + - Implement ~P Sigil + - Implement utility functions + - Implement arithmetic functions + """ + + defstruct year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0 + + @typedoc "Duration in calendar units" + @type t :: %__MODULE__{ + year: integer(), + month: integer(), + week: integer(), + day: integer(), + hour: integer(), + minute: integer(), + second: integer(), + microsecond: integer() + } + + @typedoc "Individually valid Duration units" + @type duration_unit :: + {:year, integer()} + | {:month, integer()} + | {:week, integer()} + | {:day, integer()} + | {:hour, integer()} + | {:minute, integer()} + | {:second, integer()} + | {:microsecond, integer()} + + @spec sorted!([duration_unit()]) :: [duration_unit()] + def sorted!(duration_units) do + Keyword.validate!(duration_units, Map.keys(%__MODULE__{}) -- [:__struct__]) + duration = struct!(__MODULE__, duration_units) + + [ + year: duration.year, + month: duration.month, + week: duration.week, + day: duration.day, + hour: duration.hour, + minute: duration.minute, + second: duration.second, + microsecond: duration.microsecond + ] + end +end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index abc824f97ae..7d0d3fcef52 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -161,15 +161,6 @@ defmodule Calendar.ISO do @type utc_offset :: integer @type format :: :basic | :extended - @typedoc """ - The duration units that can be applied when shifting dates. - """ - @type duration_unit :: - {:year, integer()} - | {:month, integer()} - | {:week, integer()} - | {:day, integer()} - @typedoc """ Microseconds with stored precision. @@ -1465,9 +1456,9 @@ defmodule Calendar.ISO do end @doc """ - Shifts the given date by a list of duration units and values. + Shifts date by Calendar.Duration units according to its calendar. - Available units are: `:year, :month, :week, :day` which are applied in that order. + Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. When shifting by month: - it will shift to the current day of a month @@ -1484,41 +1475,90 @@ defmodule Calendar.ISO do iex> Calendar.ISO.shift_date(2016, 1, 31, year: 4, day: 1) {2020, 2, 1} """ - @spec shift_date(year(), month(), day(), [duration_unit()]) :: {year, month, day} + @spec shift_date(year(), month(), day(), [Calendar.Duration.duration_unit()]) :: + {year, month, day} + @impl true + def shift_date(year, month, day, duration_units) do + {year, month, day, _, _, _, _} = + shift_naive_datetime(year, month, day, 0, 0, 0, {0, 0}, duration_units) + + {year, month, day} + end + + @doc """ + Shifts naive datetime by Calendar.Duration units according to its calendar. + + Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. + + When shifting by month: + - it will shift to the current day of a month + - when the current day does not exist in a month, it will shift to the last day of a month + + ## Examples + + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, hour: 1) + {2016, 1, 3, 1, 0, 0, {0, 0}} + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, hour: 30) + {2016, 1, 4, 6, 0, 0, {0, 0}} + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, microsecond: 100) + {2016, 1, 3, 0, 0, 0, {100, 6}} + """ + @spec shift_naive_datetime(year, month, day, hour, minute, second, microsecond, [ + Calendar.Duration.duration_unit() + ]) :: {year, month, day, hour, minute, second, microsecond} @impl true - def shift_date(year, month, day, duration) do - duration = Keyword.validate!(duration, year: 0, month: 0, week: 0, day: 0) - - duration_sorted = [ - year: duration[:year], - month: duration[:month], - week: duration[:week], - day: duration[:day] - ] - - Enum.reduce(duration_sorted, {year, month, day}, fn - {_opt, 0}, new_date -> new_date - {:year, value}, new_date -> shift_years(new_date, value) - {:month, value}, new_date -> shift_months(new_date, value) - {:week, value}, new_date -> shift_weeks(new_date, value) - {:day, value}, new_date -> shift_days(new_date, value) + def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration_units) do + Enum.reduce(duration_units, {year, month, day, hour, minute, second, microsecond}, fn + {_opt, 0}, naive_datetime -> naive_datetime + {:year, value}, naive_datetime -> shift_years(naive_datetime, value) + {:month, value}, naive_datetime -> shift_months(naive_datetime, value) + {:week, value}, naive_datetime -> shift_weeks(naive_datetime, value) + {time_unit, value}, naive_datetime -> shift_numerical(naive_datetime, value, time_unit) end) end + def shift_numerical(naive_datetime, value, :day) do + shift_time_unit(naive_datetime, value * 86400, :second) + end + + def shift_numerical(naive_datetime, value, :hour) do + shift_time_unit(naive_datetime, value * 3600, :second) + end + + def shift_numerical(naive_datetime, value, :minute) do + shift_time_unit(naive_datetime, value * 60, :second) + end + + def shift_numerical(naive_datetime, value, unit) do + shift_time_unit(naive_datetime, value, unit) + end + + defp shift_time_unit(naive_datetime, value, unit) do + {year, month, day, hour, minute, second, {_, ms_precision} = microsecond} = naive_datetime + + ppd = System.convert_time_unit(86400, :second, unit) + precision = max(time_unit_to_precision(unit), ms_precision) + + {year, month, day, hour, minute, second, {ms_value, _}} = + naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) + |> add_day_fraction_to_iso_days(value, ppd) + |> naive_datetime_from_iso_days() + + {year, month, day, hour, minute, second, {ms_value, precision}} + end + @doc false - defp shift_days({year, month, day}, days) do - date_to_iso_days(year, month, day) - |> Kernel.+(days) - |> date_from_iso_days() + defp shift_days(naive_datetime, days) do + shift_time_unit(naive_datetime, days * 86400, :second) end @doc false - defp shift_weeks(date, days) do - shift_days(date, days * 7) + defp shift_weeks(naive_datetime, days) do + shift_days(naive_datetime, days * 7) end @doc false - defp shift_months({year, month, day}, months) do + defp shift_months({year, month, day, hour, minute, second, microsecond}, months) do months_in_year = months_in_year(year) total_months = year * months_in_year + month + months - 1 @@ -1533,13 +1573,14 @@ defmodule Calendar.ISO do new_day = min(day, days_in_month(new_year, new_month)) - {new_year, new_month, new_day} + {new_year, new_month, new_day, hour, minute, second, microsecond} end @doc false - defp shift_years({year, _, _} = date, years) do + defp shift_years(naive_datetime, years) do + year = elem(naive_datetime, 0) months_in_year = months_in_year(year) - shift_months(date, years * months_in_year) + shift_months(naive_datetime, years * months_in_year) end ## Helpers diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index fe6674679a3..7483544ef52 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -571,6 +571,66 @@ defmodule NaiveDateTime do units1 - units2 end + @doc """ + Shifts a naive datetime by given Calendar.Duration according to its calendar. + + Check `Calendar.ISO.shift_naive_datetime/8` for more information. + + ## Examples + + iex> NaiveDateTime.shift(~N[2016-01-03 00:00:00], month: 2) + {:ok, ~N[2016-03-03 00:00:00]} + iex> NaiveDateTime.shift(~N[2016-02-29 00:00:00], month: 1) + {:ok, ~N[2016-03-29 00:00:00]} + iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) + {:ok, ~N[2016-02-29 00:00:00]} + iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1) + {:ok, ~N[2020-02-01 00:00:00]} + iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45) + {:ok, ~N[2016-01-31 00:00:45]} + iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: 100) + {:ok, ~N[2016-01-31 00:00:00.000100]} + + """ + @spec shift(Calendar.date(), [Calendar.Duration.duration_units()]) :: {:ok, t} + def shift(%{calendar: calendar} = date, duration_units) do + duration = Calendar.Duration.sorted!(duration_units) + + %{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + } = date + + {year, month, day, hour, minute, second, microsecond} = + calendar.shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + duration + ) + + {:ok, + %NaiveDateTime{ + calendar: calendar, + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }} + end + @doc """ Returns the given naive datetime with the microsecond field truncated to the given precision (`:microsecond`, `:millisecond` or `:second`). diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index f5b73dd108e..abfe5296ce5 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -182,6 +182,7 @@ defmodule DateTest do test "shift/2" do assert Date.shift(~D[2012-02-29], day: -1) == {:ok, ~D[2012-02-28]} + assert Date.shift(~D[2012-02-29], second: 86400) == {:ok, ~D[2012-03-01]} assert Date.shift(~D[2012-02-29], month: -1) == {:ok, ~D[2012-01-29]} assert Date.shift(~D[2012-02-29], week: -9) == {:ok, ~D[2011-12-28]} assert Date.shift(~D[2012-02-29], month: 1) == {:ok, ~D[2012-03-29]} diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index ee6bea48bb5..2fb3bd5fe71 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -436,6 +436,12 @@ defmodule Calendar.ISOTest do assert Calendar.ISO.shift_date(2024, 3, 2, month: 2) == {2024, 5, 2} assert Calendar.ISO.shift_date(2024, 3, 2, week: 3) == {2024, 3, 23} assert Calendar.ISO.shift_date(2024, 3, 2, day: 5) == {2024, 3, 7} + + assert Calendar.ISO.shift_date(2024, 3, 2, hour: 24) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, minute: 1440) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, second: 86400) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, microsecond: 86400 * 1_000_000) == {2024, 3, 3} + assert Calendar.ISO.shift_date(0, 1, 1, month: 1) == {0, 2, 1} assert Calendar.ISO.shift_date(0, 1, 1, year: 1) == {1, 1, 1} assert Calendar.ISO.shift_date(0, 1, 1, year: -2, month: 2) == {-2, 3, 1} From cf41c6629e3a7e6328476f12e14e2f2e2f3b0f36 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 4 Mar 2024 10:34:33 +0100 Subject: [PATCH 10/97] build shift options from Duration.t() in calendar --- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/duration.ex | 9 ++- lib/elixir/lib/calendar/iso.ex | 39 +++++++----- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- lib/elixir/test/elixir/calendar/iso_test.exs | 62 ++++++++++---------- 5 files changed, 63 insertions(+), 51 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 350e42713a0..848fa8e0aa1 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -779,7 +779,7 @@ defmodule Date do """ @spec shift(Calendar.date(), [Calendar.Duration.duration_units()]) :: {:ok, t} def shift(%{calendar: calendar} = date, duration_units) do - duration = Calendar.Duration.sorted!(duration_units) + duration = Calendar.Duration.new!(duration_units) %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index f6d8daca1ec..a736a3918f2 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -36,11 +36,14 @@ defmodule Calendar.Duration do | {:second, integer()} | {:microsecond, integer()} - @spec sorted!([duration_unit()]) :: [duration_unit()] - def sorted!(duration_units) do + @spec new!([duration_unit]) :: t() + def new!(duration_units) do Keyword.validate!(duration_units, Map.keys(%__MODULE__{}) -- [:__struct__]) - duration = struct!(__MODULE__, duration_units) + struct!(__MODULE__, duration_units) + end + @spec to_shift_options(t()) :: [duration_unit()] + def to_shift_options(duration) do [ year: duration.year, month: duration.month, diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 7d0d3fcef52..cfb4a364c1b 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1466,21 +1466,21 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_date(2016, 1, 3, month: 2) + iex> Calendar.ISO.shift_date(2016, 1, 3, Calendar.Duration.new!(month: 2)) {2016, 3, 3} - iex> Calendar.ISO.shift_date(2016, 2, 29, month: 1) + iex> Calendar.ISO.shift_date(2016, 2, 29, Calendar.Duration.new!(month: 1)) {2016, 3, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, month: 1) + iex> Calendar.ISO.shift_date(2016, 1, 31, Calendar.Duration.new!(month: 1)) {2016, 2, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, year: 4, day: 1) + iex> Calendar.ISO.shift_date(2016, 1, 31, Calendar.Duration.new!(year: 4, day: 1)) {2020, 2, 1} """ - @spec shift_date(year(), month(), day(), [Calendar.Duration.duration_unit()]) :: + @spec shift_date(year(), month(), day(), Calendar.Duration.t()) :: {year, month, day} @impl true - def shift_date(year, month, day, duration_units) do + def shift_date(year, month, day, duration) do {year, month, day, _, _, _, _} = - shift_naive_datetime(year, month, day, 0, 0, 0, {0, 0}, duration_units) + shift_naive_datetime(year, month, day, 0, 0, 0, {0, 0}, duration) {year, month, day} end @@ -1496,19 +1496,28 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, hour: 1) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Calendar.Duration.new!(hour: 1)) {2016, 1, 3, 1, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, hour: 30) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Calendar.Duration.new!(hour: 30)) {2016, 1, 4, 6, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, microsecond: 100) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Calendar.Duration.new!(microsecond: 100)) {2016, 1, 3, 0, 0, 0, {100, 6}} """ - @spec shift_naive_datetime(year, month, day, hour, minute, second, microsecond, [ - Calendar.Duration.duration_unit() - ]) :: {year, month, day, hour, minute, second, microsecond} + @spec shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + Calendar.Duration.t() + ) :: {year, month, day, hour, minute, second, microsecond} @impl true - def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration_units) do - Enum.reduce(duration_units, {year, month, day, hour, minute, second, microsecond}, fn + def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do + shift_options = Calendar.Duration.to_shift_options(duration) + + Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn {_opt, 0}, naive_datetime -> naive_datetime {:year, value}, naive_datetime -> shift_years(naive_datetime, value) {:month, value}, naive_datetime -> shift_months(naive_datetime, value) diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 7483544ef52..4c7545e550d 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -594,7 +594,7 @@ defmodule NaiveDateTime do """ @spec shift(Calendar.date(), [Calendar.Duration.duration_units()]) :: {:ok, t} def shift(%{calendar: calendar} = date, duration_units) do - duration = Calendar.Duration.sorted!(duration_units) + duration = Calendar.Duration.new!(duration_units) %{ year: year, diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 2fb3bd5fe71..18084d3b20a 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -431,43 +431,43 @@ defmodule Calendar.ISOTest do describe "shift_date/2" do test "regular use" do - assert Calendar.ISO.shift_date(2024, 3, 2, []) == {2024, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, year: 1) == {2025, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, month: 2) == {2024, 5, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, week: 3) == {2024, 3, 23} - assert Calendar.ISO.shift_date(2024, 3, 2, day: 5) == {2024, 3, 7} - - assert Calendar.ISO.shift_date(2024, 3, 2, hour: 24) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, minute: 1440) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, second: 86400) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, microsecond: 86400 * 1_000_000) == {2024, 3, 3} - - assert Calendar.ISO.shift_date(0, 1, 1, month: 1) == {0, 2, 1} - assert Calendar.ISO.shift_date(0, 1, 1, year: 1) == {1, 1, 1} - assert Calendar.ISO.shift_date(0, 1, 1, year: -2, month: 2) == {-2, 3, 1} - assert Calendar.ISO.shift_date(-4, 1, 1, year: -1) == {-5, 1, 1} - - assert Calendar.ISO.shift_date(2024, 3, 2, year: 1, month: 2, week: 3, day: 5) == + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!([])) == {2024, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(year: 1)) == {2025, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(month: 2)) == {2024, 5, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(week: 3)) == {2024, 3, 23} + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(day: 5)) == {2024, 3, 7} + + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(hour: 24)) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(minute: 1440)) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(second: 86400)) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(microsecond: 86400 * 1_000_000)) == {2024, 3, 3} + + assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(month: 1)) == {0, 2, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(year: 1)) == {1, 1, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(year: -2, month: 2)) == {-2, 3, 1} + assert Calendar.ISO.shift_date(-4, 1, 1, Calendar.Duration.new!(year: -1)) == {-5, 1, 1} + + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(year: 1, month: 2, week: 3, day: 5)) == {2025, 5, 28} - assert Calendar.ISO.shift_date(2024, 3, 2, year: -1, month: -2, week: -3) == + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(year: -1, month: -2, week: -3)) == {2022, 12, 12} end test "leap year" do - assert Calendar.ISO.shift_date(2020, 2, 28, day: 1) == {2020, 2, 29} - assert Calendar.ISO.shift_date(2020, 2, 29, year: 1) == {2021, 2, 28} - assert Calendar.ISO.shift_date(2024, 3, 31, month: -1) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 3, 31, month: -2) == {2024, 1, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 1) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 2) == {2024, 3, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 3) == {2024, 4, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 4) == {2024, 5, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 5) == {2024, 6, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 6) == {2024, 7, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 7) == {2024, 8, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 8) == {2024, 9, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, month: 9) == {2024, 10, 31} + assert Calendar.ISO.shift_date(2020, 2, 28, Calendar.Duration.new!(day: 1)) == {2020, 2, 29} + assert Calendar.ISO.shift_date(2020, 2, 29, Calendar.Duration.new!(year: 1)) == {2021, 2, 28} + assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -2)) == {2024, 1, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 2)) == {2024, 3, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 3)) == {2024, 4, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 4)) == {2024, 5, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 5)) == {2024, 6, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 6)) == {2024, 7, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 7)) == {2024, 8, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 8)) == {2024, 9, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 9)) == {2024, 10, 31} end end end From 54c15b7abed58a78a94ef52414a5a5cc28e9422e Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 4 Mar 2024 10:53:00 +0100 Subject: [PATCH 11/97] cleanup iso shift helpers --- lib/elixir/lib/calendar/iso.ex | 32 ++------ lib/elixir/test/elixir/calendar/iso_test.exs | 83 +++++++++++++++----- 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index cfb4a364c1b..1020d1da4b1 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1519,26 +1519,26 @@ defmodule Calendar.ISO do Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn {_opt, 0}, naive_datetime -> naive_datetime - {:year, value}, naive_datetime -> shift_years(naive_datetime, value) + {:year, value}, naive_datetime -> shift_months(naive_datetime, value * 12) {:month, value}, naive_datetime -> shift_months(naive_datetime, value) - {:week, value}, naive_datetime -> shift_weeks(naive_datetime, value) + {:week, value}, naive_datetime -> shift_numerical(naive_datetime, value * 7, :day) {time_unit, value}, naive_datetime -> shift_numerical(naive_datetime, value, time_unit) end) end - def shift_numerical(naive_datetime, value, :day) do + defp shift_numerical(naive_datetime, value, :day) do shift_time_unit(naive_datetime, value * 86400, :second) end - def shift_numerical(naive_datetime, value, :hour) do + defp shift_numerical(naive_datetime, value, :hour) do shift_time_unit(naive_datetime, value * 3600, :second) end - def shift_numerical(naive_datetime, value, :minute) do + defp shift_numerical(naive_datetime, value, :minute) do shift_time_unit(naive_datetime, value * 60, :second) end - def shift_numerical(naive_datetime, value, unit) do + defp shift_numerical(naive_datetime, value, unit) do shift_time_unit(naive_datetime, value, unit) end @@ -1556,19 +1556,8 @@ defmodule Calendar.ISO do {year, month, day, hour, minute, second, {ms_value, precision}} end - @doc false - defp shift_days(naive_datetime, days) do - shift_time_unit(naive_datetime, days * 86400, :second) - end - - @doc false - defp shift_weeks(naive_datetime, days) do - shift_days(naive_datetime, days * 7) - end - - @doc false defp shift_months({year, month, day, hour, minute, second, microsecond}, months) do - months_in_year = months_in_year(year) + months_in_year = 12 total_months = year * months_in_year + month + months - 1 new_year = Integer.floor_div(total_months, months_in_year) @@ -1585,13 +1574,6 @@ defmodule Calendar.ISO do {new_year, new_month, new_day, hour, minute, second, microsecond} end - @doc false - defp shift_years(naive_datetime, years) do - year = elem(naive_datetime, 0) - months_in_year = months_in_year(year) - shift_months(naive_datetime, years * months_in_year) - end - ## Helpers @doc false diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 18084d3b20a..fb41462fd2b 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -438,36 +438,83 @@ defmodule Calendar.ISOTest do assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(day: 5)) == {2024, 3, 7} assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(hour: 24)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(minute: 1440)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(second: 86400)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(microsecond: 86400 * 1_000_000)) == {2024, 3, 3} + + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(minute: 1440)) == + {2024, 3, 3} + + assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(second: 86400)) == + {2024, 3, 3} + + assert Calendar.ISO.shift_date( + 2024, + 3, + 2, + Calendar.Duration.new!(microsecond: 86400 * 1_000_000) + ) == {2024, 3, 3} assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(month: 1)) == {0, 2, 1} assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(year: 1)) == {1, 1, 1} - assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(year: -2, month: 2)) == {-2, 3, 1} + + assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(year: -2, month: 2)) == + {-2, 3, 1} + assert Calendar.ISO.shift_date(-4, 1, 1, Calendar.Duration.new!(year: -1)) == {-5, 1, 1} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(year: 1, month: 2, week: 3, day: 5)) == + assert Calendar.ISO.shift_date( + 2024, + 3, + 2, + Calendar.Duration.new!(year: 1, month: 2, week: 3, day: 5) + ) == {2025, 5, 28} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(year: -1, month: -2, week: -3)) == + assert Calendar.ISO.shift_date( + 2024, + 3, + 2, + Calendar.Duration.new!(year: -1, month: -2, week: -3) + ) == {2022, 12, 12} end test "leap year" do assert Calendar.ISO.shift_date(2020, 2, 28, Calendar.Duration.new!(day: 1)) == {2020, 2, 29} - assert Calendar.ISO.shift_date(2020, 2, 29, Calendar.Duration.new!(year: 1)) == {2021, 2, 28} - assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -1)) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -2)) == {2024, 1, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 1)) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 2)) == {2024, 3, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 3)) == {2024, 4, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 4)) == {2024, 5, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 5)) == {2024, 6, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 6)) == {2024, 7, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 7)) == {2024, 8, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 8)) == {2024, 9, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 9)) == {2024, 10, 31} + + assert Calendar.ISO.shift_date(2020, 2, 29, Calendar.Duration.new!(year: 1)) == + {2021, 2, 28} + + assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -1)) == + {2024, 2, 29} + + assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -2)) == + {2024, 1, 31} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 1)) == + {2024, 2, 29} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 2)) == + {2024, 3, 31} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 3)) == + {2024, 4, 30} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 4)) == + {2024, 5, 31} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 5)) == + {2024, 6, 30} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 6)) == + {2024, 7, 31} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 7)) == + {2024, 8, 31} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 8)) == + {2024, 9, 30} + + assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 9)) == + {2024, 10, 31} end end end From 489b18c56abe819eee54eaa78f2f8852bcdfdea8 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 4 Mar 2024 11:12:44 +0100 Subject: [PATCH 12/97] collapse duration time units to months, seconds and microseconds --- lib/elixir/lib/calendar/duration.ex | 14 --------- lib/elixir/lib/calendar/iso.ex | 49 ++++++++++++++++------------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index a736a3918f2..8d466889cc7 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -41,18 +41,4 @@ defmodule Calendar.Duration do Keyword.validate!(duration_units, Map.keys(%__MODULE__{}) -- [:__struct__]) struct!(__MODULE__, duration_units) end - - @spec to_shift_options(t()) :: [duration_unit()] - def to_shift_options(duration) do - [ - year: duration.year, - month: duration.month, - week: duration.week, - day: duration.day, - hour: duration.hour, - minute: duration.minute, - second: duration.second, - microsecond: duration.microsecond - ] - end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 1020d1da4b1..8c6fcbab938 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1515,34 +1515,41 @@ defmodule Calendar.ISO do ) :: {year, month, day, hour, minute, second, microsecond} @impl true def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do - shift_options = Calendar.Duration.to_shift_options(duration) + shift_options = shift_options(duration) Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn - {_opt, 0}, naive_datetime -> naive_datetime - {:year, value}, naive_datetime -> shift_months(naive_datetime, value * 12) - {:month, value}, naive_datetime -> shift_months(naive_datetime, value) - {:week, value}, naive_datetime -> shift_numerical(naive_datetime, value * 7, :day) - {time_unit, value}, naive_datetime -> shift_numerical(naive_datetime, value, time_unit) - end) - end + {_opt, 0}, naive_datetime -> + naive_datetime - defp shift_numerical(naive_datetime, value, :day) do - shift_time_unit(naive_datetime, value * 86400, :second) - end + {:month, value}, naive_datetime -> + shift_months(naive_datetime, value) - defp shift_numerical(naive_datetime, value, :hour) do - shift_time_unit(naive_datetime, value * 3600, :second) - end + {:second, value}, naive_datetime -> + shift_numerical(naive_datetime, value, :second) - defp shift_numerical(naive_datetime, value, :minute) do - shift_time_unit(naive_datetime, value * 60, :second) - end - - defp shift_numerical(naive_datetime, value, unit) do - shift_time_unit(naive_datetime, value, unit) + {:microsecond, value}, naive_datetime -> + shift_numerical(naive_datetime, value, :microsecond) + end) end - defp shift_time_unit(naive_datetime, value, unit) do + defp shift_options(%Calendar.Duration{ + year: year, + month: month, + week: week, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }) do + [ + month: year * 12 + month, + second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second, + microsecond: microsecond + ] + end + + defp shift_numerical(naive_datetime, value, unit) when unit in [:second, :microsecond] do {year, month, day, hour, minute, second, {_, ms_precision} = microsecond} = naive_datetime ppd = System.convert_time_unit(86400, :second, unit) From c91f3b6cac3fa3d4c4ce434da36c78119b16fa9e Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 4 Mar 2024 11:22:10 +0100 Subject: [PATCH 13/97] specs + iso function order --- lib/elixir/lib/calendar.ex | 15 +++++--- lib/elixir/lib/calendar/iso.ex | 62 +++++++++++++++++----------------- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 40857e01bd5..175cd72e7ca 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -341,15 +341,22 @@ defmodule Calendar do @doc """ Shifts date by given duration according to its calendar. """ - @callback shift_date(year, month, day, [Calendar.Duration.duration_unit()]) :: + @callback shift_date(year, month, day, Calendar.Duration.t()) :: {year, month, day} @doc """ Shifts naive datetime by given duration according to its calendar. """ - @callback shift_naive_datetime(year, month, day, hour, minute, second, microsecond, [ - Calendar.Duration.duration_unit() - ]) :: {year, month, day, hour, minute, second, microsecond} + @callback shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + Calendar.Duration.t() + ) :: {year, month, day, hour, minute, second, microsecond} # General Helpers diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 8c6fcbab938..5a4efdb1660 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1532,37 +1532,6 @@ defmodule Calendar.ISO do end) end - defp shift_options(%Calendar.Duration{ - year: year, - month: month, - week: week, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - }) do - [ - month: year * 12 + month, - second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second, - microsecond: microsecond - ] - end - - defp shift_numerical(naive_datetime, value, unit) when unit in [:second, :microsecond] do - {year, month, day, hour, minute, second, {_, ms_precision} = microsecond} = naive_datetime - - ppd = System.convert_time_unit(86400, :second, unit) - precision = max(time_unit_to_precision(unit), ms_precision) - - {year, month, day, hour, minute, second, {ms_value, _}} = - naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) - |> add_day_fraction_to_iso_days(value, ppd) - |> naive_datetime_from_iso_days() - - {year, month, day, hour, minute, second, {ms_value, precision}} - end - defp shift_months({year, month, day, hour, minute, second, microsecond}, months) do months_in_year = 12 total_months = year * months_in_year + month + months - 1 @@ -1581,6 +1550,37 @@ defmodule Calendar.ISO do {new_year, new_month, new_day, hour, minute, second, microsecond} end + defp shift_numerical(naive_datetime, value, unit) when unit in [:second, :microsecond] do + {year, month, day, hour, minute, second, {_, ms_precision} = microsecond} = naive_datetime + + ppd = System.convert_time_unit(86400, :second, unit) + precision = max(time_unit_to_precision(unit), ms_precision) + + {year, month, day, hour, minute, second, {ms_value, _}} = + naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) + |> add_day_fraction_to_iso_days(value, ppd) + |> naive_datetime_from_iso_days() + + {year, month, day, hour, minute, second, {ms_value, precision}} + end + + defp shift_options(%Calendar.Duration{ + year: year, + month: month, + week: week, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }) do + [ + month: year * 12 + month, + second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second, + microsecond: microsecond + ] + end + ## Helpers @doc false From b5aaa40039ad108ad20ba596b06106cc31181a00 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 4 Mar 2024 11:38:46 +0100 Subject: [PATCH 14/97] separate shift_date/4 and shift_naive_datetime/8 implementations --- lib/elixir/lib/calendar/iso.ex | 56 ++++++++++++++++++++++++++++++---- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 5a4efdb1660..3a6c74133ee 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1479,8 +1479,25 @@ defmodule Calendar.ISO do {year, month, day} @impl true def shift_date(year, month, day, duration) do + shift_options = shift_date_options(duration) + {year, month, day, _, _, _, _} = - shift_naive_datetime(year, month, day, 0, 0, 0, {0, 0}, duration) + Enum.reduce(shift_options, {year, month, day, 0, 0, 0, {0, 0}}, fn + {_opt, 0}, naive_datetime -> + naive_datetime + + {:month, value}, naive_datetime -> + shift_months(naive_datetime, value) + + {:day, value}, naive_datetime -> + shift_days(naive_datetime, value) + + {:second, value}, naive_datetime -> + shift_time_unit(naive_datetime, value, :second) + + {:microsecond, value}, naive_datetime -> + shift_time_unit(naive_datetime, value, :microsecond) + end) {year, month, day} end @@ -1515,7 +1532,7 @@ defmodule Calendar.ISO do ) :: {year, month, day, hour, minute, second, microsecond} @impl true def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do - shift_options = shift_options(duration) + shift_options = shift_naive_datetime_options(duration) Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn {_opt, 0}, naive_datetime -> @@ -1525,13 +1542,22 @@ defmodule Calendar.ISO do shift_months(naive_datetime, value) {:second, value}, naive_datetime -> - shift_numerical(naive_datetime, value, :second) + shift_time_unit(naive_datetime, value, :second) {:microsecond, value}, naive_datetime -> - shift_numerical(naive_datetime, value, :microsecond) + shift_time_unit(naive_datetime, value, :microsecond) end) end + defp shift_days({year, month, day, hour, minute, second, microsecond}, days) do + {year, month, day} = + date_to_iso_days(year, month, day) + |> Kernel.+(days) + |> date_from_iso_days() + + {year, month, day, hour, minute, second, microsecond} + end + defp shift_months({year, month, day, hour, minute, second, microsecond}, months) do months_in_year = 12 total_months = year * months_in_year + month + months - 1 @@ -1550,7 +1576,7 @@ defmodule Calendar.ISO do {new_year, new_month, new_day, hour, minute, second, microsecond} end - defp shift_numerical(naive_datetime, value, unit) when unit in [:second, :microsecond] do + defp shift_time_unit(naive_datetime, value, unit) when unit in [:second, :microsecond] do {year, month, day, hour, minute, second, {_, ms_precision} = microsecond} = naive_datetime ppd = System.convert_time_unit(86400, :second, unit) @@ -1564,7 +1590,7 @@ defmodule Calendar.ISO do {year, month, day, hour, minute, second, {ms_value, precision}} end - defp shift_options(%Calendar.Duration{ + defp shift_naive_datetime_options(%Calendar.Duration{ year: year, month: month, week: week, @@ -1581,6 +1607,24 @@ defmodule Calendar.ISO do ] end + defp shift_date_options(%Calendar.Duration{ + year: year, + month: month, + week: week, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }) do + [ + month: year * 12 + month, + day: week * 7 + day, + second: hour * 3600 + minute * 60 + second, + microsecond: microsecond + ] + end + ## Helpers @doc false From 4580d21ac8527682738a2d3c1d4aa5b19ae727da Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 4 Mar 2024 12:43:03 +0100 Subject: [PATCH 15/97] add tests --- lib/elixir/lib/calendar/iso.ex | 4 +- lib/elixir/test/elixir/calendar/iso_test.exs | 127 ++++++++++++++++++ .../elixir/calendar/naive_datetime_test.exs | 58 ++++++++ 3 files changed, 187 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 3a6c74133ee..3e0114b67b6 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1483,7 +1483,7 @@ defmodule Calendar.ISO do {year, month, day, _, _, _, _} = Enum.reduce(shift_options, {year, month, day, 0, 0, 0, {0, 0}}, fn - {_opt, 0}, naive_datetime -> + {_, 0}, naive_datetime -> naive_datetime {:month, value}, naive_datetime -> @@ -1535,7 +1535,7 @@ defmodule Calendar.ISO do shift_options = shift_naive_datetime_options(duration) Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn - {_opt, 0}, naive_datetime -> + {_, 0}, naive_datetime -> naive_datetime {:month, value}, naive_datetime -> diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index fb41462fd2b..c8959e610a0 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -517,4 +517,131 @@ defmodule Calendar.ISOTest do {2024, 10, 31} end end + + describe "shift_naive_datetime/2" do + test "regular use" do + assert Calendar.ISO.shift_naive_datetime( + 2024, + 3, + 2, + 0, + 0, + 0, + {0, 0}, + Calendar.Duration.new!([]) + ) == {2024, 3, 2, 0, 0, 0, {0, 0}} + end + + test "leap year" do + assert Calendar.ISO.shift_naive_datetime( + 2000, + 1, + 1, + 0, + 0, + 0, + {0, 0}, + Calendar.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}, + Calendar.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}, + Calendar.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}, + Calendar.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}, + Calendar.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}, + Calendar.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}, + Calendar.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}, + Calendar.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}, + Calendar.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}, + Calendar.Duration.new!(month: -1, day: -29) + ) == {1999, 12, 31, 0, 0, 0, {0, 0}} + end + end end diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index df37de1007f..34d6b591c6f 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -391,4 +391,62 @@ defmodule NaiveDateTimeTest do assert NaiveDateTime.end_of_day(~N[2000-01-01 23:00:07]) == ~N[2000-01-01 23:59:59] end end + + describe "shift/2" do + naive_datetime = ~N[2000-01-01 00:00:00] + assert NaiveDateTime.shift(naive_datetime, year: 1) == {:ok, ~N[2001-01-01 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, month: 1) == {:ok, ~N[2000-02-01 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, week: 3) == {:ok, ~N[2000-01-22 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, day: 2) == {:ok, ~N[2000-01-03 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, hour: 6) == {:ok, ~N[2000-01-01 06:00:00]} + assert NaiveDateTime.shift(naive_datetime, minute: 30) == {:ok, ~N[2000-01-01 00:30:00]} + assert NaiveDateTime.shift(naive_datetime, second: 45) == {:ok, ~N[2000-01-01 00:00:45]} + + assert NaiveDateTime.shift(naive_datetime, year: -1) == {:ok, ~N[1999-01-01 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, month: -1) == {:ok, ~N[1999-12-01 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, week: -1) == {:ok, ~N[1999-12-25 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, day: -1) == {:ok, ~N[1999-12-31 00:00:00]} + assert NaiveDateTime.shift(naive_datetime, hour: -12) == {:ok, ~N[1999-12-31 12:00:00]} + assert NaiveDateTime.shift(naive_datetime, minute: -45) == {:ok, ~N[1999-12-31 23:15:00]} + assert NaiveDateTime.shift(naive_datetime, second: -30) == {:ok, ~N[1999-12-31 23:59:30]} + + assert NaiveDateTime.shift(naive_datetime, microsecond: -500) == + {:ok, ~N[1999-12-31 23:59:59.999500]} + + assert NaiveDateTime.shift(naive_datetime, microsecond: 500) == + {:ok, ~N[2000-01-01 00:00:00.000500]} + + assert NaiveDateTime.shift(naive_datetime, year: 1, month: 2) == + {:ok, ~N[2001-03-01 00:00:00]} + + assert NaiveDateTime.shift(naive_datetime, month: 2, day: 3, hour: 6, minute: 15) == + {:ok, ~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 + ) == {:ok, ~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 + ) == {:ok, ~N[1998-10-06 18:53:52.999992]} + + assert_raise ArgumentError, fn -> + naive_datetime = naive_datetime + NaiveDateTime.shift(naive_datetime, months: 12) + end + end end From d81a809c998cd531b13e5e3c45cbf8c417ebcfaa Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 4 Mar 2024 15:57:23 +0100 Subject: [PATCH 16/97] add sigil_P --- lib/elixir/lib/calendar/date.ex | 12 +- lib/elixir/lib/calendar/duration.ex | 33 ++++- lib/elixir/lib/calendar/naive_datetime.ex | 16 ++- lib/elixir/lib/kernel.ex | 24 ++++ lib/elixir/test/elixir/calendar/date_test.exs | 2 + .../test/elixir/calendar/duration_test.exs | 134 ++++++++++++++++++ .../elixir/calendar/naive_datetime_test.exs | 3 + 7 files changed, 213 insertions(+), 11 deletions(-) create mode 100644 lib/elixir/test/elixir/calendar/duration_test.exs diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 848fa8e0aa1..955675ea72c 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -777,15 +777,19 @@ defmodule Date do {:ok, ~D[2020-02-01]} """ - @spec shift(Calendar.date(), [Calendar.Duration.duration_units()]) :: {:ok, t} - def shift(%{calendar: calendar} = date, duration_units) do - duration = Calendar.Duration.new!(duration_units) - + @spec shift(Calendar.date(), Calendar.Duration.t() | [Calendar.Duration.duration_units()]) :: + {:ok, t} + def shift(%Date{calendar: calendar} = date, %Calendar.Duration{} = duration) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} end + def shift(%Date{} = date = date, duration_units) do + duration = Calendar.Duration.new!(duration_units) + shift(date, duration) + 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}} diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 8d466889cc7..4fca4c2dea0 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -5,8 +5,6 @@ defmodule Calendar.Duration do https://en.wikipedia.org/wiki/ISO_8601#Durations TODO: - - Implement parser - - Implement ~P Sigil - Implement utility functions - Implement arithmetic functions """ @@ -41,4 +39,35 @@ defmodule Calendar.Duration do Keyword.validate!(duration_units, Map.keys(%__MODULE__{}) -- [:__struct__]) struct!(__MODULE__, duration_units) end + + @duration_regex ~r/P(?:(?-?\d+)Y)?(?:(?-?\d+)M)?(?:(?-?\d+)W)?(?:(?-?\d+)D)?(?:T(?:(?-?\d+)H)?(?:(?-?\d+)M)?(?:(?-?\d+)S)?)?/ + @spec parse!(String.t()) :: t() + def parse!(duration_string) when is_binary(duration_string) do + case Regex.named_captures(@duration_regex, duration_string) do + %{ + "year" => year, + "month" => month, + "week" => week, + "day" => day, + "hour" => hour, + "minute" => minute, + "second" => second + } -> + new!( + year: parse_unit(year), + month: parse_unit(month), + week: parse_unit(week), + day: parse_unit(day), + hour: parse_unit(hour), + minute: parse_unit(minute), + second: parse_unit(second) + ) + + _ -> + raise ArgumentError, "invalid duration string" + end + end + + defp parse_unit(""), do: 0 + defp parse_unit(part), do: String.to_integer(part) end diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 4c7545e550d..2afb6e3bb7e 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -592,10 +592,11 @@ defmodule NaiveDateTime do {:ok, ~N[2016-01-31 00:00:00.000100]} """ - @spec shift(Calendar.date(), [Calendar.Duration.duration_units()]) :: {:ok, t} - def shift(%{calendar: calendar} = date, duration_units) do - duration = Calendar.Duration.new!(duration_units) - + @spec shift( + Calendar.naive_datetime(), + Calendar.Duration.t() | [Calendar.Duration.duration_units()] + ) :: {:ok, t} + def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Calendar.Duration{} = duration) do %{ year: year, month: month, @@ -604,7 +605,7 @@ defmodule NaiveDateTime do minute: minute, second: second, microsecond: microsecond - } = date + } = naive_datetime {year, month, day, hour, minute, second, microsecond} = calendar.shift_naive_datetime( @@ -631,6 +632,11 @@ defmodule NaiveDateTime do }} end + def shift(%NaiveDateTime{} = naive_datetime, duration_units) do + duration = Calendar.Duration.new!(duration_units) + shift(naive_datetime, duration) + end + @doc """ Returns the given naive datetime with the microsecond field truncated to the given precision (`:microsecond`, `:millisecond` or `:second`). diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index fd428e1d7c1..e98a2879a5c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6310,6 +6310,30 @@ defmodule Kernel do to_calendar_struct(Date, calendar: calendar, year: year, month: month, day: day) end + @doc ~S""" + Handles the sigil `~P` for durations in ISO8601 format. + + ## Examples + + iex> ~P[3Y6M4DT12H30M5S] + %Calendar.Duration{ + year: 3, + month: 6, + week: 0, + day: 4, + hour: 12, + minute: 30, + second: 5, + microsecond: 0 + } + + """ + defmacro sigil_P({:<<>>, _, [duration_string]}, []) do + quote do + Calendar.Duration.parse!(unquote("P#{duration_string}")) + end + end + @doc ~S""" Handles the sigil `~T` for times. diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index abfe5296ce5..40c17a60c0e 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -195,6 +195,8 @@ defmodule DateTest do assert Date.shift(~D[2000-01-01], month: 12) == {:ok, ~D[2001-01-01]} assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} + assert Date.shift(~D[2012-01-01], ~P[1Y3M2D]) == {:ok, ~D[2013-04-03]} + assert_raise ArgumentError, fn -> date = ~D[2000-01-01] Date.shift(date, months: 12) diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs new file mode 100644 index 00000000000..f50a5190b15 --- /dev/null +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -0,0 +1,134 @@ +Code.require_file("../test_helper.exs", __DIR__) + +defmodule Calendar.DurationTest do + use ExUnit.Case, async: true + doctest Calendar.Duration + + test "new!/1" do + # TODO + assert true + end + + test "sigil_P" do + assert ~P[3Y6M4DT12H30M5S] == %Calendar.Duration{ + year: 3, + month: 6, + week: 0, + day: 4, + hour: 12, + minute: 30, + second: 5, + microsecond: 0 + } + + assert ~P[T1H30M] == %Calendar.Duration{ + year: 0, + month: 0, + week: 0, + day: 0, + hour: 1, + minute: 30, + second: 0, + microsecond: 0 + } + + assert ~P[1DT2H] == %Calendar.Duration{ + year: 0, + month: 0, + week: 0, + day: 1, + hour: 2, + minute: 0, + second: 0, + microsecond: 0 + } + + assert ~P[5Y2M3DT12H] == %Calendar.Duration{ + year: 5, + month: 2, + week: 0, + day: 3, + hour: 12, + minute: 0, + second: 0, + microsecond: 0 + } + + assert ~P[10W3DT5H] == %Calendar.Duration{ + year: 0, + month: 0, + week: 10, + day: 3, + hour: 5, + minute: 0, + second: 0, + microsecond: 0 + } + + assert ~P[T10M] == %Calendar.Duration{ + year: 0, + month: 0, + week: 0, + day: 0, + hour: 0, + minute: 10, + second: 0, + microsecond: 0 + } + + assert ~P[1Y] == %Calendar.Duration{ + year: 1, + month: 0, + week: 0, + day: 0, + hour: 0, + minute: 0, + second: 0, + microsecond: 0 + } + + assert ~P[3W] == %Calendar.Duration{ + year: 0, + month: 0, + week: 3, + day: 0, + hour: 0, + minute: 0, + second: 0, + microsecond: 0 + } + + assert ~P[1DT12H] == %Calendar.Duration{ + year: 0, + month: 0, + week: 0, + day: 1, + hour: 12, + minute: 0, + second: 0, + microsecond: 0 + } + + assert ~P[T2S] == %Calendar.Duration{ + year: 0, + month: 0, + week: 0, + day: 0, + hour: 0, + minute: 0, + second: 2, + microsecond: 0 + } + + assert ~P[-3Y-6M-4DT-12H-30M-5S] == %Calendar.Duration{ + year: -3, + month: -6, + week: 0, + day: -4, + hour: -12, + minute: -30, + second: -5, + microsecond: 0 + } + end +end diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 34d6b591c6f..82d2a59e864 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -410,6 +410,9 @@ defmodule NaiveDateTimeTest do assert NaiveDateTime.shift(naive_datetime, minute: -45) == {:ok, ~N[1999-12-31 23:15:00]} assert NaiveDateTime.shift(naive_datetime, second: -30) == {:ok, ~N[1999-12-31 23:59:30]} + assert NaiveDateTime.shift(naive_datetime, ~P[1Y2M3DT4H5M6S]) == + {:ok, ~N[2001-03-04 04:05:06]} + assert NaiveDateTime.shift(naive_datetime, microsecond: -500) == {:ok, ~N[1999-12-31 23:59:59.999500]} From d1b93230022c53b78d4575b8fbae393a92c93b7e Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 4 Mar 2024 19:04:09 +0100 Subject: [PATCH 17/97] gracefully handle invalid duration args --- lib/elixir/lib/calendar/date.ex | 11 ++++++++--- lib/elixir/lib/calendar/duration.ex | 12 +++++++++++- lib/elixir/lib/calendar/naive_datetime.ex | 11 ++++++++--- lib/elixir/test/elixir/calendar/date_test.exs | 5 +---- .../test/elixir/calendar/naive_datetime_test.exs | 5 +---- 5 files changed, 29 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 955675ea72c..10ebb2fe03b 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -778,7 +778,7 @@ defmodule Date do """ @spec shift(Calendar.date(), Calendar.Duration.t() | [Calendar.Duration.duration_units()]) :: - {:ok, t} + {:ok, t} | {:error, :invalid_duration} def shift(%Date{calendar: calendar} = date, %Calendar.Duration{} = duration) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) @@ -786,8 +786,13 @@ defmodule Date do end def shift(%Date{} = date = date, duration_units) do - duration = Calendar.Duration.new!(duration_units) - shift(date, duration) + case Calendar.Duration.new(duration_units) do + {:ok, duration} -> + shift(date, duration) + + {:error, :invalid_duration} -> + {:error, :invalid_duration} + end end @doc false diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 4fca4c2dea0..18894830af1 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -36,10 +36,20 @@ defmodule Calendar.Duration do @spec new!([duration_unit]) :: t() def new!(duration_units) do - Keyword.validate!(duration_units, Map.keys(%__MODULE__{}) -- [:__struct__]) struct!(__MODULE__, duration_units) end + @spec new([duration_unit]) :: {:ok, t()} | {:error, :invalid_duration} + def new(duration_units) do + case Keyword.validate(duration_units, Map.keys(%__MODULE__{}) -- [:__struct__]) do + {:ok, duration_units} -> + {:ok, struct(__MODULE__, duration_units)} + + {:error, _invalid_keys} -> + {:error, :invalid_duration} + end + end + @duration_regex ~r/P(?:(?-?\d+)Y)?(?:(?-?\d+)M)?(?:(?-?\d+)W)?(?:(?-?\d+)D)?(?:T(?:(?-?\d+)H)?(?:(?-?\d+)M)?(?:(?-?\d+)S)?)?/ @spec parse!(String.t()) :: t() def parse!(duration_string) when is_binary(duration_string) do diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 2afb6e3bb7e..341357642f0 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -595,7 +595,7 @@ defmodule NaiveDateTime do @spec shift( Calendar.naive_datetime(), Calendar.Duration.t() | [Calendar.Duration.duration_units()] - ) :: {:ok, t} + ) :: {:ok, t} | {:error, :invalid_duration} def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Calendar.Duration{} = duration) do %{ year: year, @@ -633,8 +633,13 @@ defmodule NaiveDateTime do end def shift(%NaiveDateTime{} = naive_datetime, duration_units) do - duration = Calendar.Duration.new!(duration_units) - shift(naive_datetime, duration) + case Calendar.Duration.new(duration_units) do + {:ok, duration} -> + shift(naive_datetime, duration) + + {:error, :invalid_duration} -> + {:error, :invalid_duration} + end end @doc """ diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index 40c17a60c0e..c90570f0ad6 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -197,10 +197,7 @@ defmodule DateTest do assert Date.shift(~D[2012-01-01], ~P[1Y3M2D]) == {:ok, ~D[2013-04-03]} - assert_raise ArgumentError, fn -> - date = ~D[2000-01-01] - Date.shift(date, months: 12) - end + assert Date.shift(~D[2012-01-01], months: 12) == {:error, :invalid_duration} assert_raise UndefinedFunctionError, fn -> date = Calendar.Holocene.date(12000, 01, 01) diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 82d2a59e864..885bfa6ae25 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -447,9 +447,6 @@ defmodule NaiveDateTimeTest do microsecond: -8 ) == {:ok, ~N[1998-10-06 18:53:52.999992]} - assert_raise ArgumentError, fn -> - naive_datetime = naive_datetime - NaiveDateTime.shift(naive_datetime, months: 12) - end + assert NaiveDateTime.shift(naive_datetime, months: 12) == {:error, :invalid_duration} end end From 2d5b06714dd079227dc2c493a5e6ceab1d9e9673 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 4 Mar 2024 19:44:56 +0100 Subject: [PATCH 18/97] add Time.shift/2 --- lib/elixir/lib/calendar.ex | 6 ++ lib/elixir/lib/calendar/iso.ex | 83 ++++++++++++++++--- lib/elixir/lib/calendar/time.ex | 45 ++++++++++ lib/elixir/test/elixir/calendar/time_test.exs | 10 +++ 4 files changed, 133 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 175cd72e7ca..0bdf4b2a66f 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -358,6 +358,12 @@ defmodule Calendar do Calendar.Duration.t() ) :: {year, month, day, hour, minute, second, microsecond} + @doc """ + Shifts time by given duration according to its calendar. + """ + @callback shift_time(hour, minute, second, microsecond, Calendar.Duration.t()) :: + {hour, minute, second, microsecond} + # General Helpers @doc """ diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 3e0114b67b6..4de767b5c28 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1475,11 +1475,10 @@ defmodule Calendar.ISO do iex> Calendar.ISO.shift_date(2016, 1, 31, Calendar.Duration.new!(year: 4, day: 1)) {2020, 2, 1} """ - @spec shift_date(year(), month(), day(), Calendar.Duration.t()) :: - {year, month, day} + @spec shift_date(year, month, day, Calendar.Duration.t()) :: {year, month, day} @impl true def shift_date(year, month, day, duration) do - shift_options = shift_date_options(duration) + shift_options = get_shift_options(:date, duration) {year, month, day, _, _, _, _} = Enum.reduce(shift_options, {year, month, day, 0, 0, 0, {0, 0}}, fn @@ -1532,7 +1531,7 @@ defmodule Calendar.ISO do ) :: {year, month, day, hour, minute, second, microsecond} @impl true def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do - shift_options = shift_naive_datetime_options(duration) + shift_options = get_shift_options(:naive_datetime, duration) Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn {_, 0}, naive_datetime -> @@ -1549,6 +1548,36 @@ defmodule Calendar.ISO do end) end + @doc """ + Shifts time by Calendar.Duration units according to its calendar. + + Available units are: `:hour, :minute, :second, :microsecond`. + + ## Examples + + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Calendar.Duration.new!(hour: 2)) + {15, 0, 0, {0, 0}} + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Calendar.Duration.new!(microsecond: 100)) + {13, 0, 0, {100, 6}} + """ + @spec shift_time(hour, minute, second, microsecond, Calendar.Duration.t()) :: + {hour, minute, second, microsecond} + @impl true + def shift_time(hour, minute, second, microsecond, duration) do + shift_options = get_shift_options(:time, duration) + + Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn + {_, 0}, naive_datetime -> + naive_datetime + + {:second, value}, naive_datetime -> + shift_time_unit(naive_datetime, value, :second) + + {:microsecond, value}, naive_datetime -> + shift_time_unit(naive_datetime, value, :microsecond) + end) + end + defp shift_days({year, month, day, hour, minute, second, microsecond}, days) do {year, month, day} = date_to_iso_days(year, month, day) @@ -1576,9 +1605,12 @@ defmodule Calendar.ISO do {new_year, new_month, new_day, hour, minute, second, microsecond} end - defp shift_time_unit(naive_datetime, value, unit) when unit in [:second, :microsecond] do - {year, month, day, hour, minute, second, {_, ms_precision} = microsecond} = naive_datetime - + defp shift_time_unit( + {year, month, day, hour, minute, second, {_, ms_precision} = microsecond}, + value, + unit + ) + when unit in [:second, :microsecond] do ppd = System.convert_time_unit(86400, :second, unit) precision = max(time_unit_to_precision(unit), ms_precision) @@ -1590,7 +1622,24 @@ defmodule Calendar.ISO do {year, month, day, hour, minute, second, {ms_value, precision}} end - defp shift_naive_datetime_options(%Calendar.Duration{ + defp shift_time_unit( + {hour, minute, second, {_, precision} = microsecond}, + value, + unit + ) + when unit in [:second, :microsecond] do + time = {0, time_to_day_fraction(hour, minute, second, microsecond)} + amount_to_add = System.convert_time_unit(value, unit, :microsecond) + total = iso_days_to_unit(time, :microsecond) + amount_to_add + parts = Integer.mod(total, @parts_per_day) + precision = max(time_unit_to_precision(unit), precision) + + {hour, minute, second, {microsecond, _}} = time_from_day_fraction({parts, @parts_per_day}) + + {hour, minute, second, {microsecond, precision}} + end + + defp get_shift_options(:date, %{ year: year, month: month, week: week, @@ -1602,12 +1651,13 @@ defmodule Calendar.ISO do }) do [ month: year * 12 + month, - second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second, + day: week * 7 + day, + second: hour * 3600 + minute * 60 + second, microsecond: microsecond ] end - defp shift_date_options(%Calendar.Duration{ + defp get_shift_options(:naive_datetime, %{ year: year, month: month, week: week, @@ -1619,7 +1669,18 @@ defmodule Calendar.ISO do }) do [ month: year * 12 + month, - day: week * 7 + day, + second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second, + microsecond: microsecond + ] + end + + defp get_shift_options(:time, %{ + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }) do + [ second: hour * 3600 + minute * 60 + second, microsecond: microsecond ] diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 43ef7a6e2d5..1892db32678 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -558,6 +558,51 @@ defmodule Time do Calendar.ISO.iso_days_to_unit(iso_days, :microsecond) end + @doc """ + Shifts a Time by given Calendar.Duration according to its calendar. + + Check `Calendar.ISO.shift_time/4` for more information. + + ## Examples + + iex> Time.shift(~T[01:00:15], hour: 12) + {:ok, ~T[13:00:15]} + iex> Time.shift(~T[01:15:00], hour: 6, minute: 15) + {:ok, ~T[07:30:00]} + iex> Time.shift(~T[01:15:00], second: 125) + {:ok, ~T[01:17:05]} + iex> Time.shift(~T[01:00:15], microsecond: 100) + {:ok, ~T[01:00:15.000100]} + + """ + @spec shift(Calendar.time(), Calendar.Duration.t() | [Calendar.Duration.duration_units()]) :: + {:ok, t()} | {:error, :invalid_duration} + def shift(%Time{calendar: calendar} = time, %Calendar.Duration{} = duration) do + %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time + + {hour, minute, second, microsecond} = + calendar.shift_time(hour, minute, second, microsecond, duration) + + {:ok, + %Time{ + calendar: calendar, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }} + end + + def shift(%Time{} = date = date, duration_units) do + case Calendar.Duration.new(duration_units) do + {:ok, duration} -> + shift(date, duration) + + {:error, :invalid_duration} -> + {:error, :invalid_duration} + end + end + @doc """ Compares two time structs. diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 125ff55e6b8..530042a065e 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -102,4 +102,14 @@ defmodule TimeTest do Time.add(time, 1, 0) end end + + test "shift/2" do + time = ~T[00:00:00.0] + assert Time.shift(time, hour: 1) == {:ok, ~T[01:00:00.0]} + assert Time.shift(time, hour: 25) == {:ok, ~T[01:00:00.0]} + assert Time.shift(time, minute: 25) == {:ok, ~T[00:25:00.0]} + assert Time.shift(time, second: 50) == {:ok, ~T[00:00:50.0]} + assert Time.shift(time, microsecond: 150) == {:ok, ~T[00:00:00.000150]} + assert Time.shift(time, hour: 2, minute: 65, second: 5) == {:ok, ~T[03:05:05.0]} + end end From 27e02fd113054e8ea04f03eaa08935e2fe367b14 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 4 Mar 2024 19:46:34 +0100 Subject: [PATCH 19/97] variable name --- lib/elixir/lib/calendar/iso.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 4de767b5c28..b3af51520a7 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1567,14 +1567,14 @@ defmodule Calendar.ISO do shift_options = get_shift_options(:time, duration) Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn - {_, 0}, naive_datetime -> - naive_datetime + {_, 0}, time -> + time - {:second, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, :second) + {:second, value}, time -> + shift_time_unit(time, value, :second) - {:microsecond, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, :microsecond) + {:microsecond, value}, time -> + shift_time_unit(time, value, :microsecond) end) end From ce16ad3ef4556ee1db5273227dd826ae43dc1590 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 4 Mar 2024 20:31:58 +0100 Subject: [PATCH 20/97] add DateTime.shift/2 --- lib/elixir/lib/calendar/datetime.ex | 70 +++++++++++++++++++ .../test/elixir/calendar/datetime_test.exs | 37 ++++++++++ 2 files changed, 107 insertions(+) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 119bf71a086..e4a916fe214 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1674,6 +1674,76 @@ defmodule DateTime do end end + @doc """ + Shifts a datetime by given Calendar.Duration according to its calendar. + + Can return an ambiguous or gap datetime tuple. + + ## Examples + + iex> DateTime.shift(~U[2016-01-03 00:00:00Z], month: 2) + {:ok, ~U[2016-03-03 00:00:00Z]} + + """ + @spec shift( + Calendar.datetime(), + Calendar.Duration.t() | [Calendar.Duration.duration_units()], + Calendar.time_zone_database() + ) :: + {:ok, t} + | {:ambiguous, first_datetime :: t, second_datetime :: t} + | {:gap, t, t} + | {:error, + :incompatible_calendars | :time_zone_not_found | :utc_only_time_zone_database, + :invalid_duration} + def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) + + def shift( + %DateTime{calendar: calendar} = datetime, + %Calendar.Duration{} = duration, + time_zone_database + ) do + %{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond, + time_zone: time_zone + } = datetime + + {year, month, day, hour, minute, second, microsecond} = + calendar.shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + duration + ) + + new( + Date.new!(year, month, day), + Time.new!(hour, minute, second, microsecond), + time_zone, + time_zone_database + ) + end + + def shift(%DateTime{} = datetime, duration_units, time_zone_database) do + case Calendar.Duration.new(duration_units) do + {:ok, duration} -> + shift(datetime, duration, time_zone_database) + + {:error, :invalid_duration} -> + {:error, :invalid_duration} + end + end + @doc """ Returns the given datetime with the microsecond field truncated to the given precision (`:microsecond`, `:millisecond` or `:second`). diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index e988874e928..2d1160d5a6e 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1071,4 +1071,41 @@ defmodule DateTimeTest do assert catch_error(DateTime.to_naive(~N[2000-02-29 12:23:34])) end end + + test "shift/2" do + assert DateTime.shift(~U[2000-01-01 00:00:00Z], year: 1) == {:ok, ~U[2001-01-01 00:00:00Z]} + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1) == {:ok, ~U[2000-02-01 00:00:00Z]} + + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1, day: 28) == + {:ok, ~U[2000-02-29 00:00:00Z]} + + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1, day: 30) == + {:ok, ~U[2000-03-02 00:00:00Z]} + + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 2, day: 29) == + {:ok, ~U[2000-03-30 00:00:00Z]} + + assert DateTime.shift(~U[2000-02-29 00:00:00Z], year: -1) == {:ok, ~U[1999-02-28 00:00:00Z]} + assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1) == {:ok, ~U[2000-01-29 00:00:00Z]} + + assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1, day: -28) == + {:ok, ~U[2000-01-01 00:00:00Z]} + + assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1, day: -30) == + {:ok, ~U[1999-12-30 00:00:00Z]} + + assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1, day: -29) == + {:ok, ~U[1999-12-31 00:00:00Z]} + + datetime = + DateTime.new!( + Date.new!(2018, 10, 27), + Time.new!(2, 30, 0, {0, 0}), + "Europe/Copenhagen", + FakeTimeZoneDatabase + ) + + assert {:ambiguous, %DateTime{}, %DateTime{}} = + DateTime.shift(datetime, [day: 1], FakeTimeZoneDatabase) + end end From 287c8af2d5cbbc50896cceb54d286fda26948c99 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 4 Mar 2024 20:42:10 +0100 Subject: [PATCH 21/97] add sigil variant to all examples --- lib/elixir/lib/calendar/date.ex | 6 +++--- lib/elixir/lib/calendar/datetime.ex | 4 +++- lib/elixir/lib/calendar/naive_datetime.ex | 8 +++----- lib/elixir/lib/calendar/time.ex | 4 +++- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 10ebb2fe03b..2a79b24f0f5 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -761,18 +761,18 @@ defmodule Date do end @doc """ - Shifts a date by given Calendar.Duration according to its calendar. + Shifts a date by given duration according to its calendar. Check `Calendar.ISO.shift_date/4` for more information. ## Examples + iex> Date.shift(~D[2016-01-03], ~P[4Y1M2W26D]) + {:ok, ~D[2020-03-14]} iex> Date.shift(~D[2016-01-03], month: 2) {:ok, ~D[2016-03-03]} iex> Date.shift(~D[2016-02-29], month: 1) {:ok, ~D[2016-03-29]} - iex> Date.shift(~D[2016-01-31], month: 1) - {:ok, ~D[2016-02-29]} iex> Date.shift(~D[2016-01-31], year: 4, day: 1) {:ok, ~D[2020-02-01]} diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index e4a916fe214..af32d7243f9 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1675,12 +1675,14 @@ defmodule DateTime do end @doc """ - Shifts a datetime by given Calendar.Duration according to its calendar. + Shifts a datetime by given duration according to its calendar. Can return an ambiguous or gap datetime tuple. ## Examples + iex> DateTime.shift(~U[2016-01-03 00:00:00Z], ~P[4Y1M2W26D]) + {:ok, ~U[2020-03-14 00:00:00Z]} iex> DateTime.shift(~U[2016-01-03 00:00:00Z], month: 2) {:ok, ~U[2016-03-03 00:00:00Z]} diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 341357642f0..907b2614fca 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -572,16 +572,14 @@ defmodule NaiveDateTime do end @doc """ - Shifts a naive datetime by given Calendar.Duration according to its calendar. + Shifts a naive datetime by given duration according to its calendar. Check `Calendar.ISO.shift_naive_datetime/8` for more information. ## Examples - iex> NaiveDateTime.shift(~N[2016-01-03 00:00:00], month: 2) - {:ok, ~N[2016-03-03 00:00:00]} - iex> NaiveDateTime.shift(~N[2016-02-29 00:00:00], month: 1) - {:ok, ~N[2016-03-29 00:00:00]} + iex> NaiveDateTime.shift(~N[2016-01-03 00:00:00], ~P[4Y1M2W26D]) + {:ok, ~N[2020-03-14 00:00:00]} iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) {:ok, ~N[2016-02-29 00:00:00]} iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 1892db32678..4b83749ac01 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -565,6 +565,8 @@ defmodule Time do ## Examples + iex> Time.shift(~T[00:00:00], ~P[2DT4H30M25S]) + {:ok, ~T[04:30:25]} iex> Time.shift(~T[01:00:15], hour: 12) {:ok, ~T[13:00:15]} iex> Time.shift(~T[01:15:00], hour: 6, minute: 15) @@ -576,7 +578,7 @@ defmodule Time do """ @spec shift(Calendar.time(), Calendar.Duration.t() | [Calendar.Duration.duration_units()]) :: - {:ok, t()} | {:error, :invalid_duration} + {:ok, t} | {:error, :invalid_duration} def shift(%Time{calendar: calendar} = time, %Calendar.Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time From 063a33c7f90df6b6ed68cbabb375eaa52b56cdd5 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 4 Mar 2024 21:07:29 +0100 Subject: [PATCH 22/97] cleanup specs --- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/datetime.ex | 8 +++++--- lib/elixir/lib/calendar/duration.ex | 18 +++++++++--------- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- lib/elixir/lib/calendar/time.ex | 2 +- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 2a79b24f0f5..d528c5d2635 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -777,7 +777,7 @@ defmodule Date do {:ok, ~D[2020-02-01]} """ - @spec shift(Calendar.date(), Calendar.Duration.t() | [Calendar.Duration.duration_units()]) :: + @spec shift(Calendar.date(), Calendar.Duration.t() | [Calendar.Duration.unit()]) :: {:ok, t} | {:error, :invalid_duration} def shift(%Date{calendar: calendar} = date, %Calendar.Duration{} = duration) do %{year: year, month: month, day: day} = date diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index af32d7243f9..705ab9a8455 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1689,15 +1689,17 @@ defmodule DateTime do """ @spec shift( Calendar.datetime(), - Calendar.Duration.t() | [Calendar.Duration.duration_units()], + Calendar.Duration.t() | [Calendar.Duration.unit()], Calendar.time_zone_database() ) :: {:ok, t} | {:ambiguous, first_datetime :: t, second_datetime :: t} | {:gap, t, t} | {:error, - :incompatible_calendars | :time_zone_not_found | :utc_only_time_zone_database, - :invalid_duration} + :incompatible_calendars + | :time_zone_not_found + | :utc_only_time_zone_database + | :invalid_duration} def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) def shift( diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 18894830af1..ed17610ade7 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -24,7 +24,7 @@ defmodule Calendar.Duration do } @typedoc "Individually valid Duration units" - @type duration_unit :: + @type unit :: {:year, integer()} | {:month, integer()} | {:week, integer()} @@ -34,16 +34,16 @@ defmodule Calendar.Duration do | {:second, integer()} | {:microsecond, integer()} - @spec new!([duration_unit]) :: t() - def new!(duration_units) do - struct!(__MODULE__, duration_units) + @spec new!([unit]) :: t() + def new!(units) do + struct!(__MODULE__, units) end - @spec new([duration_unit]) :: {:ok, t()} | {:error, :invalid_duration} - def new(duration_units) do - case Keyword.validate(duration_units, Map.keys(%__MODULE__{}) -- [:__struct__]) do - {:ok, duration_units} -> - {:ok, struct(__MODULE__, duration_units)} + @spec new([unit]) :: {:ok, t()} | {:error, :invalid_duration} + def new(units) do + case Keyword.validate(units, Map.keys(%__MODULE__{}) -- [:__struct__]) do + {:ok, units} -> + {:ok, struct(__MODULE__, units)} {:error, _invalid_keys} -> {:error, :invalid_duration} diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 907b2614fca..efa2c70cd2e 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -592,7 +592,7 @@ defmodule NaiveDateTime do """ @spec shift( Calendar.naive_datetime(), - Calendar.Duration.t() | [Calendar.Duration.duration_units()] + Calendar.Duration.t() | [Calendar.Duration.unit()] ) :: {:ok, t} | {:error, :invalid_duration} def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Calendar.Duration{} = duration) do %{ diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 4b83749ac01..18dbf020316 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -577,7 +577,7 @@ defmodule Time do {:ok, ~T[01:00:15.000100]} """ - @spec shift(Calendar.time(), Calendar.Duration.t() | [Calendar.Duration.duration_units()]) :: + @spec shift(Calendar.time(), Calendar.Duration.t() | [Calendar.Duration.unit()]) :: {:ok, t} | {:error, :invalid_duration} def shift(%Time{calendar: calendar} = time, %Calendar.Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time From 8c1e536e646c1a22e2ea98d246ba5904baec45cd Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 4 Mar 2024 22:01:55 +0100 Subject: [PATCH 23/97] more typespecs --- lib/elixir/lib/calendar.ex | 3 +-- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/duration.ex | 40 +++++++++++++++-------------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 0bdf4b2a66f..598927f633a 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -341,8 +341,7 @@ defmodule Calendar do @doc """ Shifts date by given duration according to its calendar. """ - @callback shift_date(year, month, day, Calendar.Duration.t()) :: - {year, month, day} + @callback shift_date(year, month, day, Calendar.Duration.t()) :: {year, month, day} @doc """ Shifts naive datetime by given duration according to its calendar. diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index d528c5d2635..352a15c6a7c 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -51,7 +51,7 @@ defmodule Date do iex> Date.add(~D[1970-01-01], 14716) ~D[2010-04-17] - iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 3) + iex> Date.shift(~D[1970-01-01], ~P[40Y3M2W3D]) {:ok, ~D[2010-04-18]} Those functions are optimized to deal with common epochs, such diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index ed17610ade7..1ced4d7a8e6 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -13,33 +13,33 @@ defmodule Calendar.Duration do @typedoc "Duration in calendar units" @type t :: %__MODULE__{ - year: integer(), - month: integer(), - week: integer(), - day: integer(), - hour: integer(), - minute: integer(), - second: integer(), - microsecond: integer() + year: integer, + month: integer, + week: integer, + day: integer, + hour: integer, + minute: integer, + second: integer, + microsecond: integer } @typedoc "Individually valid Duration units" @type unit :: - {:year, integer()} - | {:month, integer()} - | {:week, integer()} - | {:day, integer()} - | {:hour, integer()} - | {:minute, integer()} - | {:second, integer()} - | {:microsecond, integer()} + {:year, integer} + | {:month, integer} + | {:week, integer} + | {:day, integer} + | {:hour, integer} + | {:minute, integer} + | {:second, integer} + | {:microsecond, integer} - @spec new!([unit]) :: t() + @spec new!([unit]) :: t def new!(units) do struct!(__MODULE__, units) end - @spec new([unit]) :: {:ok, t()} | {:error, :invalid_duration} + @spec new([unit]) :: {:ok, t} | {:error, :invalid_duration} def new(units) do case Keyword.validate(units, Map.keys(%__MODULE__{}) -- [:__struct__]) do {:ok, units} -> @@ -50,8 +50,10 @@ defmodule Calendar.Duration do end end + # TODO write parser instead of relying on regex + # TODO write formatter to implement in Inspect + String.Chars @duration_regex ~r/P(?:(?-?\d+)Y)?(?:(?-?\d+)M)?(?:(?-?\d+)W)?(?:(?-?\d+)D)?(?:T(?:(?-?\d+)H)?(?:(?-?\d+)M)?(?:(?-?\d+)S)?)?/ - @spec parse!(String.t()) :: t() + @spec parse!(binary) :: t def parse!(duration_string) when is_binary(duration_string) do case Regex.named_captures(@duration_regex, duration_string) do %{ From c6ab2ee762ed0354e9ecccb87d77f434fbee1467 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Tue, 5 Mar 2024 10:11:23 +0100 Subject: [PATCH 24/97] drop sigil_P --- lib/elixir/lib/calendar/date.ex | 4 +- lib/elixir/lib/calendar/datetime.ex | 2 - lib/elixir/lib/calendar/duration.ex | 33 ----- lib/elixir/lib/calendar/naive_datetime.ex | 2 - lib/elixir/lib/calendar/time.ex | 2 - lib/elixir/lib/kernel.ex | 24 ---- lib/elixir/test/elixir/calendar/date_test.exs | 2 - .../test/elixir/calendar/duration_test.exs | 128 ------------------ .../elixir/calendar/naive_datetime_test.exs | 3 - 9 files changed, 1 insertion(+), 199 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 352a15c6a7c..be893f45bf7 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -51,7 +51,7 @@ defmodule Date do iex> Date.add(~D[1970-01-01], 14716) ~D[2010-04-17] - iex> Date.shift(~D[1970-01-01], ~P[40Y3M2W3D]) + iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 3) {:ok, ~D[2010-04-18]} Those functions are optimized to deal with common epochs, such @@ -767,8 +767,6 @@ defmodule Date do ## Examples - iex> Date.shift(~D[2016-01-03], ~P[4Y1M2W26D]) - {:ok, ~D[2020-03-14]} iex> Date.shift(~D[2016-01-03], month: 2) {:ok, ~D[2016-03-03]} iex> Date.shift(~D[2016-02-29], month: 1) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 705ab9a8455..0ae67e2ef26 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1681,8 +1681,6 @@ defmodule DateTime do ## Examples - iex> DateTime.shift(~U[2016-01-03 00:00:00Z], ~P[4Y1M2W26D]) - {:ok, ~U[2020-03-14 00:00:00Z]} iex> DateTime.shift(~U[2016-01-03 00:00:00Z], month: 2) {:ok, ~U[2016-03-03 00:00:00Z]} diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 1ced4d7a8e6..61bd4db2c7e 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -49,37 +49,4 @@ defmodule Calendar.Duration do {:error, :invalid_duration} end end - - # TODO write parser instead of relying on regex - # TODO write formatter to implement in Inspect + String.Chars - @duration_regex ~r/P(?:(?-?\d+)Y)?(?:(?-?\d+)M)?(?:(?-?\d+)W)?(?:(?-?\d+)D)?(?:T(?:(?-?\d+)H)?(?:(?-?\d+)M)?(?:(?-?\d+)S)?)?/ - @spec parse!(binary) :: t - def parse!(duration_string) when is_binary(duration_string) do - case Regex.named_captures(@duration_regex, duration_string) do - %{ - "year" => year, - "month" => month, - "week" => week, - "day" => day, - "hour" => hour, - "minute" => minute, - "second" => second - } -> - new!( - year: parse_unit(year), - month: parse_unit(month), - week: parse_unit(week), - day: parse_unit(day), - hour: parse_unit(hour), - minute: parse_unit(minute), - second: parse_unit(second) - ) - - _ -> - raise ArgumentError, "invalid duration string" - end - end - - defp parse_unit(""), do: 0 - defp parse_unit(part), do: String.to_integer(part) end diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index efa2c70cd2e..43931a3b003 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -578,8 +578,6 @@ defmodule NaiveDateTime do ## Examples - iex> NaiveDateTime.shift(~N[2016-01-03 00:00:00], ~P[4Y1M2W26D]) - {:ok, ~N[2020-03-14 00:00:00]} iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) {:ok, ~N[2016-02-29 00:00:00]} iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 18dbf020316..8a3bead2f64 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -565,8 +565,6 @@ defmodule Time do ## Examples - iex> Time.shift(~T[00:00:00], ~P[2DT4H30M25S]) - {:ok, ~T[04:30:25]} iex> Time.shift(~T[01:00:15], hour: 12) {:ok, ~T[13:00:15]} iex> Time.shift(~T[01:15:00], hour: 6, minute: 15) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index e98a2879a5c..fd428e1d7c1 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6310,30 +6310,6 @@ defmodule Kernel do to_calendar_struct(Date, calendar: calendar, year: year, month: month, day: day) end - @doc ~S""" - Handles the sigil `~P` for durations in ISO8601 format. - - ## Examples - - iex> ~P[3Y6M4DT12H30M5S] - %Calendar.Duration{ - year: 3, - month: 6, - week: 0, - day: 4, - hour: 12, - minute: 30, - second: 5, - microsecond: 0 - } - - """ - defmacro sigil_P({:<<>>, _, [duration_string]}, []) do - quote do - Calendar.Duration.parse!(unquote("P#{duration_string}")) - end - end - @doc ~S""" Handles the sigil `~T` for times. diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index c90570f0ad6..90bf07544c8 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -195,8 +195,6 @@ defmodule DateTest do assert Date.shift(~D[2000-01-01], month: 12) == {:ok, ~D[2001-01-01]} assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} - assert Date.shift(~D[2012-01-01], ~P[1Y3M2D]) == {:ok, ~D[2013-04-03]} - assert Date.shift(~D[2012-01-01], months: 12) == {:error, :invalid_duration} assert_raise UndefinedFunctionError, fn -> diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index f50a5190b15..4599a136950 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -3,132 +3,4 @@ Code.require_file("../test_helper.exs", __DIR__) defmodule Calendar.DurationTest do use ExUnit.Case, async: true doctest Calendar.Duration - - test "new!/1" do - # TODO - assert true - end - - test "sigil_P" do - assert ~P[3Y6M4DT12H30M5S] == %Calendar.Duration{ - year: 3, - month: 6, - week: 0, - day: 4, - hour: 12, - minute: 30, - second: 5, - microsecond: 0 - } - - assert ~P[T1H30M] == %Calendar.Duration{ - year: 0, - month: 0, - week: 0, - day: 0, - hour: 1, - minute: 30, - second: 0, - microsecond: 0 - } - - assert ~P[1DT2H] == %Calendar.Duration{ - year: 0, - month: 0, - week: 0, - day: 1, - hour: 2, - minute: 0, - second: 0, - microsecond: 0 - } - - assert ~P[5Y2M3DT12H] == %Calendar.Duration{ - year: 5, - month: 2, - week: 0, - day: 3, - hour: 12, - minute: 0, - second: 0, - microsecond: 0 - } - - assert ~P[10W3DT5H] == %Calendar.Duration{ - year: 0, - month: 0, - week: 10, - day: 3, - hour: 5, - minute: 0, - second: 0, - microsecond: 0 - } - - assert ~P[T10M] == %Calendar.Duration{ - year: 0, - month: 0, - week: 0, - day: 0, - hour: 0, - minute: 10, - second: 0, - microsecond: 0 - } - - assert ~P[1Y] == %Calendar.Duration{ - year: 1, - month: 0, - week: 0, - day: 0, - hour: 0, - minute: 0, - second: 0, - microsecond: 0 - } - - assert ~P[3W] == %Calendar.Duration{ - year: 0, - month: 0, - week: 3, - day: 0, - hour: 0, - minute: 0, - second: 0, - microsecond: 0 - } - - assert ~P[1DT12H] == %Calendar.Duration{ - year: 0, - month: 0, - week: 0, - day: 1, - hour: 12, - minute: 0, - second: 0, - microsecond: 0 - } - - assert ~P[T2S] == %Calendar.Duration{ - year: 0, - month: 0, - week: 0, - day: 0, - hour: 0, - minute: 0, - second: 2, - microsecond: 0 - } - - assert ~P[-3Y-6M-4DT-12H-30M-5S] == %Calendar.Duration{ - year: -3, - month: -6, - week: 0, - day: -4, - hour: -12, - minute: -30, - second: -5, - microsecond: 0 - } - end end diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 885bfa6ae25..cfc39358a8b 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -410,9 +410,6 @@ defmodule NaiveDateTimeTest do assert NaiveDateTime.shift(naive_datetime, minute: -45) == {:ok, ~N[1999-12-31 23:15:00]} assert NaiveDateTime.shift(naive_datetime, second: -30) == {:ok, ~N[1999-12-31 23:59:30]} - assert NaiveDateTime.shift(naive_datetime, ~P[1Y2M3DT4H5M6S]) == - {:ok, ~N[2001-03-04 04:05:06]} - assert NaiveDateTime.shift(naive_datetime, microsecond: -500) == {:ok, ~N[1999-12-31 23:59:59.999500]} From 7ead281ae84226f0ff6b93d333b836dc553dfa0f Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Tue, 5 Mar 2024 11:18:41 +0100 Subject: [PATCH 25/97] add initial Calendar.Duration api --- lib/elixir/lib/calendar/duration.ex | 130 ++++++++++- .../test/elixir/calendar/duration_test.exs | 204 ++++++++++++++++++ 2 files changed, 323 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 61bd4db2c7e..ae1c851ac8f 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -1,12 +1,6 @@ defmodule Calendar.Duration do @moduledoc """ - The Duration type following ISO 8601. - - https://en.wikipedia.org/wiki/ISO_8601#Durations - - TODO: - - Implement utility functions - - Implement arithmetic functions + The calendar Duration type. """ defstruct year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0 @@ -34,11 +28,19 @@ defmodule Calendar.Duration do | {:second, integer} | {:microsecond, integer} - @spec new!([unit]) :: t - def new!(units) do - struct!(__MODULE__, units) - end + @doc """ + Create `Calendar.Duration` struct from valid duration units. + Returns `{:error, :invalid_duration}` when called with invalid units. + + ## Examples + + iex> Calendar.Duration.new(month: 2) + {:ok, %Calendar.Duration{month: 2}} + iex> Calendar.Duration.new(months: 2) + {:error, :invalid_duration} + + """ @spec new([unit]) :: {:ok, t} | {:error, :invalid_duration} def new(units) do case Keyword.validate(units, Map.keys(%__MODULE__{}) -- [:__struct__]) do @@ -49,4 +51,110 @@ defmodule Calendar.Duration do {:error, :invalid_duration} end end + + @doc """ + Same as `new/1` but raises a KeyError when called with invalid units. + + ## Examples + + iex> Calendar.Duration.new!(month: 2) + %Calendar.Duration{month: 2} + + """ + @spec new!([unit]) :: t + def new!(units) do + struct!(__MODULE__, units) + end + + @doc """ + Adds two durations to one new duration. + + ## Examples + + iex> Calendar.Duration.add(%Calendar.Duration{week: 2, day: 1}, %Calendar.Duration{day: 2}) + %Calendar.Duration{week: 2, day: 3} + + """ + @spec add(t, t) :: t + def add(%__MODULE__{} = d1, %__MODULE__{} = d2) do + %__MODULE__{ + year: d1.year + d2.year, + month: d1.month + d2.month, + week: d1.week + d2.week, + day: d1.day + d2.day, + hour: d1.hour + d2.hour, + minute: d1.minute + d2.minute, + second: d1.second + d2.second, + microsecond: d1.microsecond + d2.microsecond + } + end + + @doc """ + Subtracts two durations to one new duration. + + ## Examples + + iex> Calendar.Duration.subtract(%Calendar.Duration{week: 2, day: 1}, %Calendar.Duration{day: 2}) + %Calendar.Duration{week: 2, day: -1} + + """ + @spec subtract(t, t) :: t + def subtract(%__MODULE__{} = d1, %__MODULE__{} = d2) do + %__MODULE__{ + year: d1.year - d2.year, + month: d1.month - d2.month, + week: d1.week - d2.week, + day: d1.day - d2.day, + hour: d1.hour - d2.hour, + minute: d1.minute - d2.minute, + second: d1.second - d2.second, + microsecond: d1.microsecond - d2.microsecond + } + end + + @doc """ + Multiplies all Calendar.Duration units by given integer. + + ## Examples + + iex> Calendar.Duration.multiply(%Calendar.Duration{day: 1, minute: 15, second: -10}, 3) + %Calendar.Duration{day: 3, minute: 45, second: -30} + + """ + @spec multiply(t, integer) :: t + def multiply(%__MODULE__{} = duration, integer) when is_integer(integer) do + %__MODULE__{ + year: duration.year * integer, + month: duration.month * integer, + week: duration.week * integer, + day: duration.day * integer, + hour: duration.hour * integer, + minute: duration.minute * integer, + second: duration.second * integer, + microsecond: duration.microsecond * integer + } + end + + @doc """ + Negates all units of a Calendar.Duration. + + ## Examples + + iex> Calendar.Duration.negate(%Calendar.Duration{day: 1, minute: 15, second: -10}) + %Calendar.Duration{day: -1, minute: -15, second: 10} + + """ + @spec negate(t) :: t + def negate(%__MODULE__{} = duration) do + %__MODULE__{ + year: -duration.year, + month: -duration.month, + week: -duration.week, + day: -duration.day, + hour: -duration.hour, + minute: -duration.minute, + second: -duration.second, + microsecond: -duration.microsecond + } + end end diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 4599a136950..da6adf9d1b2 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -3,4 +3,208 @@ Code.require_file("../test_helper.exs", __DIR__) defmodule Calendar.DurationTest do use ExUnit.Case, async: true doctest Calendar.Duration + + test "new/1" do + assert Calendar.Duration.new(year: 2, month: 1, week: 3) == + {:ok, %Calendar.Duration{year: 2, month: 1, week: 3}} + + assert Calendar.Duration.new(months: 1) == {:error, :invalid_duration} + end + + test "new!/1" do + assert Calendar.Duration.new!(year: 2, month: 1, week: 3) == %Calendar.Duration{ + year: 2, + month: 1, + week: 3 + } + + assert_raise KeyError, ~s/key :months not found/, fn -> + Calendar.Duration.new!(months: 1) + end + end + + test "add/2" do + d1 = %Calendar.Duration{ + year: 1, + month: 2, + week: 3, + day: 4, + hour: 5, + minute: 6, + second: 7, + microsecond: 8 + } + + d2 = %Calendar.Duration{ + year: 8, + month: 7, + week: 6, + day: 5, + hour: 4, + minute: 3, + second: 2, + microsecond: 1 + } + + assert Calendar.Duration.add(d1, d2) == %Calendar.Duration{ + year: 9, + month: 9, + week: 9, + day: 9, + hour: 9, + minute: 9, + second: 9, + microsecond: 9 + } + + assert Calendar.Duration.add(d1, d2) == Calendar.Duration.add(d2, d1) + + d1 = %Calendar.Duration{month: 2, week: 3, day: 4} + d2 = %Calendar.Duration{year: 8, day: 2, second: 2} + + assert Calendar.Duration.add(d1, d2) == %Calendar.Duration{ + year: 8, + month: 2, + week: 3, + day: 6, + hour: 0, + minute: 0, + second: 2, + microsecond: 0 + } + end + + test "subtract/2" do + d1 = %Calendar.Duration{ + year: 1, + month: 2, + week: 3, + day: 4, + hour: 5, + minute: 6, + second: 7, + microsecond: 8 + } + + d2 = %Calendar.Duration{ + year: 8, + month: 7, + week: 6, + day: 5, + hour: 4, + minute: 3, + second: 2, + microsecond: 1 + } + + assert Calendar.Duration.subtract(d1, d2) == %Calendar.Duration{ + year: -7, + month: -5, + week: -3, + day: -1, + hour: 1, + minute: 3, + second: 5, + microsecond: 7 + } + + assert Calendar.Duration.subtract(d2, d1) == %Calendar.Duration{ + year: 7, + month: 5, + week: 3, + day: 1, + hour: -1, + minute: -3, + second: -5, + microsecond: -7 + } + + assert Calendar.Duration.subtract(d1, d2) != Calendar.Duration.subtract(d2, d1) + + d1 = %Calendar.Duration{year: 10, month: 2, week: 3, day: 4} + d2 = %Calendar.Duration{year: 8, day: 2, second: 2} + + assert Calendar.Duration.subtract(d1, d2) == %Calendar.Duration{ + year: 2, + month: 2, + week: 3, + day: 2, + hour: 0, + minute: 0, + second: -2, + microsecond: 0 + } + end + + test "multiply/2" do + duration = %Calendar.Duration{ + year: 1, + month: 2, + week: 3, + day: 4, + hour: 5, + minute: 6, + second: 7, + microsecond: 8 + } + + assert Calendar.Duration.multiply(duration, 3) == %Calendar.Duration{ + year: 3, + month: 6, + week: 9, + day: 12, + hour: 15, + minute: 18, + second: 21, + microsecond: 24 + } + + assert Calendar.Duration.multiply(%Calendar.Duration{year: 2, day: 4, minute: 5}, 4) == + %Calendar.Duration{ + year: 8, + month: 0, + week: 0, + day: 16, + hour: 0, + minute: 20, + second: 0, + microsecond: 0 + } + end + + test "negate/1" do + duration = %Calendar.Duration{ + year: 1, + month: 2, + week: 3, + day: 4, + hour: 5, + minute: 6, + second: 7, + microsecond: 8 + } + + assert Calendar.Duration.negate(duration) == %Calendar.Duration{ + year: -1, + month: -2, + week: -3, + day: -4, + hour: -5, + minute: -6, + second: -7, + microsecond: -8 + } + + assert Calendar.Duration.negate(%Calendar.Duration{year: 2, day: 4, minute: 5}) == + %Calendar.Duration{ + year: -2, + month: 0, + week: 0, + day: -4, + hour: 0, + minute: -5, + second: 0, + microsecond: 0 + } + end end From 39e3d5d9b718f474ef0dde8a2f10445cce9ee5e9 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Tue, 5 Mar 2024 11:56:24 +0100 Subject: [PATCH 26/97] align examples in Date module --- lib/elixir/lib/calendar/date.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index be893f45bf7..c5732299ad7 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -51,8 +51,8 @@ defmodule Date do iex> Date.add(~D[1970-01-01], 14716) ~D[2010-04-17] - iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 3) - {:ok, ~D[2010-04-18]} + iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2) + {:ok, ~D[2010-04-17]} Those functions are optimized to deal with common epochs, such as the Unix Epoch above or the Gregorian Epoch (0000-01-01). From 1d17efe4385bf412ecb5fb12648934488c4c33ce Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Tue, 5 Mar 2024 22:04:53 +0100 Subject: [PATCH 27/97] slightly less verbose shift_time_unit/3 --- lib/elixir/lib/calendar/duration.ex | 7 +++++-- lib/elixir/lib/calendar/iso.ex | 14 ++++---------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index ae1c851ac8f..a98a0096dce 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -3,7 +3,10 @@ defmodule Calendar.Duration do The calendar Duration type. """ - defstruct year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0 + @default [year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0] + @fields Keyword.keys(@default) + + defstruct @default @typedoc "Duration in calendar units" @type t :: %__MODULE__{ @@ -43,7 +46,7 @@ defmodule Calendar.Duration do """ @spec new([unit]) :: {:ok, t} | {:error, :invalid_duration} def new(units) do - case Keyword.validate(units, Map.keys(%__MODULE__{}) -- [:__struct__]) do + case Keyword.validate(units, @fields) do {:ok, units} -> {:ok, struct(__MODULE__, units)} diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index b3af51520a7..df75acc0976 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1491,11 +1491,8 @@ defmodule Calendar.ISO do {:day, value}, naive_datetime -> shift_days(naive_datetime, value) - {:second, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, :second) - - {:microsecond, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, :microsecond) + {time_unit, value}, naive_datetime -> + shift_time_unit(naive_datetime, value, time_unit) end) {year, month, day} @@ -1540,11 +1537,8 @@ defmodule Calendar.ISO do {:month, value}, naive_datetime -> shift_months(naive_datetime, value) - {:second, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, :second) - - {:microsecond, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, :microsecond) + {time_unit, value}, naive_datetime -> + shift_time_unit(naive_datetime, value, time_unit) end) end From a9aa6b1c458db17cb8fc08f323168f86187d93e8 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 00:18:01 +0100 Subject: [PATCH 28/97] Calendar.Duration -> Duration --- lib/elixir/lib/calendar.ex | 6 +- lib/elixir/lib/calendar/date.ex | 6 +- lib/elixir/lib/calendar/datetime.ex | 6 +- lib/elixir/lib/calendar/duration.ex | 36 ++--- lib/elixir/lib/calendar/iso.ex | 30 ++-- lib/elixir/lib/calendar/naive_datetime.ex | 6 +- lib/elixir/lib/calendar/time.ex | 8 +- .../test/elixir/calendar/duration_test.exs | 64 ++++---- lib/elixir/test/elixir/calendar/iso_test.exs | 146 ++++++------------ 9 files changed, 127 insertions(+), 181 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 598927f633a..3a98b997505 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -341,7 +341,7 @@ defmodule Calendar do @doc """ Shifts date by given duration according to its calendar. """ - @callback shift_date(year, month, day, Calendar.Duration.t()) :: {year, month, day} + @callback shift_date(year, month, day, Duration.t()) :: {year, month, day} @doc """ Shifts naive datetime by given duration according to its calendar. @@ -354,13 +354,13 @@ defmodule Calendar do minute, second, microsecond, - Calendar.Duration.t() + Duration.t() ) :: {year, month, day, hour, minute, second, microsecond} @doc """ Shifts time by given duration according to its calendar. """ - @callback shift_time(hour, minute, second, microsecond, Calendar.Duration.t()) :: + @callback shift_time(hour, minute, second, microsecond, Duration.t()) :: {hour, minute, second, microsecond} # General Helpers diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index c5732299ad7..4a418b9458b 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -775,16 +775,16 @@ defmodule Date do {:ok, ~D[2020-02-01]} """ - @spec shift(Calendar.date(), Calendar.Duration.t() | [Calendar.Duration.unit()]) :: + @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: {:ok, t} | {:error, :invalid_duration} - def shift(%Date{calendar: calendar} = date, %Calendar.Duration{} = duration) do + def shift(%Date{calendar: calendar} = date, %Duration{} = duration) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} end def shift(%Date{} = date = date, duration_units) do - case Calendar.Duration.new(duration_units) do + case Duration.new(duration_units) do {:ok, duration} -> shift(date, duration) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 0ae67e2ef26..a89ccfe65aa 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1687,7 +1687,7 @@ defmodule DateTime do """ @spec shift( Calendar.datetime(), - Calendar.Duration.t() | [Calendar.Duration.unit()], + Duration.t() | [Duration.unit()], Calendar.time_zone_database() ) :: {:ok, t} @@ -1702,7 +1702,7 @@ defmodule DateTime do def shift( %DateTime{calendar: calendar} = datetime, - %Calendar.Duration{} = duration, + %Duration{} = duration, time_zone_database ) do %{ @@ -1737,7 +1737,7 @@ defmodule DateTime do end def shift(%DateTime{} = datetime, duration_units, time_zone_database) do - case Calendar.Duration.new(duration_units) do + case Duration.new(duration_units) do {:ok, duration} -> shift(datetime, duration, time_zone_database) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index a98a0096dce..3b91ca18b46 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -1,6 +1,6 @@ -defmodule Calendar.Duration do +defmodule Duration do @moduledoc """ - The calendar Duration type. + The Duration type. """ @default [year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0] @@ -32,15 +32,15 @@ defmodule Calendar.Duration do | {:microsecond, integer} @doc """ - Create `Calendar.Duration` struct from valid duration units. + Create `Duration` struct from valid duration units. Returns `{:error, :invalid_duration}` when called with invalid units. ## Examples - iex> Calendar.Duration.new(month: 2) - {:ok, %Calendar.Duration{month: 2}} - iex> Calendar.Duration.new(months: 2) + iex> Duration.new(month: 2) + {:ok, %Duration{month: 2}} + iex> Duration.new(months: 2) {:error, :invalid_duration} """ @@ -60,8 +60,8 @@ defmodule Calendar.Duration do ## Examples - iex> Calendar.Duration.new!(month: 2) - %Calendar.Duration{month: 2} + iex> Duration.new!(month: 2) + %Duration{month: 2} """ @spec new!([unit]) :: t @@ -74,8 +74,8 @@ defmodule Calendar.Duration do ## Examples - iex> Calendar.Duration.add(%Calendar.Duration{week: 2, day: 1}, %Calendar.Duration{day: 2}) - %Calendar.Duration{week: 2, day: 3} + iex> Duration.add(%Duration{week: 2, day: 1}, %Duration{day: 2}) + %Duration{week: 2, day: 3} """ @spec add(t, t) :: t @@ -97,8 +97,8 @@ defmodule Calendar.Duration do ## Examples - iex> Calendar.Duration.subtract(%Calendar.Duration{week: 2, day: 1}, %Calendar.Duration{day: 2}) - %Calendar.Duration{week: 2, day: -1} + iex> Duration.subtract(%Duration{week: 2, day: 1}, %Duration{day: 2}) + %Duration{week: 2, day: -1} """ @spec subtract(t, t) :: t @@ -116,12 +116,12 @@ defmodule Calendar.Duration do end @doc """ - Multiplies all Calendar.Duration units by given integer. + Multiplies all Duration units by given integer. ## Examples - iex> Calendar.Duration.multiply(%Calendar.Duration{day: 1, minute: 15, second: -10}, 3) - %Calendar.Duration{day: 3, minute: 45, second: -30} + iex> Duration.multiply(%Duration{day: 1, minute: 15, second: -10}, 3) + %Duration{day: 3, minute: 45, second: -30} """ @spec multiply(t, integer) :: t @@ -139,12 +139,12 @@ defmodule Calendar.Duration do end @doc """ - Negates all units of a Calendar.Duration. + Negates all units of a Duration. ## Examples - iex> Calendar.Duration.negate(%Calendar.Duration{day: 1, minute: 15, second: -10}) - %Calendar.Duration{day: -1, minute: -15, second: 10} + iex> Duration.negate(%Duration{day: 1, minute: 15, second: -10}) + %Duration{day: -1, minute: -15, second: 10} """ @spec negate(t) :: t diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index df75acc0976..039311679bb 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1456,7 +1456,7 @@ defmodule Calendar.ISO do end @doc """ - Shifts date by Calendar.Duration units according to its calendar. + Shifts date by Duration units according to its calendar. Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. @@ -1466,16 +1466,16 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_date(2016, 1, 3, Calendar.Duration.new!(month: 2)) + iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new!(month: 2)) {2016, 3, 3} - iex> Calendar.ISO.shift_date(2016, 2, 29, Calendar.Duration.new!(month: 1)) + iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new!(month: 1)) {2016, 3, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, Calendar.Duration.new!(month: 1)) + iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(month: 1)) {2016, 2, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, Calendar.Duration.new!(year: 4, day: 1)) + iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(year: 4, day: 1)) {2020, 2, 1} """ - @spec shift_date(year, month, day, Calendar.Duration.t()) :: {year, month, day} + @spec shift_date(year, month, day, Duration.t()) :: {year, month, day} @impl true def shift_date(year, month, day, duration) do shift_options = get_shift_options(:date, duration) @@ -1499,7 +1499,7 @@ defmodule Calendar.ISO do end @doc """ - Shifts naive datetime by Calendar.Duration units according to its calendar. + Shifts naive datetime by Duration units according to its calendar. Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. @@ -1509,11 +1509,11 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Calendar.Duration.new!(hour: 1)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 1)) {2016, 1, 3, 1, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Calendar.Duration.new!(hour: 30)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 30)) {2016, 1, 4, 6, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Calendar.Duration.new!(microsecond: 100)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(microsecond: 100)) {2016, 1, 3, 0, 0, 0, {100, 6}} """ @spec shift_naive_datetime( @@ -1524,7 +1524,7 @@ defmodule Calendar.ISO do minute, second, microsecond, - Calendar.Duration.t() + Duration.t() ) :: {year, month, day, hour, minute, second, microsecond} @impl true def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do @@ -1543,18 +1543,18 @@ defmodule Calendar.ISO do end @doc """ - Shifts time by Calendar.Duration units according to its calendar. + Shifts time by Duration units according to its calendar. Available units are: `:hour, :minute, :second, :microsecond`. ## Examples - iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Calendar.Duration.new!(hour: 2)) + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(hour: 2)) {15, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Calendar.Duration.new!(microsecond: 100)) + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(microsecond: 100)) {13, 0, 0, {100, 6}} """ - @spec shift_time(hour, minute, second, microsecond, Calendar.Duration.t()) :: + @spec shift_time(hour, minute, second, microsecond, Duration.t()) :: {hour, minute, second, microsecond} @impl true def shift_time(hour, minute, second, microsecond, duration) do diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 43931a3b003..0b6227b125f 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -590,9 +590,9 @@ defmodule NaiveDateTime do """ @spec shift( Calendar.naive_datetime(), - Calendar.Duration.t() | [Calendar.Duration.unit()] + Duration.t() | [Duration.unit()] ) :: {:ok, t} | {:error, :invalid_duration} - def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Calendar.Duration{} = duration) do + def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Duration{} = duration) do %{ year: year, month: month, @@ -629,7 +629,7 @@ defmodule NaiveDateTime do end def shift(%NaiveDateTime{} = naive_datetime, duration_units) do - case Calendar.Duration.new(duration_units) do + case Duration.new(duration_units) do {:ok, duration} -> shift(naive_datetime, duration) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 8a3bead2f64..cdb08a4ebd1 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -559,7 +559,7 @@ defmodule Time do end @doc """ - Shifts a Time by given Calendar.Duration according to its calendar. + Shifts a Time by given Duration according to its calendar. Check `Calendar.ISO.shift_time/4` for more information. @@ -575,9 +575,9 @@ defmodule Time do {:ok, ~T[01:00:15.000100]} """ - @spec shift(Calendar.time(), Calendar.Duration.t() | [Calendar.Duration.unit()]) :: + @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: {:ok, t} | {:error, :invalid_duration} - def shift(%Time{calendar: calendar} = time, %Calendar.Duration{} = duration) do + def shift(%Time{calendar: calendar} = time, %Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time {hour, minute, second, microsecond} = @@ -594,7 +594,7 @@ defmodule Time do end def shift(%Time{} = date = date, duration_units) do - case Calendar.Duration.new(duration_units) do + case Duration.new(duration_units) do {:ok, duration} -> shift(date, duration) diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index da6adf9d1b2..f5cfe2fe61d 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -1,30 +1,26 @@ Code.require_file("../test_helper.exs", __DIR__) -defmodule Calendar.DurationTest do +defmodule DurationTest do use ExUnit.Case, async: true - doctest Calendar.Duration + doctest Duration test "new/1" do - assert Calendar.Duration.new(year: 2, month: 1, week: 3) == - {:ok, %Calendar.Duration{year: 2, month: 1, week: 3}} + assert Duration.new(year: 2, month: 1, week: 3) == + {:ok, %Duration{year: 2, month: 1, week: 3}} - assert Calendar.Duration.new(months: 1) == {:error, :invalid_duration} + assert Duration.new(months: 1) == {:error, :invalid_duration} end test "new!/1" do - assert Calendar.Duration.new!(year: 2, month: 1, week: 3) == %Calendar.Duration{ - year: 2, - month: 1, - week: 3 - } + assert Duration.new!(year: 2, month: 1, week: 3) == %Duration{year: 2, month: 1, week: 3} assert_raise KeyError, ~s/key :months not found/, fn -> - Calendar.Duration.new!(months: 1) + Duration.new!(months: 1) end end test "add/2" do - d1 = %Calendar.Duration{ + d1 = %Duration{ year: 1, month: 2, week: 3, @@ -35,7 +31,7 @@ defmodule Calendar.DurationTest do microsecond: 8 } - d2 = %Calendar.Duration{ + d2 = %Duration{ year: 8, month: 7, week: 6, @@ -46,7 +42,7 @@ defmodule Calendar.DurationTest do microsecond: 1 } - assert Calendar.Duration.add(d1, d2) == %Calendar.Duration{ + assert Duration.add(d1, d2) == %Duration{ year: 9, month: 9, week: 9, @@ -57,12 +53,12 @@ defmodule Calendar.DurationTest do microsecond: 9 } - assert Calendar.Duration.add(d1, d2) == Calendar.Duration.add(d2, d1) + assert Duration.add(d1, d2) == Duration.add(d2, d1) - d1 = %Calendar.Duration{month: 2, week: 3, day: 4} - d2 = %Calendar.Duration{year: 8, day: 2, second: 2} + d1 = %Duration{month: 2, week: 3, day: 4} + d2 = %Duration{year: 8, day: 2, second: 2} - assert Calendar.Duration.add(d1, d2) == %Calendar.Duration{ + assert Duration.add(d1, d2) == %Duration{ year: 8, month: 2, week: 3, @@ -75,7 +71,7 @@ defmodule Calendar.DurationTest do end test "subtract/2" do - d1 = %Calendar.Duration{ + d1 = %Duration{ year: 1, month: 2, week: 3, @@ -86,7 +82,7 @@ defmodule Calendar.DurationTest do microsecond: 8 } - d2 = %Calendar.Duration{ + d2 = %Duration{ year: 8, month: 7, week: 6, @@ -97,7 +93,7 @@ defmodule Calendar.DurationTest do microsecond: 1 } - assert Calendar.Duration.subtract(d1, d2) == %Calendar.Duration{ + assert Duration.subtract(d1, d2) == %Duration{ year: -7, month: -5, week: -3, @@ -108,7 +104,7 @@ defmodule Calendar.DurationTest do microsecond: 7 } - assert Calendar.Duration.subtract(d2, d1) == %Calendar.Duration{ + assert Duration.subtract(d2, d1) == %Duration{ year: 7, month: 5, week: 3, @@ -119,12 +115,12 @@ defmodule Calendar.DurationTest do microsecond: -7 } - assert Calendar.Duration.subtract(d1, d2) != Calendar.Duration.subtract(d2, d1) + assert Duration.subtract(d1, d2) != Duration.subtract(d2, d1) - d1 = %Calendar.Duration{year: 10, month: 2, week: 3, day: 4} - d2 = %Calendar.Duration{year: 8, day: 2, second: 2} + d1 = %Duration{year: 10, month: 2, week: 3, day: 4} + d2 = %Duration{year: 8, day: 2, second: 2} - assert Calendar.Duration.subtract(d1, d2) == %Calendar.Duration{ + assert Duration.subtract(d1, d2) == %Duration{ year: 2, month: 2, week: 3, @@ -137,7 +133,7 @@ defmodule Calendar.DurationTest do end test "multiply/2" do - duration = %Calendar.Duration{ + duration = %Duration{ year: 1, month: 2, week: 3, @@ -148,7 +144,7 @@ defmodule Calendar.DurationTest do microsecond: 8 } - assert Calendar.Duration.multiply(duration, 3) == %Calendar.Duration{ + assert Duration.multiply(duration, 3) == %Duration{ year: 3, month: 6, week: 9, @@ -159,8 +155,8 @@ defmodule Calendar.DurationTest do microsecond: 24 } - assert Calendar.Duration.multiply(%Calendar.Duration{year: 2, day: 4, minute: 5}, 4) == - %Calendar.Duration{ + assert Duration.multiply(%Duration{year: 2, day: 4, minute: 5}, 4) == + %Duration{ year: 8, month: 0, week: 0, @@ -173,7 +169,7 @@ defmodule Calendar.DurationTest do end test "negate/1" do - duration = %Calendar.Duration{ + duration = %Duration{ year: 1, month: 2, week: 3, @@ -184,7 +180,7 @@ defmodule Calendar.DurationTest do microsecond: 8 } - assert Calendar.Duration.negate(duration) == %Calendar.Duration{ + assert Duration.negate(duration) == %Duration{ year: -1, month: -2, week: -3, @@ -195,8 +191,8 @@ defmodule Calendar.DurationTest do microsecond: -8 } - assert Calendar.Duration.negate(%Calendar.Duration{year: 2, day: 4, minute: 5}) == - %Calendar.Duration{ + assert Duration.negate(%Duration{year: 2, day: 4, minute: 5}) == + %Duration{ year: -2, month: 0, week: 0, diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index c8959e610a0..96e493b5fe0 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -429,93 +429,43 @@ defmodule Calendar.ISOTest do end end - describe "shift_date/2" do - test "regular use" do - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!([])) == {2024, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(year: 1)) == {2025, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(month: 2)) == {2024, 5, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(week: 3)) == {2024, 3, 23} - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(day: 5)) == {2024, 3, 7} - - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(hour: 24)) == {2024, 3, 3} - - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(minute: 1440)) == - {2024, 3, 3} - - assert Calendar.ISO.shift_date(2024, 3, 2, Calendar.Duration.new!(second: 86400)) == - {2024, 3, 3} - - assert Calendar.ISO.shift_date( - 2024, - 3, - 2, - Calendar.Duration.new!(microsecond: 86400 * 1_000_000) - ) == {2024, 3, 3} - - assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(month: 1)) == {0, 2, 1} - assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(year: 1)) == {1, 1, 1} - - assert Calendar.ISO.shift_date(0, 1, 1, Calendar.Duration.new!(year: -2, month: 2)) == - {-2, 3, 1} - - assert Calendar.ISO.shift_date(-4, 1, 1, Calendar.Duration.new!(year: -1)) == {-5, 1, 1} - - assert Calendar.ISO.shift_date( - 2024, - 3, - 2, - Calendar.Duration.new!(year: 1, month: 2, week: 3, day: 5) - ) == - {2025, 5, 28} - - assert Calendar.ISO.shift_date( - 2024, - 3, - 2, - Calendar.Duration.new!(year: -1, month: -2, week: -3) - ) == - {2022, 12, 12} - end - - test "leap year" do - assert Calendar.ISO.shift_date(2020, 2, 28, Calendar.Duration.new!(day: 1)) == {2020, 2, 29} - - assert Calendar.ISO.shift_date(2020, 2, 29, Calendar.Duration.new!(year: 1)) == - {2021, 2, 28} - - assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -1)) == - {2024, 2, 29} - - assert Calendar.ISO.shift_date(2024, 3, 31, Calendar.Duration.new!(month: -2)) == - {2024, 1, 31} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 1)) == - {2024, 2, 29} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 2)) == - {2024, 3, 31} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 3)) == - {2024, 4, 30} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 4)) == - {2024, 5, 31} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 5)) == - {2024, 6, 30} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 6)) == - {2024, 7, 31} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 7)) == - {2024, 8, 31} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 8)) == - {2024, 9, 30} - - assert Calendar.ISO.shift_date(2024, 1, 31, Calendar.Duration.new!(month: 9)) == - {2024, 10, 31} - end + test "shift_date/2" do + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!([])) == {2024, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: 1)) == {2025, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(month: 2)) == {2024, 5, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(week: 3)) == {2024, 3, 23} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(day: 5)) == {2024, 3, 7} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(hour: 24)) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(minute: 1440)) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(second: 86400)) == {2024, 3, 3} + + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(microsecond: 86400 * 1_000_000)) == + {2024, 3, 3} + + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(month: 1)) == {0, 2, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(year: 1)) == {1, 1, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(year: -2, month: 2)) == {-2, 3, 1} + assert Calendar.ISO.shift_date(-4, 1, 1, Duration.new!(year: -1)) == {-5, 1, 1} + + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: 1, month: 2, week: 3, day: 5)) == + {2025, 5, 28} + + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: -1, month: -2, week: -3)) == + {2022, 12, 12} + + assert Calendar.ISO.shift_date(2020, 2, 28, Duration.new!(day: 1)) == {2020, 2, 29} + assert Calendar.ISO.shift_date(2020, 2, 29, Duration.new!(year: 1)) == {2021, 2, 28} + assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new!(month: -1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new!(month: -2)) == {2024, 1, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 2)) == {2024, 3, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 3)) == {2024, 4, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 4)) == {2024, 5, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 5)) == {2024, 6, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 6)) == {2024, 7, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 7)) == {2024, 8, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 8)) == {2024, 9, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 9)) == {2024, 10, 31} end describe "shift_naive_datetime/2" do @@ -528,7 +478,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!([]) + Duration.new!([]) ) == {2024, 3, 2, 0, 0, 0, {0, 0}} end @@ -541,7 +491,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(year: 1) + Duration.new!(year: 1) ) == {2001, 1, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -552,7 +502,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: 1) + Duration.new!(month: 1) ) == {2000, 2, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -563,7 +513,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: 1, day: 28) + Duration.new!(month: 1, day: 28) ) == {2000, 2, 29, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -574,7 +524,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: 1, day: 30) + Duration.new!(month: 1, day: 30) ) == {2000, 3, 2, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -585,7 +535,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: 2, day: 29) + Duration.new!(month: 2, day: 29) ) == {2000, 3, 30, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -596,7 +546,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(year: -1) + Duration.new!(year: -1) ) == {1999, 2, 28, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -607,7 +557,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: -1) + Duration.new!(month: -1) ) == {2000, 1, 29, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -618,7 +568,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: -1, day: -28) + Duration.new!(month: -1, day: -28) ) == {2000, 1, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -629,7 +579,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: -1, day: -30) + Duration.new!(month: -1, day: -30) ) == {1999, 12, 30, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -640,7 +590,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Calendar.Duration.new!(month: -1, day: -29) + Duration.new!(month: -1, day: -29) ) == {1999, 12, 31, 0, 0, 0, {0, 0}} end end From 05664389392fbefa307da9cb6ecda84c61dedf4d Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 00:20:47 +0100 Subject: [PATCH 29/97] flatten shift_naive_datetime iso test --- lib/elixir/test/elixir/calendar/iso_test.exs | 246 +++++++++---------- 1 file changed, 121 insertions(+), 125 deletions(-) diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 96e493b5fe0..fd3df5df927 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -468,130 +468,126 @@ defmodule Calendar.ISOTest do assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 9)) == {2024, 10, 31} end - describe "shift_naive_datetime/2" do - test "regular use" 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}} - end - - test "leap year" do - 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}} - 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}} end end From 7852620d7864030f8ab3d8219b595e0baa97cb6c Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 01:22:43 +0100 Subject: [PATCH 30/97] consistent style in duration.ex --- lib/elixir/lib/calendar/duration.ex | 30 ++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 3b91ca18b46..b9c6492a37f 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -9,7 +9,7 @@ defmodule Duration do defstruct @default @typedoc "Duration in calendar units" - @type t :: %__MODULE__{ + @type t :: %Duration{ year: integer, month: integer, week: integer, @@ -48,7 +48,7 @@ defmodule Duration do def new(units) do case Keyword.validate(units, @fields) do {:ok, units} -> - {:ok, struct(__MODULE__, units)} + {:ok, struct(Duration, units)} {:error, _invalid_keys} -> {:error, :invalid_duration} @@ -66,11 +66,11 @@ defmodule Duration do """ @spec new!([unit]) :: t def new!(units) do - struct!(__MODULE__, units) + struct!(Duration, units) end @doc """ - Adds two durations to one new duration. + Adds two durations. ## Examples @@ -79,8 +79,8 @@ defmodule Duration do """ @spec add(t, t) :: t - def add(%__MODULE__{} = d1, %__MODULE__{} = d2) do - %__MODULE__{ + def add(%Duration{} = d1, %Duration{} = d2) do + %Duration{ year: d1.year + d2.year, month: d1.month + d2.month, week: d1.week + d2.week, @@ -93,7 +93,7 @@ defmodule Duration do end @doc """ - Subtracts two durations to one new duration. + Subtracts two durations. ## Examples @@ -102,8 +102,8 @@ defmodule Duration do """ @spec subtract(t, t) :: t - def subtract(%__MODULE__{} = d1, %__MODULE__{} = d2) do - %__MODULE__{ + def subtract(%Duration{} = d1, %Duration{} = d2) do + %Duration{ year: d1.year - d2.year, month: d1.month - d2.month, week: d1.week - d2.week, @@ -116,7 +116,7 @@ defmodule Duration do end @doc """ - Multiplies all Duration units by given integer. + Multiplies all duration units by given integer. ## Examples @@ -125,8 +125,8 @@ defmodule Duration do """ @spec multiply(t, integer) :: t - def multiply(%__MODULE__{} = duration, integer) when is_integer(integer) do - %__MODULE__{ + def multiply(%Duration{} = duration, integer) when is_integer(integer) do + %Duration{ year: duration.year * integer, month: duration.month * integer, week: duration.week * integer, @@ -139,7 +139,7 @@ defmodule Duration do end @doc """ - Negates all units of a Duration. + Negates all duration units. ## Examples @@ -148,8 +148,8 @@ defmodule Duration do """ @spec negate(t) :: t - def negate(%__MODULE__{} = duration) do - %__MODULE__{ + def negate(%Duration{} = duration) do + %Duration{ year: -duration.year, month: -duration.month, week: -duration.week, From 92bc5ef832835059dd5e10f6efa1d5fc30d29b8c Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 02:00:27 +0100 Subject: [PATCH 31/97] add more duration public functions --- lib/elixir/lib/calendar/duration.ex | 94 +++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index b9c6492a37f..5a8e7457e9e 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -3,6 +3,8 @@ defmodule Duration do The Duration type. """ + @seconds_per_day 86400 + @default [year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0] @fields Keyword.keys(@default) @@ -160,4 +162,96 @@ defmodule Duration do microsecond: -duration.microsecond } end + + @doc """ + Compares two durations. + + Returns `:gt` if the first duration is longer than the second and `:lt` for vice versa. + If the two durations are equal in length in seconds `:eq` is returned. + + ## Examples + + iex> Duration.compare(%Duration{hour: 1, minute: 15}, %Duration{hour: 2, minute: -45}) + :eq + iex> Duration.compare(%Duration{year: 1, minute: 15}, %Duration{minute: 15}) + :gt + iex> Duration.compare(%Duration{day: 1, minute: 15}, %Duration{day: 2}) + :lt + + """ + @spec compare(t, t) :: :lt | :eq | :gt + def compare(%Duration{} = d1, %Duration{} = d2) do + case {to_seconds(d1), to_seconds(d2)} do + {first, second} when first > second -> :gt + {first, second} when first < second -> :lt + _ -> :eq + end + end + + @doc """ + Converts duration to seconds. + + ## Examples + + iex> Duration.to_seconds(%Duration{day: 1, minute: 15, second: -10}) + 87290 + + """ + @spec to_seconds(t) :: integer + def to_seconds(%Duration{ + year: year, + month: month, + week: week, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }) do + Enum.sum([ + year * 365 * @seconds_per_day, + month * 30 * @seconds_per_day, + week * 7 * @seconds_per_day, + day * @seconds_per_day, + hour * 60 * 60, + minute * 60, + second, + div(microsecond, 1_000_000) + ]) + end + + @doc """ + Converts seconds to duration. + + ## Examples + + iex> Duration.from_seconds(87290) + %Duration{day: 1, minute: 14, second: 50} + + """ + @spec from_seconds(integer) :: t + def from_seconds(seconds) do + {years, seconds} = div_rem(seconds, 365 * @seconds_per_day) + {months, seconds} = div_rem(seconds, 30 * @seconds_per_day) + {weeks, seconds} = div_rem(seconds, 7 * @seconds_per_day) + {days, seconds} = div_rem(seconds, @seconds_per_day) + {hours, seconds} = div_rem(seconds, 60 * 60) + {minutes, seconds} = div_rem(seconds, 60) + {seconds, microseconds} = div_rem(seconds, 1) + + %Duration{ + year: years, + month: months, + week: weeks, + day: days, + hour: hours, + minute: minutes, + second: seconds, + microsecond: microseconds * 1_000_000 + } + end + + defp div_rem(dividend, divisor) do + {div(dividend, divisor), rem(dividend, divisor)} + end end From c24286ba801e1d426ee5a0ed186bef1ecb631faf Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 02:52:52 +0100 Subject: [PATCH 32/97] comparison rounds to second --- lib/elixir/lib/calendar/duration.ex | 8 ++++--- .../test/elixir/calendar/duration_test.exs | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 5a8e7457e9e..63d93b9dd75 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -169,6 +169,8 @@ defmodule Duration do Returns `:gt` if the first duration is longer than the second and `:lt` for vice versa. If the two durations are equal in length in seconds `:eq` is returned. + Comparison is rounded down to the second. + ## Examples iex> Duration.compare(%Duration{hour: 1, minute: 15}, %Duration{hour: 2, minute: -45}) @@ -177,6 +179,8 @@ defmodule Duration do :gt iex> Duration.compare(%Duration{day: 1, minute: 15}, %Duration{day: 2}) :lt + iex> Duration.compare(%Duration{day: 1, microsecond: 15}, %Duration{day: 1}) + :eq """ @spec compare(t, t) :: :lt | :eq | :gt @@ -237,7 +241,6 @@ defmodule Duration do {days, seconds} = div_rem(seconds, @seconds_per_day) {hours, seconds} = div_rem(seconds, 60 * 60) {minutes, seconds} = div_rem(seconds, 60) - {seconds, microseconds} = div_rem(seconds, 1) %Duration{ year: years, @@ -246,8 +249,7 @@ defmodule Duration do day: days, hour: hours, minute: minutes, - second: seconds, - microsecond: microseconds * 1_000_000 + second: seconds } end diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index f5cfe2fe61d..2ee7994d0d1 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -203,4 +203,25 @@ defmodule DurationTest do microsecond: 0 } end + + test "compare/2" do + d1 = %Duration{year: 1, month: 2, second: 7} + d2 = %Duration{year: 1, month: 2, second: 7} + + assert Duration.compare(d1, d2) == :eq + + d1 = %Duration{year: 1, month: 2, day: 1, second: 7} + d2 = %Duration{year: 1, month: 2, second: 7} + + assert Duration.compare(d1, d2) == :gt + assert Duration.compare(d2, d1) == :lt + end + + test "from_seconds/2" do + assert Duration.from_seconds(36_806_407) == %Duration{year: 1, month: 2, day: 1, second: 7} + end + + test "to_seconds/2" do + assert Duration.to_seconds(%Duration{year: 1, month: 2, day: 1, second: 7}) == 36_806_407 + end end From be1fcd63978051a20109a31f39eede31c2ec3de8 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 07:22:27 +0100 Subject: [PATCH 33/97] add calendar callbacks for Duration.to_seconds/1 and Duration.from_seconds/1 --- lib/elixir/lib/calendar.ex | 10 +++++ lib/elixir/lib/calendar/duration.ex | 51 +++-------------------- lib/elixir/lib/calendar/iso.ex | 63 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 45 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 3a98b997505..8cff4eb51ce 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -338,6 +338,16 @@ defmodule Calendar do @doc since: "1.15.0" @callback iso_days_to_end_of_day(iso_days) :: iso_days + @doc """ + Converts seconds to duration. + """ + @callback duration_from_seconds(integer) :: Duration.t() + + @doc """ + Converts duration to seconds. + """ + @callback duration_to_seconds(Duration.t()) :: integer + @doc """ Shifts date by given duration according to its calendar. """ diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 63d93b9dd75..a922bc673f1 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -3,8 +3,6 @@ defmodule Duration do The Duration type. """ - @seconds_per_day 86400 - @default [year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0] @fields Keyword.keys(@default) @@ -201,27 +199,9 @@ defmodule Duration do 87290 """ - @spec to_seconds(t) :: integer - def to_seconds(%Duration{ - year: year, - month: month, - week: week, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - }) do - Enum.sum([ - year * 365 * @seconds_per_day, - month * 30 * @seconds_per_day, - week * 7 * @seconds_per_day, - day * @seconds_per_day, - hour * 60 * 60, - minute * 60, - second, - div(microsecond, 1_000_000) - ]) + @spec to_seconds(t, Calendar.calendar()) :: integer + def to_seconds(duration, calendar \\ Calendar.ISO) do + calendar.duration_to_seconds(duration) end @doc """ @@ -233,27 +213,8 @@ defmodule Duration do %Duration{day: 1, minute: 14, second: 50} """ - @spec from_seconds(integer) :: t - def from_seconds(seconds) do - {years, seconds} = div_rem(seconds, 365 * @seconds_per_day) - {months, seconds} = div_rem(seconds, 30 * @seconds_per_day) - {weeks, seconds} = div_rem(seconds, 7 * @seconds_per_day) - {days, seconds} = div_rem(seconds, @seconds_per_day) - {hours, seconds} = div_rem(seconds, 60 * 60) - {minutes, seconds} = div_rem(seconds, 60) - - %Duration{ - year: years, - month: months, - week: weeks, - day: days, - hour: hours, - minute: minutes, - second: seconds - } - end - - defp div_rem(dividend, divisor) do - {div(dividend, divisor), rem(dividend, divisor)} + @spec from_seconds(integer, Calendar.calendar()) :: t + def from_seconds(seconds, calendar \\ Calendar.ISO) do + calendar.duration_from_seconds(seconds) end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 039311679bb..ce28e98adae 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1415,6 +1415,69 @@ defmodule Calendar.ISO do "-" <> zero_pad(-val, count) end + @doc """ + Converts seconds to duration based on ISO time unit scale. + + ## Examples + + iex> Calendar.ISO.duration_from_seconds(87290) + %Duration{day: 1, minute: 14, second: 50} + + """ + @spec duration_from_seconds(integer) :: Duration.t() + @impl true + def duration_from_seconds(seconds) do + {years, seconds} = div_rem(seconds, 365 * @seconds_per_day) + {months, seconds} = div_rem(seconds, 30 * @seconds_per_day) + {weeks, seconds} = div_rem(seconds, 7 * @seconds_per_day) + {days, seconds} = div_rem(seconds, @seconds_per_day) + {hours, seconds} = div_rem(seconds, 60 * 60) + {minutes, seconds} = div_rem(seconds, 60) + + %Duration{ + year: years, + month: months, + week: weeks, + day: days, + hour: hours, + minute: minutes, + second: seconds + } + end + + @doc """ + Converts duration to seconds based on ISO time unit scale. + + ## Examples + + iex> Calendar.ISO.duration_to_seconds(%Duration{day: 1, minute: 14, second: 50}) + 87290 + + """ + @spec duration_to_seconds(Duration.t()) :: integer + @impl true + def duration_to_seconds(%Duration{ + year: year, + month: month, + week: week, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }) do + Enum.sum([ + year * 365 * @seconds_per_day, + month * 30 * @seconds_per_day, + week * 7 * @seconds_per_day, + day * @seconds_per_day, + hour * 60 * 60, + minute * 60, + second, + div(microsecond, 1_000_000) + ]) + end + @doc """ Converts the `t:Calendar.iso_days/0` to the first moment of the day. From f8844a458c552fc57d606a02a1084d22f80af042 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 07:24:09 +0100 Subject: [PATCH 34/97] consider microseconds in Duration.compare/2 --- lib/elixir/lib/calendar/duration.ex | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index a922bc673f1..e1f4df32274 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -167,8 +167,6 @@ defmodule Duration do Returns `:gt` if the first duration is longer than the second and `:lt` for vice versa. If the two durations are equal in length in seconds `:eq` is returned. - Comparison is rounded down to the second. - ## Examples iex> Duration.compare(%Duration{hour: 1, minute: 15}, %Duration{hour: 2, minute: -45}) @@ -178,12 +176,16 @@ defmodule Duration do iex> Duration.compare(%Duration{day: 1, minute: 15}, %Duration{day: 2}) :lt iex> Duration.compare(%Duration{day: 1, microsecond: 15}, %Duration{day: 1}) - :eq + :gt """ - @spec compare(t, t) :: :lt | :eq | :gt - def compare(%Duration{} = d1, %Duration{} = d2) do - case {to_seconds(d1), to_seconds(d2)} do + @spec compare(t, t, Calendar.calendar()) :: :lt | :eq | :gt + def compare( + %Duration{microsecond: m1} = d1, + %Duration{microsecond: m2} = d2, + calendar \\ Calendar.ISO + ) do + case {to_seconds(d1, calendar) + m1, to_seconds(d2, calendar) + m2} do {first, second} when first > second -> :gt {first, second} when first < second -> :lt _ -> :eq From 3b302f5ac68ee708b292adad8d90571c0598df26 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 08:23:05 +0100 Subject: [PATCH 35/97] drop duration utility functions --- lib/elixir/lib/calendar.ex | 10 --- lib/elixir/lib/calendar/duration.ex | 59 ----------------- lib/elixir/lib/calendar/iso.ex | 63 ------------------- .../test/elixir/calendar/duration_test.exs | 21 ------- 4 files changed, 153 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 8cff4eb51ce..3a98b997505 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -338,16 +338,6 @@ defmodule Calendar do @doc since: "1.15.0" @callback iso_days_to_end_of_day(iso_days) :: iso_days - @doc """ - Converts seconds to duration. - """ - @callback duration_from_seconds(integer) :: Duration.t() - - @doc """ - Converts duration to seconds. - """ - @callback duration_to_seconds(Duration.t()) :: integer - @doc """ Shifts date by given duration according to its calendar. """ diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index e1f4df32274..b9c6492a37f 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -160,63 +160,4 @@ defmodule Duration do microsecond: -duration.microsecond } end - - @doc """ - Compares two durations. - - Returns `:gt` if the first duration is longer than the second and `:lt` for vice versa. - If the two durations are equal in length in seconds `:eq` is returned. - - ## Examples - - iex> Duration.compare(%Duration{hour: 1, minute: 15}, %Duration{hour: 2, minute: -45}) - :eq - iex> Duration.compare(%Duration{year: 1, minute: 15}, %Duration{minute: 15}) - :gt - iex> Duration.compare(%Duration{day: 1, minute: 15}, %Duration{day: 2}) - :lt - iex> Duration.compare(%Duration{day: 1, microsecond: 15}, %Duration{day: 1}) - :gt - - """ - @spec compare(t, t, Calendar.calendar()) :: :lt | :eq | :gt - def compare( - %Duration{microsecond: m1} = d1, - %Duration{microsecond: m2} = d2, - calendar \\ Calendar.ISO - ) do - case {to_seconds(d1, calendar) + m1, to_seconds(d2, calendar) + m2} do - {first, second} when first > second -> :gt - {first, second} when first < second -> :lt - _ -> :eq - end - end - - @doc """ - Converts duration to seconds. - - ## Examples - - iex> Duration.to_seconds(%Duration{day: 1, minute: 15, second: -10}) - 87290 - - """ - @spec to_seconds(t, Calendar.calendar()) :: integer - def to_seconds(duration, calendar \\ Calendar.ISO) do - calendar.duration_to_seconds(duration) - end - - @doc """ - Converts seconds to duration. - - ## Examples - - iex> Duration.from_seconds(87290) - %Duration{day: 1, minute: 14, second: 50} - - """ - @spec from_seconds(integer, Calendar.calendar()) :: t - def from_seconds(seconds, calendar \\ Calendar.ISO) do - calendar.duration_from_seconds(seconds) - end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index ce28e98adae..039311679bb 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1415,69 +1415,6 @@ defmodule Calendar.ISO do "-" <> zero_pad(-val, count) end - @doc """ - Converts seconds to duration based on ISO time unit scale. - - ## Examples - - iex> Calendar.ISO.duration_from_seconds(87290) - %Duration{day: 1, minute: 14, second: 50} - - """ - @spec duration_from_seconds(integer) :: Duration.t() - @impl true - def duration_from_seconds(seconds) do - {years, seconds} = div_rem(seconds, 365 * @seconds_per_day) - {months, seconds} = div_rem(seconds, 30 * @seconds_per_day) - {weeks, seconds} = div_rem(seconds, 7 * @seconds_per_day) - {days, seconds} = div_rem(seconds, @seconds_per_day) - {hours, seconds} = div_rem(seconds, 60 * 60) - {minutes, seconds} = div_rem(seconds, 60) - - %Duration{ - year: years, - month: months, - week: weeks, - day: days, - hour: hours, - minute: minutes, - second: seconds - } - end - - @doc """ - Converts duration to seconds based on ISO time unit scale. - - ## Examples - - iex> Calendar.ISO.duration_to_seconds(%Duration{day: 1, minute: 14, second: 50}) - 87290 - - """ - @spec duration_to_seconds(Duration.t()) :: integer - @impl true - def duration_to_seconds(%Duration{ - year: year, - month: month, - week: week, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - }) do - Enum.sum([ - year * 365 * @seconds_per_day, - month * 30 * @seconds_per_day, - week * 7 * @seconds_per_day, - day * @seconds_per_day, - hour * 60 * 60, - minute * 60, - second, - div(microsecond, 1_000_000) - ]) - end - @doc """ Converts the `t:Calendar.iso_days/0` to the first moment of the day. diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 2ee7994d0d1..f5cfe2fe61d 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -203,25 +203,4 @@ defmodule DurationTest do microsecond: 0 } end - - test "compare/2" do - d1 = %Duration{year: 1, month: 2, second: 7} - d2 = %Duration{year: 1, month: 2, second: 7} - - assert Duration.compare(d1, d2) == :eq - - d1 = %Duration{year: 1, month: 2, day: 1, second: 7} - d2 = %Duration{year: 1, month: 2, second: 7} - - assert Duration.compare(d1, d2) == :gt - assert Duration.compare(d2, d1) == :lt - end - - test "from_seconds/2" do - assert Duration.from_seconds(36_806_407) == %Duration{year: 1, month: 2, day: 1, second: 7} - end - - test "to_seconds/2" do - assert Duration.to_seconds(%Duration{year: 1, month: 2, day: 1, second: 7}) == 36_806_407 - end end From 64f8af30fa9adf5aa8a94fec9f2bba9636f1b276 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 08:43:40 +0100 Subject: [PATCH 36/97] let shift functions raise when called with invalid units --- lib/elixir/lib/calendar/date.ex | 11 +-- lib/elixir/lib/calendar/datetime.ex | 11 +-- lib/elixir/lib/calendar/duration.ex | 33 +------- lib/elixir/lib/calendar/iso.ex | 18 ++--- lib/elixir/lib/calendar/naive_datetime.ex | 10 +-- lib/elixir/lib/calendar/time.ex | 11 +-- lib/elixir/test/elixir/calendar/date_test.exs | 4 +- .../test/elixir/calendar/datetime_test.exs | 4 + .../test/elixir/calendar/duration_test.exs | 11 +-- lib/elixir/test/elixir/calendar/iso_test.exs | 80 +++++++++---------- .../elixir/calendar/naive_datetime_test.exs | 4 +- lib/elixir/test/elixir/calendar/time_test.exs | 4 + 12 files changed, 77 insertions(+), 124 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 4a418b9458b..3150d6ed16c 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -775,8 +775,7 @@ defmodule Date do {:ok, ~D[2020-02-01]} """ - @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: - {:ok, t} | {:error, :invalid_duration} + @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: {:ok, t} def shift(%Date{calendar: calendar} = date, %Duration{} = duration) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) @@ -784,13 +783,7 @@ defmodule Date do end def shift(%Date{} = date = date, duration_units) do - case Duration.new(duration_units) do - {:ok, duration} -> - shift(date, duration) - - {:error, :invalid_duration} -> - {:error, :invalid_duration} - end + shift(date, Duration.new(duration_units)) end @doc false diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index a89ccfe65aa..a7d1dd26710 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1696,8 +1696,7 @@ defmodule DateTime do | {:error, :incompatible_calendars | :time_zone_not_found - | :utc_only_time_zone_database - | :invalid_duration} + | :utc_only_time_zone_database} def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) def shift( @@ -1737,13 +1736,7 @@ defmodule DateTime do end def shift(%DateTime{} = datetime, duration_units, time_zone_database) do - case Duration.new(duration_units) do - {:ok, duration} -> - shift(datetime, duration, time_zone_database) - - {:error, :invalid_duration} -> - {:error, :invalid_duration} - end + shift(datetime, Duration.new(duration_units), time_zone_database) end @doc """ diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index b9c6492a37f..c4a38d3987f 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -3,10 +3,7 @@ defmodule Duration do The Duration type. """ - @default [year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0] - @fields Keyword.keys(@default) - - defstruct @default + defstruct year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0 @typedoc "Duration in calendar units" @type t :: %Duration{ @@ -34,38 +31,16 @@ defmodule Duration do @doc """ Create `Duration` struct from valid duration units. - Returns `{:error, :invalid_duration}` when called with invalid units. + Raises a KeyError when called with invalid units. ## Examples iex> Duration.new(month: 2) - {:ok, %Duration{month: 2}} - iex> Duration.new(months: 2) - {:error, :invalid_duration} - - """ - @spec new([unit]) :: {:ok, t} | {:error, :invalid_duration} - def new(units) do - case Keyword.validate(units, @fields) do - {:ok, units} -> - {:ok, struct(Duration, units)} - - {:error, _invalid_keys} -> - {:error, :invalid_duration} - end - end - - @doc """ - Same as `new/1` but raises a KeyError when called with invalid units. - - ## Examples - - iex> Duration.new!(month: 2) %Duration{month: 2} """ - @spec new!([unit]) :: t - def new!(units) do + @spec new([unit]) :: t + def new(units) do struct!(Duration, units) end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 039311679bb..ff62a512bfa 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1466,13 +1466,13 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new!(month: 2)) + iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new(month: 2)) {2016, 3, 3} - iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new!(month: 1)) + iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new(month: 1)) {2016, 3, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(month: 1)) + iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new(month: 1)) {2016, 2, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(year: 4, day: 1)) + iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new(year: 4, day: 1)) {2020, 2, 1} """ @spec shift_date(year, month, day, Duration.t()) :: {year, month, day} @@ -1509,11 +1509,11 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 1)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(hour: 1)) {2016, 1, 3, 1, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 30)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(hour: 30)) {2016, 1, 4, 6, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(microsecond: 100)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(microsecond: 100)) {2016, 1, 3, 0, 0, 0, {100, 6}} """ @spec shift_naive_datetime( @@ -1549,9 +1549,9 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(hour: 2)) + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(hour: 2)) {15, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(microsecond: 100)) + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(microsecond: 100)) {13, 0, 0, {100, 6}} """ @spec shift_time(hour, minute, second, microsecond, Duration.t()) :: diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 0b6227b125f..6a6eb59844e 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -591,7 +591,7 @@ defmodule NaiveDateTime do @spec shift( Calendar.naive_datetime(), Duration.t() | [Duration.unit()] - ) :: {:ok, t} | {:error, :invalid_duration} + ) :: {:ok, t} def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Duration{} = duration) do %{ year: year, @@ -629,13 +629,7 @@ defmodule NaiveDateTime do end def shift(%NaiveDateTime{} = naive_datetime, duration_units) do - case Duration.new(duration_units) do - {:ok, duration} -> - shift(naive_datetime, duration) - - {:error, :invalid_duration} -> - {:error, :invalid_duration} - end + shift(naive_datetime, Duration.new(duration_units)) end @doc """ diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index cdb08a4ebd1..db5042c2f5a 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -575,8 +575,7 @@ defmodule Time do {:ok, ~T[01:00:15.000100]} """ - @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: - {:ok, t} | {:error, :invalid_duration} + @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: {:ok, t} def shift(%Time{calendar: calendar} = time, %Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time @@ -594,13 +593,7 @@ defmodule Time do end def shift(%Time{} = date = date, duration_units) do - case Duration.new(duration_units) do - {:ok, duration} -> - shift(date, duration) - - {:error, :invalid_duration} -> - {:error, :invalid_duration} - end + shift(date, Duration.new(duration_units)) end @doc """ diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index 90bf07544c8..97f043d866d 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -195,7 +195,9 @@ defmodule DateTest do assert Date.shift(~D[2000-01-01], month: 12) == {:ok, ~D[2001-01-01]} assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} - assert Date.shift(~D[2012-01-01], months: 12) == {:error, :invalid_duration} + assert_raise KeyError, ~s/key :months not found/, fn -> + Date.shift(~D[2012-01-01], months: 12) + end assert_raise UndefinedFunctionError, fn -> date = Calendar.Holocene.date(12000, 01, 01) diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index 2d1160d5a6e..69e968d1324 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1107,5 +1107,9 @@ defmodule DateTimeTest do assert {:ambiguous, %DateTime{}, %DateTime{}} = DateTime.shift(datetime, [day: 1], FakeTimeZoneDatabase) + + assert_raise KeyError, ~s/key :months not found/, fn -> + DateTime.shift(~U[2012-01-01 00:00:00Z], months: 12) + end end end diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index f5cfe2fe61d..e18352e3ed5 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -5,17 +5,10 @@ defmodule DurationTest do doctest Duration test "new/1" do - assert Duration.new(year: 2, month: 1, week: 3) == - {:ok, %Duration{year: 2, month: 1, week: 3}} - - assert Duration.new(months: 1) == {:error, :invalid_duration} - end - - test "new!/1" do - assert Duration.new!(year: 2, month: 1, week: 3) == %Duration{year: 2, month: 1, week: 3} + assert Duration.new(year: 2, month: 1, week: 3) == %Duration{year: 2, month: 1, week: 3} assert_raise KeyError, ~s/key :months not found/, fn -> - Duration.new!(months: 1) + Duration.new(months: 1) end end diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index fd3df5df927..75e05dbfde9 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -430,42 +430,42 @@ defmodule Calendar.ISOTest do end test "shift_date/2" do - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!([])) == {2024, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: 1)) == {2025, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(month: 2)) == {2024, 5, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(week: 3)) == {2024, 3, 23} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(day: 5)) == {2024, 3, 7} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(hour: 24)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(minute: 1440)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(second: 86400)) == {2024, 3, 3} - - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(microsecond: 86400 * 1_000_000)) == + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new([])) == {2024, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(year: 1)) == {2025, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(month: 2)) == {2024, 5, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(week: 3)) == {2024, 3, 23} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(day: 5)) == {2024, 3, 7} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(hour: 24)) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(minute: 1440)) == {2024, 3, 3} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(second: 86400)) == {2024, 3, 3} + + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(microsecond: 86400 * 1_000_000)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(month: 1)) == {0, 2, 1} - assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(year: 1)) == {1, 1, 1} - assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(year: -2, month: 2)) == {-2, 3, 1} - assert Calendar.ISO.shift_date(-4, 1, 1, Duration.new!(year: -1)) == {-5, 1, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(month: 1)) == {0, 2, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(year: 1)) == {1, 1, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(year: -2, month: 2)) == {-2, 3, 1} + assert Calendar.ISO.shift_date(-4, 1, 1, Duration.new(year: -1)) == {-5, 1, 1} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: 1, month: 2, week: 3, day: 5)) == + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(year: 1, month: 2, week: 3, day: 5)) == {2025, 5, 28} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: -1, month: -2, week: -3)) == + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(year: -1, month: -2, week: -3)) == {2022, 12, 12} - assert Calendar.ISO.shift_date(2020, 2, 28, Duration.new!(day: 1)) == {2020, 2, 29} - assert Calendar.ISO.shift_date(2020, 2, 29, Duration.new!(year: 1)) == {2021, 2, 28} - assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new!(month: -1)) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new!(month: -2)) == {2024, 1, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 1)) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 2)) == {2024, 3, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 3)) == {2024, 4, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 4)) == {2024, 5, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 5)) == {2024, 6, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 6)) == {2024, 7, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 7)) == {2024, 8, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 8)) == {2024, 9, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 9)) == {2024, 10, 31} + assert Calendar.ISO.shift_date(2020, 2, 28, Duration.new(day: 1)) == {2020, 2, 29} + assert Calendar.ISO.shift_date(2020, 2, 29, Duration.new(year: 1)) == {2021, 2, 28} + assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new(month: -1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new(month: -2)) == {2024, 1, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 2)) == {2024, 3, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 3)) == {2024, 4, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 4)) == {2024, 5, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 5)) == {2024, 6, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 6)) == {2024, 7, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 7)) == {2024, 8, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 8)) == {2024, 9, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 9)) == {2024, 10, 31} end test "shift_naive_datetime/2" do @@ -477,7 +477,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!([]) + Duration.new([]) ) == {2024, 3, 2, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -488,7 +488,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(year: 1) + Duration.new(year: 1) ) == {2001, 1, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -499,7 +499,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: 1) + Duration.new(month: 1) ) == {2000, 2, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -510,7 +510,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: 1, day: 28) + Duration.new(month: 1, day: 28) ) == {2000, 2, 29, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -521,7 +521,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: 1, day: 30) + Duration.new(month: 1, day: 30) ) == {2000, 3, 2, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -532,7 +532,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: 2, day: 29) + Duration.new(month: 2, day: 29) ) == {2000, 3, 30, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -543,7 +543,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(year: -1) + Duration.new(year: -1) ) == {1999, 2, 28, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -554,7 +554,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: -1) + Duration.new(month: -1) ) == {2000, 1, 29, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -565,7 +565,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: -1, day: -28) + Duration.new(month: -1, day: -28) ) == {2000, 1, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -576,7 +576,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: -1, day: -30) + Duration.new(month: -1, day: -30) ) == {1999, 12, 30, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -587,7 +587,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new!(month: -1, day: -29) + Duration.new(month: -1, day: -29) ) == {1999, 12, 31, 0, 0, 0, {0, 0}} end end diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index cfc39358a8b..1749d0fc2ce 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -444,6 +444,8 @@ defmodule NaiveDateTimeTest do microsecond: -8 ) == {:ok, ~N[1998-10-06 18:53:52.999992]} - assert NaiveDateTime.shift(naive_datetime, months: 12) == {:error, :invalid_duration} + assert_raise KeyError, ~s/key :months not found/, fn -> + NaiveDateTime.shift(naive_datetime, months: 12) + end end end diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 530042a065e..9b6c97376bd 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -111,5 +111,9 @@ defmodule TimeTest do assert Time.shift(time, second: 50) == {:ok, ~T[00:00:50.0]} assert Time.shift(time, microsecond: 150) == {:ok, ~T[00:00:00.000150]} assert Time.shift(time, hour: 2, minute: 65, second: 5) == {:ok, ~T[03:05:05.0]} + + assert_raise KeyError, ~s/key :hours not found/, fn -> + Time.shift(time, hours: 12) + end end end From f78d99d97e272c0d98a0357fd5fe529457d9cf96 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 09:06:38 +0100 Subject: [PATCH 37/97] prevent shifting date by time units --- lib/elixir/lib/calendar/date.ex | 10 ++- lib/elixir/lib/calendar/duration.ex | 8 ++ lib/elixir/lib/calendar/iso.ex | 78 ++++++------------- lib/elixir/test/elixir/calendar/date_test.exs | 5 +- lib/elixir/test/elixir/calendar/iso_test.exs | 6 -- 5 files changed, 44 insertions(+), 63 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 3150d6ed16c..84df9fab1cb 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -782,8 +782,14 @@ defmodule Date do {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} end - def shift(%Date{} = date = date, duration_units) do - shift(date, Duration.new(duration_units)) + def shift(%Date{} = date, duration_units) do + case Duration.invalid_keys(duration_units, :date) do + [] -> + shift(date, Duration.new(duration_units)) + + invalid_units -> + raise ArgumentError, "cannot shift date by time units: #{inspect(invalid_units)}" + end end @doc false diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index c4a38d3987f..7331cb828cd 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -135,4 +135,12 @@ defmodule Duration do microsecond: -duration.microsecond } end + + @doc false + def invalid_keys(duration_units, :date) do + Enum.filter( + [:hour, :minute, :second, :microsecond], + &Keyword.has_key?(duration_units, &1) + ) + end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index ff62a512bfa..f073c4d77a8 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1456,13 +1456,7 @@ defmodule Calendar.ISO do end @doc """ - Shifts date by Duration units according to its calendar. - - Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - - When shifting by month: - - it will shift to the current day of a month - - when the current day does not exist in a month, it will shift to the last day of a month + Shift Date by Duration according to its calendar. ## Examples @@ -1480,32 +1474,20 @@ defmodule Calendar.ISO do def shift_date(year, month, day, duration) do shift_options = get_shift_options(:date, duration) - {year, month, day, _, _, _, _} = - Enum.reduce(shift_options, {year, month, day, 0, 0, 0, {0, 0}}, fn - {_, 0}, naive_datetime -> - naive_datetime - - {:month, value}, naive_datetime -> - shift_months(naive_datetime, value) - - {:day, value}, naive_datetime -> - shift_days(naive_datetime, value) + Enum.reduce(shift_options, {year, month, day}, fn + {_, 0}, naive_datetime -> + naive_datetime - {time_unit, value}, naive_datetime -> - shift_time_unit(naive_datetime, value, time_unit) - end) + {:month, value}, naive_datetime -> + shift_months(naive_datetime, value) - {year, month, day} + {:day, value}, naive_datetime -> + shift_days(naive_datetime, value) + end) end @doc """ - Shifts naive datetime by Duration units according to its calendar. - - Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - - When shifting by month: - - it will shift to the current day of a month - - when the current day does not exist in a month, it will shift to the last day of a month + Shift NaiveDateTime by Duration according to its calendar. ## Examples @@ -1534,8 +1516,9 @@ defmodule Calendar.ISO do {_, 0}, naive_datetime -> naive_datetime - {:month, value}, naive_datetime -> - shift_months(naive_datetime, value) + {: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} {time_unit, value}, naive_datetime -> shift_time_unit(naive_datetime, value, time_unit) @@ -1572,16 +1555,16 @@ defmodule Calendar.ISO do end) end - defp shift_days({year, month, day, hour, minute, second, microsecond}, days) do + defp shift_days({year, month, day}, days) do {year, month, day} = date_to_iso_days(year, month, day) |> Kernel.+(days) |> date_from_iso_days() - {year, month, day, hour, minute, second, microsecond} + {year, month, day} end - defp shift_months({year, month, day, hour, minute, second, microsecond}, months) do + defp shift_months({year, month, day}, months) do months_in_year = 12 total_months = year * months_in_year + month + months - 1 @@ -1596,15 +1579,13 @@ defmodule Calendar.ISO do new_day = min(day, days_in_month(new_year, new_month)) - {new_year, new_month, new_day, hour, minute, second, microsecond} + {new_year, new_month, new_day} end - defp shift_time_unit( - {year, month, day, hour, minute, second, {_, ms_precision} = microsecond}, - value, - unit - ) + defp shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) when unit in [:second, :microsecond] do + {_, ms_precision} = microsecond + ppd = System.convert_time_unit(86400, :second, unit) precision = max(time_unit_to_precision(unit), ms_precision) @@ -1633,25 +1614,14 @@ defmodule Calendar.ISO do {hour, minute, second, {microsecond, precision}} end - defp get_shift_options(:date, %{ - year: year, - month: month, - week: week, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - }) do + defp get_shift_options(:date, %Duration{year: year, month: month, week: week, day: day}) do [ month: year * 12 + month, - day: week * 7 + day, - second: hour * 3600 + minute * 60 + second, - microsecond: microsecond + day: week * 7 + day ] end - defp get_shift_options(:naive_datetime, %{ + defp get_shift_options(:naive_datetime, %Duration{ year: year, month: month, week: week, @@ -1668,7 +1638,7 @@ defmodule Calendar.ISO do ] end - defp get_shift_options(:time, %{ + defp get_shift_options(:time, %Duration{ hour: hour, minute: minute, second: second, diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index 97f043d866d..cd7b804b7ae 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -182,7 +182,6 @@ defmodule DateTest do test "shift/2" do assert Date.shift(~D[2012-02-29], day: -1) == {:ok, ~D[2012-02-28]} - assert Date.shift(~D[2012-02-29], second: 86400) == {:ok, ~D[2012-03-01]} assert Date.shift(~D[2012-02-29], month: -1) == {:ok, ~D[2012-01-29]} assert Date.shift(~D[2012-02-29], week: -9) == {:ok, ~D[2011-12-28]} assert Date.shift(~D[2012-02-29], month: 1) == {:ok, ~D[2012-03-29]} @@ -195,6 +194,10 @@ defmodule DateTest do assert Date.shift(~D[2000-01-01], month: 12) == {:ok, ~D[2001-01-01]} assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} + assert_raise ArgumentError, ~s/cannot shift date by time units: [:second]/, fn -> + Date.shift(~D[2012-02-29], second: 86400) + end + assert_raise KeyError, ~s/key :months not found/, fn -> Date.shift(~D[2012-01-01], months: 12) end diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 75e05dbfde9..8ec0d67bb96 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -435,12 +435,6 @@ defmodule Calendar.ISOTest do assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(month: 2)) == {2024, 5, 2} assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(week: 3)) == {2024, 3, 23} assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(day: 5)) == {2024, 3, 7} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(hour: 24)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(minute: 1440)) == {2024, 3, 3} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(second: 86400)) == {2024, 3, 3} - - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(microsecond: 86400 * 1_000_000)) == - {2024, 3, 3} assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(month: 1)) == {0, 2, 1} assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(year: 1)) == {1, 1, 1} From f008f70fa2f982e85f5f8173b7619d2f522db61f Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 09:09:43 +0100 Subject: [PATCH 38/97] prevent shifting time by date units --- lib/elixir/lib/calendar/duration.ex | 7 +++++++ lib/elixir/lib/calendar/time.ex | 10 ++++++++-- lib/elixir/test/elixir/calendar/time_test.exs | 4 ++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 7331cb828cd..6805b57a1d7 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -143,4 +143,11 @@ defmodule Duration do &Keyword.has_key?(duration_units, &1) ) end + + def invalid_keys(duration_units, :time) do + Enum.filter( + [:year, :month, :week, :day], + &Keyword.has_key?(duration_units, &1) + ) + end end diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index db5042c2f5a..06fce7b0fed 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -592,8 +592,14 @@ defmodule Time do }} end - def shift(%Time{} = date = date, duration_units) do - shift(date, Duration.new(duration_units)) + def shift(%Time{} = time, duration_units) do + case Duration.invalid_keys(duration_units, :time) do + [] -> + shift(time, Duration.new(duration_units)) + + invalid_units -> + raise ArgumentError, "cannot shift time by date units: #{inspect(invalid_units)}" + end end @doc """ diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 9b6c97376bd..1171bf89185 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -112,6 +112,10 @@ defmodule TimeTest do assert Time.shift(time, microsecond: 150) == {:ok, ~T[00:00:00.000150]} assert Time.shift(time, hour: 2, minute: 65, second: 5) == {:ok, ~T[03:05:05.0]} + assert_raise ArgumentError, ~s/cannot shift time by date units: [:day]/, fn -> + Time.shift(time, day: 1) + end + assert_raise KeyError, ~s/key :hours not found/, fn -> Time.shift(time, hours: 12) end From bf0fbc24eab3e9b02511e3ee1fa6c8336e7ae17f Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 09:18:27 +0100 Subject: [PATCH 39/97] spec invalid keys --- lib/elixir/lib/calendar/duration.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 6805b57a1d7..ec78a829952 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -137,6 +137,9 @@ defmodule Duration do end @doc false + @spec invalid_keys([unit], :date | :time) :: keyword() + def invalid_keys(duration_units, calendar_type) + def invalid_keys(duration_units, :date) do Enum.filter( [:hour, :minute, :second, :microsecond], From 75bcb42fe2b2dd383d7e4290f5cef66ef4cb4dcb Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 09:43:27 +0100 Subject: [PATCH 40/97] support millisecond in Duration --- lib/elixir/lib/calendar/duration.ex | 18 ++++++++++++++++-- lib/elixir/lib/calendar/iso.ex | 15 ++++++++------- .../elixir/calendar/naive_datetime_test.exs | 6 ++++++ lib/elixir/test/elixir/calendar/time_test.exs | 2 ++ 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index ec78a829952..f14f157c5d7 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -3,7 +3,15 @@ defmodule Duration do The Duration type. """ - defstruct year: 0, month: 0, week: 0, day: 0, hour: 0, minute: 0, second: 0, microsecond: 0 + defstruct year: 0, + month: 0, + week: 0, + day: 0, + hour: 0, + minute: 0, + second: 0, + millisecond: 0, + microsecond: 0 @typedoc "Duration in calendar units" @type t :: %Duration{ @@ -14,6 +22,7 @@ defmodule Duration do hour: integer, minute: integer, second: integer, + millisecond: integer, microsecond: integer } @@ -26,6 +35,7 @@ defmodule Duration do | {:hour, integer} | {:minute, integer} | {:second, integer} + | {:millisecond, integer} | {:microsecond, integer} @doc """ @@ -63,6 +73,7 @@ defmodule Duration do hour: d1.hour + d2.hour, minute: d1.minute + d2.minute, second: d1.second + d2.second, + millisecond: d1.millisecond + d2.millisecond, microsecond: d1.microsecond + d2.microsecond } end @@ -86,6 +97,7 @@ defmodule Duration do hour: d1.hour - d2.hour, minute: d1.minute - d2.minute, second: d1.second - d2.second, + millisecond: d1.millisecond - d2.millisecond, microsecond: d1.microsecond - d2.microsecond } end @@ -109,6 +121,7 @@ defmodule Duration do hour: duration.hour * integer, minute: duration.minute * integer, second: duration.second * integer, + millisecond: duration.millisecond * integer, microsecond: duration.microsecond * integer } end @@ -132,6 +145,7 @@ defmodule Duration do hour: -duration.hour, minute: -duration.minute, second: -duration.second, + millisecond: -duration.millisecond, microsecond: -duration.microsecond } end @@ -142,7 +156,7 @@ defmodule Duration do def invalid_keys(duration_units, :date) do Enum.filter( - [:hour, :minute, :second, :microsecond], + [:hour, :minute, :second, :millisecond, :microsecond], &Keyword.has_key?(duration_units, &1) ) end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index f073c4d77a8..ee0f17326a8 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1547,11 +1547,8 @@ defmodule Calendar.ISO do {_, 0}, time -> time - {:second, value}, time -> - shift_time_unit(time, value, :second) - - {:microsecond, value}, time -> - shift_time_unit(time, value, :microsecond) + {time_unit, value}, time -> + shift_time_unit(time, value, time_unit) end) end @@ -1583,7 +1580,7 @@ defmodule Calendar.ISO do end defp shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) - when unit in [:second, :microsecond] do + when unit in [:second, :millisecond, :microsecond] do {_, ms_precision} = microsecond ppd = System.convert_time_unit(86400, :second, unit) @@ -1602,7 +1599,7 @@ defmodule Calendar.ISO do value, unit ) - when unit in [:second, :microsecond] do + when unit in [:second, :millisecond, :microsecond] do time = {0, time_to_day_fraction(hour, minute, second, microsecond)} amount_to_add = System.convert_time_unit(value, unit, :microsecond) total = iso_days_to_unit(time, :microsecond) + amount_to_add @@ -1629,11 +1626,13 @@ defmodule Calendar.ISO do hour: hour, minute: minute, second: second, + millisecond: millisecond, microsecond: microsecond }) do [ month: year * 12 + month, second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second, + millisecond: millisecond, microsecond: microsecond ] end @@ -1642,10 +1641,12 @@ defmodule Calendar.ISO do hour: hour, minute: minute, second: second, + millisecond: millisecond, microsecond: microsecond }) do [ second: hour * 3600 + minute * 60 + second, + millisecond: millisecond, microsecond: microsecond ] end diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 1749d0fc2ce..8d61dd2368c 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -416,6 +416,12 @@ defmodule NaiveDateTimeTest do assert NaiveDateTime.shift(naive_datetime, microsecond: 500) == {:ok, ~N[2000-01-01 00:00:00.000500]} + assert NaiveDateTime.shift(naive_datetime, millisecond: 500) == + {:ok, ~N[2000-01-01 00:00:00.500]} + + assert NaiveDateTime.shift(naive_datetime, millisecond: 500, microsecond: 100) == + {:ok, ~N[2000-01-01 00:00:00.500100]} + assert NaiveDateTime.shift(naive_datetime, year: 1, month: 2) == {:ok, ~N[2001-03-01 00:00:00]} diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 1171bf89185..125e7a8b3c8 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -109,7 +109,9 @@ defmodule TimeTest do assert Time.shift(time, hour: 25) == {:ok, ~T[01:00:00.0]} assert Time.shift(time, minute: 25) == {:ok, ~T[00:25:00.0]} assert Time.shift(time, second: 50) == {:ok, ~T[00:00:50.0]} + assert Time.shift(time, millisecond: 150) == {:ok, ~T[00:00:00.150]} assert Time.shift(time, microsecond: 150) == {:ok, ~T[00:00:00.000150]} + assert Time.shift(time, millisecond: 150, microsecond: 100) == {:ok, ~T[00:00:00.150100]} assert Time.shift(time, hour: 2, minute: 65, second: 5) == {:ok, ~T[03:05:05.0]} assert_raise ArgumentError, ~s/cannot shift time by date units: [:day]/, fn -> From 65d6ee57ab7e69aee62a90a0d6c3835fb0d2174f Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 10:10:07 +0100 Subject: [PATCH 41/97] Duration.invalid_keys/2 -> Duration.invalid_units/2 --- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/duration.ex | 8 ++++---- lib/elixir/lib/calendar/time.ex | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 84df9fab1cb..80e9a224523 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -783,7 +783,7 @@ defmodule Date do end def shift(%Date{} = date, duration_units) do - case Duration.invalid_keys(duration_units, :date) do + case Duration.invalid_units(duration_units, :date) do [] -> shift(date, Duration.new(duration_units)) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index f14f157c5d7..062ada1ac0d 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -151,17 +151,17 @@ defmodule Duration do end @doc false - @spec invalid_keys([unit], :date | :time) :: keyword() - def invalid_keys(duration_units, calendar_type) + @spec invalid_units([unit], :date | :time) :: keyword() + def invalid_units(duration_units, calendar_type) - def invalid_keys(duration_units, :date) do + def invalid_units(duration_units, :date) do Enum.filter( [:hour, :minute, :second, :millisecond, :microsecond], &Keyword.has_key?(duration_units, &1) ) end - def invalid_keys(duration_units, :time) do + def invalid_units(duration_units, :time) do Enum.filter( [:year, :month, :week, :day], &Keyword.has_key?(duration_units, &1) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 06fce7b0fed..14803bb251c 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -593,7 +593,7 @@ defmodule Time do end def shift(%Time{} = time, duration_units) do - case Duration.invalid_keys(duration_units, :time) do + case Duration.invalid_units(duration_units, :time) do [] -> shift(time, Duration.new(duration_units)) From 586f4a45af66c15fa80d95f2b2fe8799fc66a9f1 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 12:05:28 +0100 Subject: [PATCH 42/97] consistent calendar microsecond format in duration --- lib/elixir/lib/calendar/duration.ex | 41 +++++++++++-------- lib/elixir/lib/calendar/iso.ex | 36 ++++++++-------- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- lib/elixir/lib/calendar/time.ex | 2 +- .../test/elixir/calendar/duration_test.exs | 38 ++++++++++------- .../elixir/calendar/naive_datetime_test.exs | 16 ++++---- lib/elixir/test/elixir/calendar/time_test.exs | 5 +-- 7 files changed, 79 insertions(+), 61 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 062ada1ac0d..a504b63c9f9 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -10,8 +10,7 @@ defmodule Duration do hour: 0, minute: 0, second: 0, - millisecond: 0, - microsecond: 0 + microsecond: {0, 0} @typedoc "Duration in calendar units" @type t :: %Duration{ @@ -22,8 +21,7 @@ defmodule Duration do hour: integer, minute: integer, second: integer, - millisecond: integer, - microsecond: integer + microsecond: {integer, integer} } @typedoc "Individually valid Duration units" @@ -35,8 +33,7 @@ defmodule Duration do | {:hour, integer} | {:minute, integer} | {:second, integer} - | {:millisecond, integer} - | {:microsecond, integer} + | {:microsecond, {integer, integer}} @doc """ Create `Duration` struct from valid duration units. @@ -51,6 +48,14 @@ defmodule Duration do """ @spec new([unit]) :: t def new(units) do + case Keyword.get(units, :microsecond) do + ms when is_integer(ms) -> + raise "microseconds must be a tuple {ms, precision}" + + _ -> + :noop + end + struct!(Duration, units) end @@ -65,6 +70,9 @@ defmodule Duration do """ @spec add(t, t) :: t def add(%Duration{} = d1, %Duration{} = d2) do + {m1, p1} = d1.microsecond + {m2, p2} = d2.microsecond + %Duration{ year: d1.year + d2.year, month: d1.month + d2.month, @@ -73,8 +81,7 @@ defmodule Duration do hour: d1.hour + d2.hour, minute: d1.minute + d2.minute, second: d1.second + d2.second, - millisecond: d1.millisecond + d2.millisecond, - microsecond: d1.microsecond + d2.microsecond + microsecond: {m1 + m2, max(p1, p2)} } end @@ -89,6 +96,9 @@ defmodule Duration do """ @spec subtract(t, t) :: t def subtract(%Duration{} = d1, %Duration{} = d2) do + {m1, p1} = d1.microsecond + {m2, p2} = d2.microsecond + %Duration{ year: d1.year - d2.year, month: d1.month - d2.month, @@ -97,8 +107,7 @@ defmodule Duration do hour: d1.hour - d2.hour, minute: d1.minute - d2.minute, second: d1.second - d2.second, - millisecond: d1.millisecond - d2.millisecond, - microsecond: d1.microsecond - d2.microsecond + microsecond: {m1 - m2, max(p1, p2)} } end @@ -112,7 +121,7 @@ defmodule Duration do """ @spec multiply(t, integer) :: t - def multiply(%Duration{} = duration, integer) when is_integer(integer) do + def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do %Duration{ year: duration.year * integer, month: duration.month * integer, @@ -121,8 +130,7 @@ defmodule Duration do hour: duration.hour * integer, minute: duration.minute * integer, second: duration.second * integer, - millisecond: duration.millisecond * integer, - microsecond: duration.microsecond * integer + microsecond: {ms * integer, p} } end @@ -136,7 +144,7 @@ defmodule Duration do """ @spec negate(t) :: t - def negate(%Duration{} = duration) do + def negate(%Duration{microsecond: {ms, p}} = duration) do %Duration{ year: -duration.year, month: -duration.month, @@ -145,8 +153,7 @@ defmodule Duration do hour: -duration.hour, minute: -duration.minute, second: -duration.second, - millisecond: -duration.millisecond, - microsecond: -duration.microsecond + microsecond: {-ms, p} } end @@ -156,7 +163,7 @@ defmodule Duration do def invalid_units(duration_units, :date) do Enum.filter( - [:hour, :minute, :second, :millisecond, :microsecond], + [:hour, :minute, :second, :microsecond], &Keyword.has_key?(duration_units, &1) ) end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index ee0f17326a8..0e80db229b6 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1495,7 +1495,7 @@ defmodule Calendar.ISO do {2016, 1, 3, 1, 0, 0, {0, 0}} iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(hour: 30)) {2016, 1, 4, 6, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(microsecond: 100)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(microsecond: {100, 6})) {2016, 1, 3, 0, 0, 0, {100, 6}} """ @spec shift_naive_datetime( @@ -1534,7 +1534,7 @@ defmodule Calendar.ISO do iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(hour: 2)) {15, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(microsecond: 100)) + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(microsecond: {100, 6})) {13, 0, 0, {100, 6}} """ @spec shift_time(hour, minute, second, microsecond, Duration.t()) :: @@ -1580,11 +1580,10 @@ defmodule Calendar.ISO do end defp shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) - when unit in [:second, :millisecond, :microsecond] do - {_, ms_precision} = microsecond + when unit in [:second, :microsecond] or is_integer(unit) do + {value, precision} = shift_time_unit_values(value, microsecond) ppd = System.convert_time_unit(86400, :second, unit) - precision = max(time_unit_to_precision(unit), ms_precision) {year, month, day, hour, minute, second, {ms_value, _}} = naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) @@ -1594,23 +1593,32 @@ defmodule Calendar.ISO do {year, month, day, hour, minute, second, {ms_value, precision}} end - defp shift_time_unit( - {hour, minute, second, {_, precision} = microsecond}, - value, - unit - ) - when unit in [:second, :millisecond, :microsecond] do + defp shift_time_unit({hour, minute, second, microsecond}, value, unit) + when unit in [:second, :microsecond] or is_integer(unit) do + {value, precision} = shift_time_unit_values(value, microsecond) + time = {0, time_to_day_fraction(hour, minute, second, microsecond)} amount_to_add = System.convert_time_unit(value, unit, :microsecond) total = iso_days_to_unit(time, :microsecond) + amount_to_add parts = Integer.mod(total, @parts_per_day) - precision = max(time_unit_to_precision(unit), precision) {hour, minute, second, {microsecond, _}} = time_from_day_fraction({parts, @parts_per_day}) {hour, minute, second, {microsecond, precision}} end + defp shift_time_unit_values({0, _}, {_, original_precision}) do + {0, original_precision} + end + + defp shift_time_unit_values({ms_value, ms_precision}, {_, _}) do + {ms_value, ms_precision} + end + + defp shift_time_unit_values(value, {_, original_precision}) do + {value, original_precision} + end + defp get_shift_options(:date, %Duration{year: year, month: month, week: week, day: day}) do [ month: year * 12 + month, @@ -1626,13 +1634,11 @@ defmodule Calendar.ISO do hour: hour, minute: minute, second: second, - millisecond: millisecond, microsecond: microsecond }) do [ month: year * 12 + month, second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second, - millisecond: millisecond, microsecond: microsecond ] end @@ -1641,12 +1647,10 @@ defmodule Calendar.ISO do hour: hour, minute: minute, second: second, - millisecond: millisecond, microsecond: microsecond }) do [ second: hour * 3600 + minute * 60 + second, - millisecond: millisecond, microsecond: microsecond ] end diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 6a6eb59844e..86ea4b55d56 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -584,7 +584,7 @@ defmodule NaiveDateTime do {:ok, ~N[2020-02-01 00:00:00]} iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45) {:ok, ~N[2016-01-31 00:00:45]} - iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: 100) + iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6}) {:ok, ~N[2016-01-31 00:00:00.000100]} """ diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 14803bb251c..7c87d1d08cd 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -571,7 +571,7 @@ defmodule Time do {:ok, ~T[07:30:00]} iex> Time.shift(~T[01:15:00], second: 125) {:ok, ~T[01:17:05]} - iex> Time.shift(~T[01:00:15], microsecond: 100) + iex> Time.shift(~T[01:00:15], microsecond: {100, 6}) {:ok, ~T[01:00:15.000100]} """ diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index e18352e3ed5..69611ca78a0 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -21,7 +21,7 @@ defmodule DurationTest do hour: 5, minute: 6, second: 7, - microsecond: 8 + microsecond: {8, 6} } d2 = %Duration{ @@ -32,7 +32,7 @@ defmodule DurationTest do hour: 4, minute: 3, second: 2, - microsecond: 1 + microsecond: {1, 6} } assert Duration.add(d1, d2) == %Duration{ @@ -43,7 +43,7 @@ defmodule DurationTest do hour: 9, minute: 9, second: 9, - microsecond: 9 + microsecond: {9, 6} } assert Duration.add(d1, d2) == Duration.add(d2, d1) @@ -59,8 +59,12 @@ defmodule DurationTest do hour: 0, minute: 0, second: 2, - microsecond: 0 + microsecond: {0, 0} } + + d1 = %Duration{microsecond: {1000, 4}} + d2 = %Duration{microsecond: {5, 6}} + assert Duration.add(d1, d2) == %Duration{microsecond: {1005, 6}} end test "subtract/2" do @@ -72,7 +76,7 @@ defmodule DurationTest do hour: 5, minute: 6, second: 7, - microsecond: 8 + microsecond: {8, 6} } d2 = %Duration{ @@ -83,7 +87,7 @@ defmodule DurationTest do hour: 4, minute: 3, second: 2, - microsecond: 1 + microsecond: {1, 6} } assert Duration.subtract(d1, d2) == %Duration{ @@ -94,7 +98,7 @@ defmodule DurationTest do hour: 1, minute: 3, second: 5, - microsecond: 7 + microsecond: {7, 6} } assert Duration.subtract(d2, d1) == %Duration{ @@ -105,7 +109,7 @@ defmodule DurationTest do hour: -1, minute: -3, second: -5, - microsecond: -7 + microsecond: {-7, 6} } assert Duration.subtract(d1, d2) != Duration.subtract(d2, d1) @@ -121,8 +125,12 @@ defmodule DurationTest do hour: 0, minute: 0, second: -2, - microsecond: 0 + microsecond: {0, 0} } + + d1 = %Duration{microsecond: {1000, 4}} + d2 = %Duration{microsecond: {5, 6}} + assert Duration.subtract(d1, d2) == %Duration{microsecond: {995, 6}} end test "multiply/2" do @@ -134,7 +142,7 @@ defmodule DurationTest do hour: 5, minute: 6, second: 7, - microsecond: 8 + microsecond: {8, 6} } assert Duration.multiply(duration, 3) == %Duration{ @@ -145,7 +153,7 @@ defmodule DurationTest do hour: 15, minute: 18, second: 21, - microsecond: 24 + microsecond: {24, 6} } assert Duration.multiply(%Duration{year: 2, day: 4, minute: 5}, 4) == @@ -157,7 +165,7 @@ defmodule DurationTest do hour: 0, minute: 20, second: 0, - microsecond: 0 + microsecond: {0, 0} } end @@ -170,7 +178,7 @@ defmodule DurationTest do hour: 5, minute: 6, second: 7, - microsecond: 8 + microsecond: {8, 6} } assert Duration.negate(duration) == %Duration{ @@ -181,7 +189,7 @@ defmodule DurationTest do hour: -5, minute: -6, second: -7, - microsecond: -8 + microsecond: {-8, 6} } assert Duration.negate(%Duration{year: 2, day: 4, minute: 5}) == @@ -193,7 +201,7 @@ defmodule DurationTest do hour: 0, minute: -5, second: 0, - microsecond: 0 + microsecond: {0, 0} } end end diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 8d61dd2368c..9e553de54d3 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -410,17 +410,17 @@ defmodule NaiveDateTimeTest do assert NaiveDateTime.shift(naive_datetime, minute: -45) == {:ok, ~N[1999-12-31 23:15:00]} assert NaiveDateTime.shift(naive_datetime, second: -30) == {:ok, ~N[1999-12-31 23:59:30]} - assert NaiveDateTime.shift(naive_datetime, microsecond: -500) == + assert NaiveDateTime.shift(naive_datetime, microsecond: {-500, 6}) == {:ok, ~N[1999-12-31 23:59:59.999500]} - assert NaiveDateTime.shift(naive_datetime, microsecond: 500) == + assert NaiveDateTime.shift(naive_datetime, microsecond: {500, 6}) == {:ok, ~N[2000-01-01 00:00:00.000500]} - assert NaiveDateTime.shift(naive_datetime, millisecond: 500) == - {:ok, ~N[2000-01-01 00:00:00.500]} + assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 6}) == + {:ok, ~N[2000-01-01 00:00:00.000100]} - assert NaiveDateTime.shift(naive_datetime, millisecond: 500, microsecond: 100) == - {:ok, ~N[2000-01-01 00:00:00.500100]} + assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 4}) == + {:ok, ~N[2000-01-01 00:00:00.0001]} assert NaiveDateTime.shift(naive_datetime, year: 1, month: 2) == {:ok, ~N[2001-03-01 00:00:00]} @@ -436,7 +436,7 @@ defmodule NaiveDateTimeTest do hour: 5, minute: 6, second: 7, - microsecond: 8 + microsecond: {8, 6} ) == {:ok, ~N[2001-03-26 05:06:07.000008]} assert NaiveDateTime.shift(naive_datetime, @@ -447,7 +447,7 @@ defmodule NaiveDateTimeTest do hour: -5, minute: -6, second: -7, - microsecond: -8 + microsecond: {-8, 6} ) == {:ok, ~N[1998-10-06 18:53:52.999992]} assert_raise KeyError, ~s/key :months not found/, fn -> diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 125e7a8b3c8..a6aa4dcf45e 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -109,9 +109,8 @@ defmodule TimeTest do assert Time.shift(time, hour: 25) == {:ok, ~T[01:00:00.0]} assert Time.shift(time, minute: 25) == {:ok, ~T[00:25:00.0]} assert Time.shift(time, second: 50) == {:ok, ~T[00:00:50.0]} - assert Time.shift(time, millisecond: 150) == {:ok, ~T[00:00:00.150]} - assert Time.shift(time, microsecond: 150) == {:ok, ~T[00:00:00.000150]} - assert Time.shift(time, millisecond: 150, microsecond: 100) == {:ok, ~T[00:00:00.150100]} + assert Time.shift(time, microsecond: {150, 6}) == {:ok, ~T[00:00:00.000150]} + assert Time.shift(time, microsecond: {1000, 4}) == {:ok, ~T[00:00:00.0010]} assert Time.shift(time, hour: 2, minute: 65, second: 5) == {:ok, ~T[03:05:05.0]} assert_raise ArgumentError, ~s/cannot shift time by date units: [:day]/, fn -> From efbcc38c7d64168163cc773ef01c7feed59964e5 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 12:17:20 +0100 Subject: [PATCH 43/97] cleanup --- lib/elixir/lib/calendar/duration.ex | 9 ++++++--- lib/elixir/lib/calendar/iso.ex | 12 ++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index a504b63c9f9..6d24e4ad95c 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -49,11 +49,14 @@ defmodule Duration do @spec new([unit]) :: t def new(units) do case Keyword.get(units, :microsecond) do - ms when is_integer(ms) -> - raise "microseconds must be a tuple {ms, precision}" + nil -> + :noop - _ -> + ms when is_tuple(ms) -> :noop + + _ -> + raise "microseconds must be a tuple {ms, precision}" end struct!(Duration, units) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 0e80db229b6..04554fb3d20 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1475,14 +1475,14 @@ defmodule Calendar.ISO do shift_options = get_shift_options(:date, duration) Enum.reduce(shift_options, {year, month, day}, fn - {_, 0}, naive_datetime -> - naive_datetime + {_, 0}, date -> + date - {:month, value}, naive_datetime -> - shift_months(naive_datetime, value) + {:month, value}, date -> + shift_months(date, value) - {:day, value}, naive_datetime -> - shift_days(naive_datetime, value) + {:day, value}, date -> + shift_days(date, value) end) end From c8e75a5eaf4986973c929749cbcf5ea6ee727b27 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 12:42:03 +0100 Subject: [PATCH 44/97] validate date and time fields Calendar.ISO --- lib/elixir/lib/calendar/date.ex | 8 +------- lib/elixir/lib/calendar/duration.ex | 28 +++++++++++++++------------- lib/elixir/lib/calendar/iso.ex | 10 ++++++++++ lib/elixir/lib/calendar/time.ex | 8 +------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 80e9a224523..d60caae2ed3 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -783,13 +783,7 @@ defmodule Date do end def shift(%Date{} = date, duration_units) do - case Duration.invalid_units(duration_units, :date) do - [] -> - shift(date, Duration.new(duration_units)) - - invalid_units -> - raise ArgumentError, "cannot shift date by time units: #{inspect(invalid_units)}" - end + shift(date, Duration.new(duration_units)) end @doc false diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 6d24e4ad95c..f273ee31eb9 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -161,20 +161,22 @@ defmodule Duration do end @doc false - @spec invalid_units([unit], :date | :time) :: keyword() - def invalid_units(duration_units, calendar_type) - - def invalid_units(duration_units, :date) do - Enum.filter( - [:hour, :minute, :second, :microsecond], - &Keyword.has_key?(duration_units, &1) - ) + @spec invalid_fields_for(t, :date | :time) :: keyword + def invalid_fields_for(duration, calendar_type) + + def invalid_fields_for(duration, :date) do + Enum.filter([:hour, :minute, :second, :microsecond], &is_set?(duration, &1)) + end + + def invalid_fields_for(duration, :time) do + Enum.filter([:year, :month, :week, :day], &is_set?(duration, &1)) + end + + defp is_set?(duration, :microsecond) do + Map.get(duration, :microsecond) != {0, 0} end - def invalid_units(duration_units, :time) do - Enum.filter( - [:year, :month, :week, :day], - &Keyword.has_key?(duration_units, &1) - ) + defp is_set?(duration, field) do + Map.get(duration, field) != 0 end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 04554fb3d20..946fc2de6a7 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1472,6 +1472,11 @@ defmodule Calendar.ISO do @spec shift_date(year, month, day, Duration.t()) :: {year, month, day} @impl true def shift_date(year, month, day, duration) do + case Duration.invalid_fields_for(duration, :date) do + [] -> :noop + units -> raise ArgumentError, "cannot shift date by time units: #{inspect(units)}" + end + shift_options = get_shift_options(:date, duration) Enum.reduce(shift_options, {year, month, day}, fn @@ -1541,6 +1546,11 @@ defmodule Calendar.ISO do {hour, minute, second, microsecond} @impl true def shift_time(hour, minute, second, microsecond, duration) do + case Duration.invalid_fields_for(duration, :time) do + [] -> :noop + units -> raise ArgumentError, "cannot shift time by date units: #{inspect(units)}" + end + shift_options = get_shift_options(:time, duration) Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 7c87d1d08cd..1df3f252c9d 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -593,13 +593,7 @@ defmodule Time do end def shift(%Time{} = time, duration_units) do - case Duration.invalid_units(duration_units, :time) do - [] -> - shift(time, Duration.new(duration_units)) - - invalid_units -> - raise ArgumentError, "cannot shift time by date units: #{inspect(invalid_units)}" - end + shift(time, Duration.new(duration_units)) end @doc """ From 210e2b9bd2c63080c6738a36061b39f44d0901f6 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 13:58:59 +0100 Subject: [PATCH 45/97] cleanup --- lib/elixir/lib/calendar/datetime.ex | 6 +----- lib/elixir/lib/calendar/duration.ex | 20 -------------------- lib/elixir/lib/calendar/iso.ex | 29 +++++++++++++++++++++-------- 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index a7d1dd26710..d39e802c737 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1699,11 +1699,7 @@ defmodule DateTime do | :utc_only_time_zone_database} def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) - def shift( - %DateTime{calendar: calendar} = datetime, - %Duration{} = duration, - time_zone_database - ) do + def shift(%DateTime{calendar: calendar} = datetime, %Duration{} = duration, time_zone_database) do %{ year: year, month: month, diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index f273ee31eb9..96c97e2507e 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -159,24 +159,4 @@ defmodule Duration do microsecond: {-ms, p} } end - - @doc false - @spec invalid_fields_for(t, :date | :time) :: keyword - def invalid_fields_for(duration, calendar_type) - - def invalid_fields_for(duration, :date) do - Enum.filter([:hour, :minute, :second, :microsecond], &is_set?(duration, &1)) - end - - def invalid_fields_for(duration, :time) do - Enum.filter([:year, :month, :week, :day], &is_set?(duration, &1)) - end - - defp is_set?(duration, :microsecond) do - Map.get(duration, :microsecond) != {0, 0} - end - - defp is_set?(duration, field) do - Map.get(duration, field) != 0 - end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 946fc2de6a7..f5acab09eec 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1472,10 +1472,7 @@ defmodule Calendar.ISO do @spec shift_date(year, month, day, Duration.t()) :: {year, month, day} @impl true def shift_date(year, month, day, duration) do - case Duration.invalid_fields_for(duration, :date) do - [] -> :noop - units -> raise ArgumentError, "cannot shift date by time units: #{inspect(units)}" - end + validate_shift_fields!(duration, :date) shift_options = get_shift_options(:date, duration) @@ -1546,10 +1543,7 @@ defmodule Calendar.ISO do {hour, minute, second, microsecond} @impl true def shift_time(hour, minute, second, microsecond, duration) do - case Duration.invalid_fields_for(duration, :time) do - [] -> :noop - units -> raise ArgumentError, "cannot shift time by date units: #{inspect(units)}" - end + validate_shift_fields!(duration, :time) shift_options = get_shift_options(:time, duration) @@ -1665,6 +1659,25 @@ defmodule Calendar.ISO do ] end + defp validate_shift_fields!(duration, :date) do + field_set? = fn + :microsecond, %{microsecond: {0, 0}} -> false + field, duration -> Map.get(duration, field) != 0 + end + + case Enum.filter([:hour, :minute, :second, :microsecond], &field_set?.(&1, duration)) do + [] -> :noop + units -> raise ArgumentError, "cannot shift date by time units: #{inspect(units)}" + end + end + + defp validate_shift_fields!(duration, :time) do + case Enum.filter([:year, :month, :week, :day], &(Map.get(duration, &1) != 0)) do + [] -> :noop + units -> raise ArgumentError, "cannot shift time by date units: #{inspect(units)}" + end + end + ## Helpers @doc false From 28e5e03e0dbbb77e7798f20f4c600b8bf5feb697 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 14:03:34 +0100 Subject: [PATCH 46/97] cleanup --- lib/elixir/lib/calendar/iso.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index f5acab09eec..446681e2658 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1584,7 +1584,7 @@ defmodule Calendar.ISO do end defp shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) - when unit in [:second, :microsecond] or is_integer(unit) do + when unit in [:second, :microsecond] do {value, precision} = shift_time_unit_values(value, microsecond) ppd = System.convert_time_unit(86400, :second, unit) @@ -1598,7 +1598,7 @@ defmodule Calendar.ISO do end defp shift_time_unit({hour, minute, second, microsecond}, value, unit) - when unit in [:second, :microsecond] or is_integer(unit) do + when unit in [:second, :microsecond] do {value, precision} = shift_time_unit_values(value, microsecond) time = {0, time_to_day_fraction(hour, minute, second, microsecond)} From 3121fa43e8baae7e2638d04f4a4f9cef8ee9b065 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 14:20:55 +0100 Subject: [PATCH 47/97] from_naive/4 in DateTime.shift/3 --- lib/elixir/lib/calendar/datetime.ex | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index d39e802c737..9d077207efd 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1723,9 +1723,17 @@ defmodule DateTime do duration ) - new( - Date.new!(year, month, day), - Time.new!(hour, minute, second, microsecond), + from_naive( + %NaiveDateTime{ + calendar: calendar, + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + }, time_zone, time_zone_database ) From f4be40caabc0748aba11e30405b719e6669695c0 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 14:30:12 +0100 Subject: [PATCH 48/97] cleanup shift options validation --- lib/elixir/lib/calendar/iso.ex | 52 +++++++++---------- lib/elixir/test/elixir/calendar/date_test.exs | 2 +- lib/elixir/test/elixir/calendar/time_test.exs | 2 +- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 446681e2658..951fd4a8bfe 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1472,9 +1472,7 @@ defmodule Calendar.ISO do @spec shift_date(year, month, day, Duration.t()) :: {year, month, day} @impl true def shift_date(year, month, day, duration) do - validate_shift_fields!(duration, :date) - - shift_options = get_shift_options(:date, duration) + shift_options = shift_date_options(duration) Enum.reduce(shift_options, {year, month, day}, fn {_, 0}, date -> @@ -1512,7 +1510,7 @@ defmodule Calendar.ISO do ) :: {year, month, day, hour, minute, second, microsecond} @impl true def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do - shift_options = get_shift_options(:naive_datetime, duration) + shift_options = shift_datetime_options(duration) Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn {_, 0}, naive_datetime -> @@ -1543,9 +1541,7 @@ defmodule Calendar.ISO do {hour, minute, second, microsecond} @impl true def shift_time(hour, minute, second, microsecond, duration) do - validate_shift_fields!(duration, :time) - - shift_options = get_shift_options(:time, duration) + shift_options = shift_time_options(duration) Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn {_, 0}, time -> @@ -1623,14 +1619,27 @@ defmodule Calendar.ISO do {value, original_precision} end - defp get_shift_options(:date, %Duration{year: year, month: month, week: week, day: day}) do + defp shift_date_options(%Duration{ + year: year, + month: month, + week: week, + day: day, + hour: 0, + minute: 0, + second: 0, + microsecond: {0, 0} + }) do [ month: year * 12 + month, day: week * 7 + day ] end - defp get_shift_options(:naive_datetime, %Duration{ + defp shift_date_options(_duration) do + raise ArgumentError, "cannot shift date by time units" + end + + defp shift_datetime_options(%Duration{ year: year, month: month, week: week, @@ -1647,7 +1656,11 @@ defmodule Calendar.ISO do ] end - defp get_shift_options(:time, %Duration{ + defp shift_time_options(%Duration{ + year: 0, + month: 0, + week: 0, + day: 0, hour: hour, minute: minute, second: second, @@ -1659,23 +1672,8 @@ defmodule Calendar.ISO do ] end - defp validate_shift_fields!(duration, :date) do - field_set? = fn - :microsecond, %{microsecond: {0, 0}} -> false - field, duration -> Map.get(duration, field) != 0 - end - - case Enum.filter([:hour, :minute, :second, :microsecond], &field_set?.(&1, duration)) do - [] -> :noop - units -> raise ArgumentError, "cannot shift date by time units: #{inspect(units)}" - end - end - - defp validate_shift_fields!(duration, :time) do - case Enum.filter([:year, :month, :week, :day], &(Map.get(duration, &1) != 0)) do - [] -> :noop - units -> raise ArgumentError, "cannot shift time by date units: #{inspect(units)}" - end + defp shift_time_options(_duration) do + raise ArgumentError, "cannot shift time by date units" end ## Helpers diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index cd7b804b7ae..043b189f2e4 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -194,7 +194,7 @@ defmodule DateTest do assert Date.shift(~D[2000-01-01], month: 12) == {:ok, ~D[2001-01-01]} assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} - assert_raise ArgumentError, ~s/cannot shift date by time units: [:second]/, fn -> + assert_raise ArgumentError, ~s/cannot shift date by time units/, fn -> Date.shift(~D[2012-02-29], second: 86400) end diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index a6aa4dcf45e..2cf4ae4c4b1 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -113,7 +113,7 @@ defmodule TimeTest do assert Time.shift(time, microsecond: {1000, 4}) == {:ok, ~T[00:00:00.0010]} assert Time.shift(time, hour: 2, minute: 65, second: 5) == {:ok, ~T[03:05:05.0]} - assert_raise ArgumentError, ~s/cannot shift time by date units: [:day]/, fn -> + assert_raise ArgumentError, ~s/cannot shift time by date units/, fn -> Time.shift(time, day: 1) end From 2fd6528e8847281e837a4eef2c867c4a6e6dd6cf Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 15:18:30 +0100 Subject: [PATCH 49/97] since doc annotations --- lib/elixir/lib/calendar.ex | 3 +++ lib/elixir/lib/calendar/date.ex | 1 + lib/elixir/lib/calendar/datetime.ex | 1 + lib/elixir/lib/calendar/naive_datetime.ex | 6 ++---- lib/elixir/lib/calendar/time.ex | 1 + 5 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index 3a98b997505..09062f156bb 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -341,11 +341,13 @@ defmodule Calendar do @doc """ Shifts date by given duration according to its calendar. """ + @doc since: "1.17.0" @callback shift_date(year, month, day, Duration.t()) :: {year, month, day} @doc """ Shifts naive datetime by given duration according to its calendar. """ + @doc since: "1.17.0" @callback shift_naive_datetime( year, month, @@ -360,6 +362,7 @@ defmodule Calendar do @doc """ Shifts time by given duration according to its calendar. """ + @doc since: "1.17.0" @callback shift_time(hour, minute, second, microsecond, Duration.t()) :: {hour, minute, second, microsecond} diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index d60caae2ed3..549559d2dd6 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -775,6 +775,7 @@ defmodule Date do {:ok, ~D[2020-02-01]} """ + @doc since: "1.7.0" @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: {:ok, t} def shift(%Date{calendar: calendar} = date, %Duration{} = duration) do %{year: year, month: month, day: day} = date diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 9d077207efd..be94cc10ef5 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1685,6 +1685,7 @@ defmodule DateTime do {:ok, ~U[2016-03-03 00:00:00Z]} """ + @doc since: "1.7.0" @spec shift( Calendar.datetime(), Duration.t() | [Duration.unit()], diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 86ea4b55d56..195c806c151 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -588,10 +588,8 @@ defmodule NaiveDateTime do {:ok, ~N[2016-01-31 00:00:00.000100]} """ - @spec shift( - Calendar.naive_datetime(), - Duration.t() | [Duration.unit()] - ) :: {:ok, t} + @doc since: "1.7.0" + @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: {:ok, t} def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Duration{} = duration) do %{ year: year, diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 1df3f252c9d..bb516017540 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -575,6 +575,7 @@ defmodule Time do {:ok, ~T[01:00:15.000100]} """ + @doc since: "1.7.0" @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: {:ok, t} def shift(%Time{calendar: calendar} = time, %Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time From af047e657e2060e6f1ef2a608a2ccc06cd6e7755 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 15:42:49 +0100 Subject: [PATCH 50/97] improve docs --- lib/elixir/lib/calendar/date.ex | 15 +++++++++++---- lib/elixir/lib/calendar/datetime.ex | 23 ++++++++++++++++++----- lib/elixir/lib/calendar/duration.ex | 23 +++++++++++++++-------- lib/elixir/lib/calendar/iso.ex | 8 +++----- lib/elixir/lib/calendar/naive_datetime.ex | 6 ++++-- lib/elixir/lib/calendar/time.ex | 8 ++++++-- 6 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 549559d2dd6..bc71f52a4cb 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -690,6 +690,9 @@ defmodule Date do The days are counted as Gregorian days. The date is returned in the same calendar as it was given in. + To move a date by a complex duration supporting various units including years, + months and weeks, days, you can use `Date.shift/2`. + ## Examples iex> Date.add(~D[2000-01-03], -2) @@ -761,18 +764,22 @@ defmodule Date do end @doc """ - Shifts a date by given duration according to its calendar. + Shifts given `date` by `duration` according to its calendar. + + Available units are: `:year, :month, :week, :day`. - Check `Calendar.ISO.shift_date/4` for more information. + Raises ArgumentError when called with time units. ## Examples iex> Date.shift(~D[2016-01-03], month: 2) {:ok, ~D[2016-03-03]} - iex> Date.shift(~D[2016-02-29], month: 1) - {:ok, ~D[2016-03-29]} + iex> Date.shift(~D[2016-01-30], month: 1) + {:ok, ~D[2016-02-29]} iex> Date.shift(~D[2016-01-31], year: 4, day: 1) {:ok, ~D[2020-02-01]} + iex> Date.shift(~D[2016-01-03], Duration.new(month: 2)) + {:ok, ~D[2016-03-03]} """ @doc since: "1.7.0" diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index be94cc10ef5..d4fa966c757 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1610,6 +1610,8 @@ defmodule DateTime do iex> result.microsecond {21000, 3} + To move a datetime by a complex duration supporting various units including years, + months, weeks as well as time units, you can use `DateTime.shift/2`. """ @doc since: "1.8.0" @spec add( @@ -1675,15 +1677,26 @@ defmodule DateTime do end @doc """ - Shifts a datetime by given duration according to its calendar. + Shifts given `datetime` by `duration` according to its calendar. - Can return an ambiguous or gap datetime tuple. + Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - ## Examples + First the datetime is converted to a naive datetime. After the shift was applied + it is converted back to a datetime using its original time zone and time zone database, + potentially resulting in an ambiguous or gap DateTime result tuple. + + Check `from_naive/3` for more information on ambiguous datetimes. - iex> DateTime.shift(~U[2016-01-03 00:00:00Z], month: 2) - {:ok, ~U[2016-03-03 00:00:00Z]} + ## Examples + iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2) + {:ok, ~U[2016-03-01 00:00:00Z]} + iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4) + {:ok, ~U[2017-01-29 00:00:00Z]} + iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 25) + {:ok, ~U[2016-01-01 00:25:00Z]} + iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4}) + {:ok, ~U[2016-01-01 00:05:00.0005Z]} """ @doc since: "1.7.0" @spec shift( diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 96c97e2507e..eccbf8d751a 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -1,6 +1,10 @@ defmodule Duration do @moduledoc """ - The Duration type. + The Duration type implements the concept of duration applicable to all calendar types. + + A `Duration` has time scale units represented as integers with + the exception of microseconds, which are represented as a tuple `{microsecond, precision}`, + to be compatible with other calendar types implementing time, such as `Time`, `DateTime` and `NaiveDateTime`. """ defstruct year: 0, @@ -12,7 +16,6 @@ defmodule Duration do second: 0, microsecond: {0, 0} - @typedoc "Duration in calendar units" @type t :: %Duration{ year: integer, month: integer, @@ -24,7 +27,6 @@ defmodule Duration do microsecond: {integer, integer} } - @typedoc "Individually valid Duration units" @type unit :: {:year, integer} | {:month, integer} @@ -36,7 +38,7 @@ defmodule Duration do | {:microsecond, {integer, integer}} @doc """ - Create `Duration` struct from valid duration units. + Creates a new `Duration` struct from given `units`. Raises a KeyError when called with invalid units. @@ -63,7 +65,9 @@ defmodule Duration do end @doc """ - Adds two durations. + Adds units of given durations `d1` and `d2`. + + Respects the the highest microsecond precision of the two. ## Examples @@ -89,7 +93,9 @@ defmodule Duration do end @doc """ - Subtracts two durations. + Subtracts units of given durations `d1` and `d2`. + + Respects the the highest microsecond precision of the two. ## Examples @@ -115,7 +121,7 @@ defmodule Duration do end @doc """ - Multiplies all duration units by given integer. + Multiplies `duration` units by given `integer`. ## Examples @@ -138,7 +144,8 @@ defmodule Duration do end @doc """ - Negates all duration units. + Negates `duration` units. + ## Examples diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 951fd4a8bfe..4af3c3f06d5 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1456,7 +1456,7 @@ defmodule Calendar.ISO do end @doc """ - Shift Date by Duration according to its calendar. + Shifts Date by Duration according to its calendar. ## Examples @@ -1487,7 +1487,7 @@ defmodule Calendar.ISO do end @doc """ - Shift NaiveDateTime by Duration according to its calendar. + Shifts NaiveDateTime by Duration according to its calendar. ## Examples @@ -1526,9 +1526,7 @@ defmodule Calendar.ISO do end @doc """ - Shifts time by Duration units according to its calendar. - - Available units are: `:hour, :minute, :second, :microsecond`. + Shifts Time by Duration units according to its calendar. ## Examples diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 195c806c151..1da06ddf80d 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -448,6 +448,8 @@ defmodule NaiveDateTime do iex> NaiveDateTime.add(dt, 21, :second) ~N[2000-02-29 23:00:28] + To move a native datetime by a complex duration supporting various units including years, + months, weeks as well as time units, you can use `NaiveDateTime.shift/2`. """ @doc since: "1.4.0" @spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t @@ -572,9 +574,9 @@ defmodule NaiveDateTime do end @doc """ - Shifts a naive datetime by given duration according to its calendar. + Shifts given `naive_datetime` by `duration` according to its calendar. - Check `Calendar.ISO.shift_naive_datetime/8` for more information. + Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. ## Examples diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index bb516017540..8c034a1d73d 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -500,6 +500,8 @@ defmodule Time do iex> result.microsecond {21000, 3} + To move a time by a complex duration supporting all time units and precisions, + you can use `Time.shift/2`. """ @doc since: "1.6.0" @spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t @@ -559,9 +561,9 @@ defmodule Time do end @doc """ - Shifts a Time by given Duration according to its calendar. + Shifts given `time` by `duration` according to its calendar. - Check `Calendar.ISO.shift_time/4` for more information. + Available duration units are: `:hour, :minute, :second, :microsecond`. ## Examples @@ -573,6 +575,8 @@ defmodule Time do {:ok, ~T[01:17:05]} iex> Time.shift(~T[01:00:15], microsecond: {100, 6}) {:ok, ~T[01:00:15.000100]} + iex> Time.shift(~T[01:15:00], Duration.new(second: 65)) + {:ok, ~T[01:16:05]} """ @doc since: "1.7.0" From bcacd15f2ecc0873bc1cff36feea7ba1a617134c Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 19:29:24 +0100 Subject: [PATCH 51/97] dont pattern match on input struct type --- lib/elixir/lib/calendar/date.ex | 4 ++-- lib/elixir/lib/calendar/datetime.ex | 4 ++-- lib/elixir/lib/calendar/naive_datetime.ex | 4 ++-- lib/elixir/lib/calendar/time.ex | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index bc71f52a4cb..667989dd5a7 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -784,13 +784,13 @@ defmodule Date do """ @doc since: "1.7.0" @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: {:ok, t} - def shift(%Date{calendar: calendar} = date, %Duration{} = duration) do + def shift(%{calendar: calendar} = date, %Duration{} = duration) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} end - def shift(%Date{} = date, duration_units) do + def shift(date, duration_units) do shift(date, Duration.new(duration_units)) end diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index d4fa966c757..35343dd0d2f 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1713,7 +1713,7 @@ defmodule DateTime do | :utc_only_time_zone_database} def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) - def shift(%DateTime{calendar: calendar} = datetime, %Duration{} = duration, time_zone_database) do + def shift(%{calendar: calendar} = datetime, %Duration{} = duration, time_zone_database) do %{ year: year, month: month, @@ -1753,7 +1753,7 @@ defmodule DateTime do ) end - def shift(%DateTime{} = datetime, duration_units, time_zone_database) do + def shift(datetime, duration_units, time_zone_database) do shift(datetime, Duration.new(duration_units), time_zone_database) end diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 1da06ddf80d..a9c0d08f1b9 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -592,7 +592,7 @@ defmodule NaiveDateTime do """ @doc since: "1.7.0" @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: {:ok, t} - def shift(%NaiveDateTime{calendar: calendar} = naive_datetime, %Duration{} = duration) do + def shift(%{calendar: calendar} = naive_datetime, %Duration{} = duration) do %{ year: year, month: month, @@ -628,7 +628,7 @@ defmodule NaiveDateTime do }} end - def shift(%NaiveDateTime{} = naive_datetime, duration_units) do + def shift(naive_datetime, duration_units) do shift(naive_datetime, Duration.new(duration_units)) end diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 8c034a1d73d..083e55aa9a0 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -581,7 +581,7 @@ defmodule Time do """ @doc since: "1.7.0" @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: {:ok, t} - def shift(%Time{calendar: calendar} = time, %Duration{} = duration) do + def shift(%{calendar: calendar} = time, %Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time {hour, minute, second, microsecond} = @@ -597,7 +597,7 @@ defmodule Time do }} end - def shift(%Time{} = time, duration_units) do + def shift(time, duration_units) do shift(time, Duration.new(duration_units)) end From 1d24815d9c2eda0c2dfc222c8dcc4f27b48a92cb Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 21:30:58 +0100 Subject: [PATCH 52/97] more docs --- lib/elixir/lib/calendar/date.ex | 8 +++++++- lib/elixir/lib/calendar/datetime.ex | 7 +++++++ lib/elixir/lib/calendar/naive_datetime.ex | 7 +++++++ lib/elixir/lib/calendar/time.ex | 4 ++++ 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 667989dd5a7..2784f4306ba 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -768,7 +768,13 @@ defmodule Date do Available units are: `:year, :month, :week, :day`. - Raises ArgumentError when called with time units. + Durations are collapsed before they are applied: + - when shifting by 1 year and 2 months the date is actually shifted by 14 months + - when shifting by 2 weeks and 3 days the date is shifted by 17 days + + Durations are applied in order of the size of the unit: `month > day`. + + Raises ArgumentError when called with time scale units. ## Examples diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 35343dd0d2f..4203496a43f 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1681,6 +1681,13 @@ defmodule DateTime do Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. + Durations are collapsed before they are applied: + - when shifting by 1 year and 2 months the date is actually shifted by 14 months + - when shifting by 2 weeks and 3 days the date is shifted by 17 days + - all smaller units are collapsed into seconds and microseconds + + Durations are applied in order of the size of the unit: `month > day > second`. + First the datetime is converted to a naive datetime. After the shift was applied it is converted back to a datetime using its original time zone and time zone database, potentially resulting in an ambiguous or gap DateTime result tuple. diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index a9c0d08f1b9..de9837d8300 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -578,6 +578,13 @@ defmodule NaiveDateTime do Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. + Durations are collapsed before they are applied: + - when shifting by 1 year and 2 months the date is actually shifted by 14 months + - when shifting by 2 weeks and 3 days the date is shifted by 17 days + - all smaller units are collapsed into seconds and microseconds + + Durations are applied in order of the size of the unit: `month > day > second`. + ## Examples iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 083e55aa9a0..7c0d6e1ee64 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -565,6 +565,10 @@ defmodule Time do Available duration units are: `:hour, :minute, :second, :microsecond`. + All duration units are collapsed to seconds and microseconds before they are applied. + + Raises ArgumentError when called with date scale units. + ## Examples iex> Time.shift(~T[01:00:15], hour: 12) From 28b3b688a40fe90246df08756923034ca224ca1e Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 21:51:04 +0100 Subject: [PATCH 53/97] add convenience wrapper shift!/2 to all calendar types --- lib/elixir/lib/calendar/date.ex | 23 +++++++++++++++ lib/elixir/lib/calendar/datetime.ex | 36 +++++++++++++++++++++++ lib/elixir/lib/calendar/naive_datetime.ex | 23 +++++++++++++++ lib/elixir/lib/calendar/time.ex | 23 +++++++++++++++ 4 files changed, 105 insertions(+) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 2784f4306ba..7cee0e0331f 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -800,6 +800,29 @@ defmodule Date do shift(date, Duration.new(duration_units)) end + @doc """ + Shifts given `date` by `duration` according to its calendar. + + Same as shift/2 but raises RuntimeError. + + ## Examples + + iex> Date.shift!(~D[2016-01-03], month: 2) + ~D[2016-03-03] + + """ + @doc since: "1.7.0" + @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: t + def shift!(date, duration_units) do + case shift(date, duration_units) do + {:ok, date} -> + date + + reason -> + raise RuntimeError, "cannot shift date, reason: #{inspect(reason)}" + end + 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}} diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 4203496a43f..7fbe30c996e 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1764,6 +1764,42 @@ defmodule DateTime do shift(datetime, Duration.new(duration_units), time_zone_database) end + @doc """ + Shifts given `datetime` by `duration` according to its calendar. + + Same as shift/2 but raises ArgumentError. + + ## Examples + + iex> DateTime.shift!(~U[2016-01-01 00:00:00Z], month: 2) + ~U[2016-03-01 00:00:00Z] + + """ + @doc since: "1.7.0" + @spec shift!(Calendar.datetime(), Duration.t() | [Duration.unit()]) :: t + def shift!(%{time_zone: time_zone} = datetime, duration_units) do + case shift(datetime, duration_units) do + {:ok, datetime} -> + datetime + + {:ambiguous, dt1, dt2} -> + raise ArgumentError, + "cannot shift datetime #{inspect(datetime)} by #{inspect(duration_units)} because the result " <> + "is ambiguous in time zone #{time_zone} as there is an overlap " <> + "between #{inspect(dt1)} and #{inspect(dt2)}" + + {:gap, dt1, dt2} -> + raise ArgumentError, + "cannot shift datetime #{inspect(datetime)} by #{inspect(duration_units)} because the result " <> + "does not exist in time zone #{time_zone} as there is a gap " <> + "between #{inspect(dt1)} and #{inspect(dt2)}" + + {:error, reason} -> + raise ArgumentError, + "cannot shift datetime #{inspect(datetime)} by #{inspect(duration_units)}, reason: #{inspect(reason)}" + end + end + @doc """ Returns the given datetime with the microsecond field truncated to the given precision (`:microsecond`, `:millisecond` or `:second`). diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index de9837d8300..328ac75474a 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -639,6 +639,29 @@ defmodule NaiveDateTime do shift(naive_datetime, Duration.new(duration_units)) end + @doc """ + Shifts given `naive_datetime` by `duration` according to its calendar. + + Same as shift/2 but raises RuntimeError. + + ## Examples + + iex> NaiveDateTime.shift!(~N[2016-01-31 00:00:00], month: 1) + ~N[2016-02-29 00:00:00] + + """ + @doc since: "1.7.0" + @spec shift!(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: t + def shift!(naive_datetime, duration_units) do + case shift(naive_datetime, duration_units) do + {:ok, naive_datetime} -> + naive_datetime + + reason -> + raise RuntimeError, "cannot shift naive_datetime, reason: #{inspect(reason)}" + end + end + @doc """ Returns the given naive datetime with the microsecond field truncated to the given precision (`:microsecond`, `:millisecond` or `:second`). diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 7c0d6e1ee64..38fad7e529d 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -605,6 +605,29 @@ defmodule Time do shift(time, Duration.new(duration_units)) end + @doc """ + Shifts given `time` by `duration` according to its calendar. + + Same as shift/2 but raises RuntimeError. + + ## Examples + + iex> Time.shift!(~T[01:00:15], hour: 12) + ~T[13:00:15] + + """ + @doc since: "1.7.0" + @spec shift!(Calendar.time(), Duration.t() | [Duration.unit()]) :: t + def shift!(time, duration_units) do + case shift(time, duration_units) do + {:ok, time} -> + time + + reason -> + raise RuntimeError, "cannot shift time, reason: #{inspect(reason)}" + end + end + @doc """ Compares two time structs. From 7027e7f99bde539298af7201480d1c85ba954031 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 22:16:42 +0100 Subject: [PATCH 54/97] docs --- lib/elixir/lib/calendar/date.ex | 6 ++++-- lib/elixir/lib/calendar/datetime.ex | 14 ++++++++------ lib/elixir/lib/calendar/naive_datetime.ex | 2 ++ lib/elixir/lib/calendar/time.ex | 6 ++++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 7cee0e0331f..f9d235fd648 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -768,14 +768,16 @@ defmodule Date do Available units are: `:year, :month, :week, :day`. + Raises ArgumentError when called with time scale units. + + When used with the default calendar `Calendar.ISO`: + Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - when shifting by 2 weeks and 3 days the date is shifted by 17 days Durations are applied in order of the size of the unit: `month > day`. - Raises ArgumentError when called with time scale units. - ## Examples iex> Date.shift(~D[2016-01-03], month: 2) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 7fbe30c996e..a25ec49ddfb 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1681,6 +1681,14 @@ defmodule DateTime do Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. + First the datetime is converted to a naive datetime. After the shift was applied + it is converted back to a datetime using its original time zone and time zone database, + potentially resulting in an ambiguous or gap DateTime result tuple. + + Check `from_naive/3` for more information on ambiguous datetimes. + + When used with the default calendar `Calendar.ISO`: + Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - when shifting by 2 weeks and 3 days the date is shifted by 17 days @@ -1688,12 +1696,6 @@ defmodule DateTime do Durations are applied in order of the size of the unit: `month > day > second`. - First the datetime is converted to a naive datetime. After the shift was applied - it is converted back to a datetime using its original time zone and time zone database, - potentially resulting in an ambiguous or gap DateTime result tuple. - - Check `from_naive/3` for more information on ambiguous datetimes. - ## Examples iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2) diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 328ac75474a..2e3736e2c9b 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -578,6 +578,8 @@ defmodule NaiveDateTime do Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. + When used with the default calendar `Calendar.ISO`: + Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - when shifting by 2 weeks and 3 days the date is shifted by 17 days diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 38fad7e529d..e170891a6e0 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -565,10 +565,12 @@ defmodule Time do Available duration units are: `:hour, :minute, :second, :microsecond`. - All duration units are collapsed to seconds and microseconds before they are applied. - Raises ArgumentError when called with date scale units. + When used with the default calendar `Calendar.ISO`: + + All duration units are collapsed to seconds and microseconds before they are applied. + ## Examples iex> Time.shift(~T[01:00:15], hour: 12) From 73c1151791c99c7fec7588cca66bccd076aea6f1 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 22:19:43 +0100 Subject: [PATCH 55/97] quote functions --- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/datetime.ex | 2 +- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- lib/elixir/lib/calendar/time.ex | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index f9d235fd648..7d6cd2381eb 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -805,7 +805,7 @@ defmodule Date do @doc """ Shifts given `date` by `duration` according to its calendar. - Same as shift/2 but raises RuntimeError. + Same as `shift/2` but raises RuntimeError. ## Examples diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index a25ec49ddfb..bb876b63970 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1769,7 +1769,7 @@ defmodule DateTime do @doc """ Shifts given `datetime` by `duration` according to its calendar. - Same as shift/2 but raises ArgumentError. + Same as `shift/2` but raises ArgumentError. ## Examples diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 2e3736e2c9b..8a8ccabde5b 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -644,7 +644,7 @@ defmodule NaiveDateTime do @doc """ Shifts given `naive_datetime` by `duration` according to its calendar. - Same as shift/2 but raises RuntimeError. + Same as `shift/2` but raises RuntimeError. ## Examples diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index e170891a6e0..f8a6558d45e 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -610,7 +610,7 @@ defmodule Time do @doc """ Shifts given `time` by `duration` according to its calendar. - Same as shift/2 but raises RuntimeError. + Same as `shift/2` but raises RuntimeError. ## Examples From 1bf41fd8ca88fd3727b3b312af99ff9bf6cea806 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 6 Mar 2024 23:01:31 +0100 Subject: [PATCH 56/97] consolidate Date.add/2 and Time.add/3 --- lib/elixir/lib/calendar/date.ex | 7 +------ lib/elixir/lib/calendar/iso.ex | 13 ++++++++----- lib/elixir/lib/calendar/time.ex | 29 ++++++----------------------- 3 files changed, 15 insertions(+), 34 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 7d6cd2381eb..0bda7efb0a4 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -709,12 +709,7 @@ defmodule Date do @spec add(Calendar.date(), integer()) :: t def add(%{calendar: Calendar.ISO} = date, days) do %{year: year, month: month, day: day} = date - - {year, month, day} = - Calendar.ISO.date_to_iso_days(year, month, day) - |> Kernel.+(days) - |> Calendar.ISO.date_from_iso_days() - + {year, month, day} = Calendar.ISO.shift_days({year, month, day}, days) %Date{calendar: Calendar.ISO, year: year, month: month, day: day} end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 4af3c3f06d5..7497ced27f4 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1550,7 +1550,8 @@ defmodule Calendar.ISO do end) end - defp shift_days({year, month, day}, days) do + @doc false + def shift_days({year, month, day}, days) do {year, month, day} = date_to_iso_days(year, month, day) |> Kernel.+(days) @@ -1577,8 +1578,9 @@ defmodule Calendar.ISO do {new_year, new_month, new_day} end - defp shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) - when unit in [:second, :microsecond] do + @doc false + def shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) + when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do {value, precision} = shift_time_unit_values(value, microsecond) ppd = System.convert_time_unit(86400, :second, unit) @@ -1591,8 +1593,9 @@ defmodule Calendar.ISO do {year, month, day, hour, minute, second, {ms_value, precision}} end - defp shift_time_unit({hour, minute, second, microsecond}, value, unit) - when unit in [:second, :microsecond] do + @doc false + def shift_time_unit({hour, minute, second, microsecond}, value, unit) + when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do {value, precision} = shift_time_unit_values(value, microsecond) time = {0, time_to_day_fraction(hour, minute, second, microsecond)} diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index f8a6558d45e..372bb06c8b5 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -50,7 +50,6 @@ defmodule Time do calendar: Calendar.calendar() } - @parts_per_day 86_400_000_000 @seconds_per_day 24 * 60 * 60 @doc """ @@ -528,38 +527,22 @@ defmodule Time do ":microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" end - amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond) - total = time_to_microseconds(time) + amount_to_add - parts = Integer.mod(total, @parts_per_day) - precision = max(Calendar.ISO.time_unit_to_precision(unit), precision) + %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time - {hour, minute, second, {microsecond, _}} = - calendar.time_from_day_fraction({parts, @parts_per_day}) + {hour, minute, second, {ms_value, _}} = + Calendar.ISO.shift_time_unit({hour, minute, second, microsecond}, amount_to_add, unit) + + precision = max(Calendar.ISO.time_unit_to_precision(unit), precision) %Time{ hour: hour, minute: minute, second: second, - microsecond: {microsecond, precision}, + microsecond: {ms_value, precision}, calendar: calendar } end - defp time_to_microseconds(%{ - calendar: Calendar.ISO, - hour: 0, - minute: 0, - second: 0, - microsecond: {0, _} - }) do - 0 - end - - defp time_to_microseconds(time) do - iso_days = {0, to_day_fraction(time)} - Calendar.ISO.iso_days_to_unit(iso_days, :microsecond) - end - @doc """ Shifts given `time` by `duration` according to its calendar. From 4b0a2bce00c9935283b49e1a96689b76a37a9c9d Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 7 Mar 2024 08:23:10 +0100 Subject: [PATCH 57/97] Revert "consolidate Date.add/2 and Time.add/3" This reverts commit 1bf41fd8ca88fd3727b3b312af99ff9bf6cea806. --- lib/elixir/lib/calendar/date.ex | 7 ++++++- lib/elixir/lib/calendar/iso.ex | 13 +++++-------- lib/elixir/lib/calendar/time.ex | 29 +++++++++++++++++++++++------ 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 0bda7efb0a4..7d6cd2381eb 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -709,7 +709,12 @@ defmodule Date do @spec add(Calendar.date(), integer()) :: t def add(%{calendar: Calendar.ISO} = date, days) do %{year: year, month: month, day: day} = date - {year, month, day} = Calendar.ISO.shift_days({year, month, day}, days) + + {year, month, day} = + Calendar.ISO.date_to_iso_days(year, month, day) + |> Kernel.+(days) + |> Calendar.ISO.date_from_iso_days() + %Date{calendar: Calendar.ISO, year: year, month: month, day: day} end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 7497ced27f4..4af3c3f06d5 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1550,8 +1550,7 @@ defmodule Calendar.ISO do end) end - @doc false - def shift_days({year, month, day}, days) do + defp shift_days({year, month, day}, days) do {year, month, day} = date_to_iso_days(year, month, day) |> Kernel.+(days) @@ -1578,9 +1577,8 @@ defmodule Calendar.ISO do {new_year, new_month, new_day} end - @doc false - def shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) - when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do + defp shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit) + when unit in [:second, :microsecond] do {value, precision} = shift_time_unit_values(value, microsecond) ppd = System.convert_time_unit(86400, :second, unit) @@ -1593,9 +1591,8 @@ defmodule Calendar.ISO do {year, month, day, hour, minute, second, {ms_value, precision}} end - @doc false - def shift_time_unit({hour, minute, second, microsecond}, value, unit) - when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do + defp shift_time_unit({hour, minute, second, microsecond}, value, unit) + when unit in [:second, :microsecond] do {value, precision} = shift_time_unit_values(value, microsecond) time = {0, time_to_day_fraction(hour, minute, second, microsecond)} diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 372bb06c8b5..f8a6558d45e 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -50,6 +50,7 @@ defmodule Time do calendar: Calendar.calendar() } + @parts_per_day 86_400_000_000 @seconds_per_day 24 * 60 * 60 @doc """ @@ -527,22 +528,38 @@ defmodule Time do ":microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" end - %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time - - {hour, minute, second, {ms_value, _}} = - Calendar.ISO.shift_time_unit({hour, minute, second, microsecond}, amount_to_add, unit) - + amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond) + total = time_to_microseconds(time) + amount_to_add + parts = Integer.mod(total, @parts_per_day) precision = max(Calendar.ISO.time_unit_to_precision(unit), precision) + {hour, minute, second, {microsecond, _}} = + calendar.time_from_day_fraction({parts, @parts_per_day}) + %Time{ hour: hour, minute: minute, second: second, - microsecond: {ms_value, precision}, + microsecond: {microsecond, precision}, calendar: calendar } end + defp time_to_microseconds(%{ + calendar: Calendar.ISO, + hour: 0, + minute: 0, + second: 0, + microsecond: {0, _} + }) do + 0 + end + + defp time_to_microseconds(time) do + iso_days = {0, to_day_fraction(time)} + Calendar.ISO.iso_days_to_unit(iso_days, :microsecond) + end + @doc """ Shifts given `time` by `duration` according to its calendar. From d6243f1ee2398377093c4a8f13e57fd527cc6576 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 7 Mar 2024 10:22:28 +0100 Subject: [PATCH 58/97] correct shift docs --- lib/elixir/lib/calendar/date.ex | 4 ++-- lib/elixir/lib/calendar/datetime.ex | 3 +-- lib/elixir/lib/calendar/naive_datetime.ex | 3 +-- lib/elixir/lib/calendar/time.ex | 4 ++-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 7d6cd2381eb..ade66271a24 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -768,8 +768,6 @@ defmodule Date do Available units are: `:year, :month, :week, :day`. - Raises ArgumentError when called with time scale units. - When used with the default calendar `Calendar.ISO`: Durations are collapsed before they are applied: @@ -778,6 +776,8 @@ defmodule Date do Durations are applied in order of the size of the unit: `month > day`. + Raises ArgumentError when called with time scale units. + ## Examples iex> Date.shift(~D[2016-01-03], month: 2) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index bb876b63970..3e808c4fd7e 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1691,8 +1691,7 @@ defmodule DateTime do Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - - when shifting by 2 weeks and 3 days the date is shifted by 17 days - - all smaller units are collapsed into seconds and microseconds + - weeks, days and smaller units are collapsed into seconds and microseconds Durations are applied in order of the size of the unit: `month > day > second`. diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 8a8ccabde5b..d044df8e376 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -582,8 +582,7 @@ defmodule NaiveDateTime do Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - - when shifting by 2 weeks and 3 days the date is shifted by 17 days - - all smaller units are collapsed into seconds and microseconds + - weeks, days and smaller units are collapsed into seconds and microseconds Durations are applied in order of the size of the unit: `month > day > second`. diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index f8a6558d45e..66a048b312f 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -565,12 +565,12 @@ defmodule Time do Available duration units are: `:hour, :minute, :second, :microsecond`. - Raises ArgumentError when called with date scale units. - When used with the default calendar `Calendar.ISO`: All duration units are collapsed to seconds and microseconds before they are applied. + Raises ArgumentError when called with date scale units. + ## Examples iex> Time.shift(~T[01:00:15], hour: 12) From 715403afdcf2ba327e271d8d9ebb2d65e8178c81 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 7 Mar 2024 11:12:28 +0100 Subject: [PATCH 59/97] noop implementation to test calendar callback --- lib/elixir/test/elixir/calendar/date_test.exs | 7 +++---- lib/elixir/test/elixir/calendar/holocene.exs | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index 043b189f2e4..97b4e59ac72 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -202,9 +202,8 @@ defmodule DateTest do Date.shift(~D[2012-01-01], months: 12) end - assert_raise UndefinedFunctionError, fn -> - date = Calendar.Holocene.date(12000, 01, 01) - Date.shift(date, month: 12) - end + # Implements calendar callback + date = Calendar.Holocene.date(10000, 01, 01) + assert Date.shift(date, []) == {:ok, ~D[10000-01-01 Calendar.Holocene]} end end diff --git a/lib/elixir/test/elixir/calendar/holocene.exs b/lib/elixir/test/elixir/calendar/holocene.exs index 50046323c75..54bfcac0c88 100644 --- a/lib/elixir/test/elixir/calendar/holocene.exs +++ b/lib/elixir/test/elixir/calendar/holocene.exs @@ -154,4 +154,21 @@ defmodule Calendar.Holocene do @impl true defdelegate iso_days_to_end_of_day(iso_days), to: Calendar.ISO + + # The Holocene calendar extends most year and day count guards implemented in the ISO calendar, + # therefore we just return the input date parts. + @impl true + def shift_date(year, month, day, _duration) do + {year, month, day} + end + + @impl true + def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, _duration) do + {year, month, day, hour, minute, second, microsecond} + end + + @impl true + def shift_time(hour, minute, second, microsecond, _duration) do + {hour, minute, second, microsecond} + end end From dadba31d4fcf2e14a70f86b8673c08e26322da26 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 7 Mar 2024 11:33:15 +0100 Subject: [PATCH 60/97] raise instead of noop in Calendar.Holocene date shift test --- lib/elixir/test/elixir/calendar/date_test.exs | 6 ++++-- lib/elixir/test/elixir/calendar/holocene.exs | 15 +++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index 97b4e59ac72..d7ed944986b 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -203,7 +203,9 @@ defmodule DateTest do end # Implements calendar callback - date = Calendar.Holocene.date(10000, 01, 01) - assert Date.shift(date, []) == {:ok, ~D[10000-01-01 Calendar.Holocene]} + assert_raise RuntimeError, "shift_date/4 not implemented", fn -> + date = Calendar.Holocene.date(10000, 01, 01) + Date.shift(date, month: 1) + end end end diff --git a/lib/elixir/test/elixir/calendar/holocene.exs b/lib/elixir/test/elixir/calendar/holocene.exs index 54bfcac0c88..668a9569af5 100644 --- a/lib/elixir/test/elixir/calendar/holocene.exs +++ b/lib/elixir/test/elixir/calendar/holocene.exs @@ -155,20 +155,19 @@ defmodule Calendar.Holocene do @impl true defdelegate iso_days_to_end_of_day(iso_days), to: Calendar.ISO - # The Holocene calendar extends most year and day count guards implemented in the ISO calendar, - # therefore we just return the input date parts. + # The Holocene calendar extends most year and day count guards implemented in the ISO calendars. @impl true - def shift_date(year, month, day, _duration) do - {year, month, day} + def shift_date(_year, _month, _day, _duration) do + raise "shift_date/4 not implemented" end @impl true - def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, _duration) do - {year, month, day, hour, minute, second, microsecond} + def shift_naive_datetime(_year, _month, _day, _hour, _minute, _second, _microsecond, _duration) do + raise "shift_naive_datetime/8 not implemented" end @impl true - def shift_time(hour, minute, second, microsecond, _duration) do - {hour, minute, second, microsecond} + def shift_time(_hour, _minute, _second, _microsecond, _duration) do + raise "shift_time/5 not implemented" end end From 5952c1717eff3a2174d5224b21fed010b761e788 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 7 Mar 2024 12:35:44 +0100 Subject: [PATCH 61/97] docs --- lib/elixir/lib/calendar/date.ex | 4 ++-- lib/elixir/lib/calendar/datetime.ex | 4 ++-- lib/elixir/lib/calendar/naive_datetime.ex | 4 ++-- lib/elixir/lib/calendar/time.ex | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index ade66271a24..a529795367f 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -690,8 +690,8 @@ defmodule Date do The days are counted as Gregorian days. The date is returned in the same calendar as it was given in. - To move a date by a complex duration supporting various units including years, - months and weeks, days, you can use `Date.shift/2`. + To shift a date by a complex duration supporting various units including years, + months, weeks and days, you can use `Date.shift/2`. ## Examples diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 3e808c4fd7e..ac63a23a731 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1610,8 +1610,8 @@ defmodule DateTime do iex> result.microsecond {21000, 3} - To move a datetime by a complex duration supporting various units including years, - months, weeks as well as time units, you can use `DateTime.shift/2`. + To shift a datetime by a complex duration supporting various units including years, + months, weeks, days as well as time units, you can use `DateTime.shift/2`. """ @doc since: "1.8.0" @spec add( diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index d044df8e376..5d9f17ba1c0 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -448,8 +448,8 @@ defmodule NaiveDateTime do iex> NaiveDateTime.add(dt, 21, :second) ~N[2000-02-29 23:00:28] - To move a native datetime by a complex duration supporting various units including years, - months, weeks as well as time units, you can use `NaiveDateTime.shift/2`. + To shift a naive datetime by a complex duration supporting various units including years, + months, weeks, days and time units, you can use `NaiveDateTime.shift/2`. """ @doc since: "1.4.0" @spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 66a048b312f..8400823f128 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -500,7 +500,7 @@ defmodule Time do iex> result.microsecond {21000, 3} - To move a time by a complex duration supporting all time units and precisions, + To shift time by a complex duration supporting all time units and precisions, you can use `Time.shift/2`. """ @doc since: "1.6.0" From 7167711ac73b3bad9df7e1ea6f67eef6b8fabb8e Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 7 Mar 2024 12:44:30 +0100 Subject: [PATCH 62/97] doc structure --- lib/elixir/scripts/elixir_docs.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/scripts/elixir_docs.exs b/lib/elixir/scripts/elixir_docs.exs index 011547e81a9..d249184aab9 100644 --- a/lib/elixir/scripts/elixir_docs.exs +++ b/lib/elixir/scripts/elixir_docs.exs @@ -93,6 +93,7 @@ canonical = System.fetch_env!("CANONICAL") Bitwise, Date, DateTime, + Duration, Exception, Float, Function, From 45b208c6723983c165ffd1ed1711d09e4b0b1053 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 7 Mar 2024 13:30:13 +0100 Subject: [PATCH 63/97] docs --- lib/elixir/lib/calendar/date.ex | 10 +++++----- lib/elixir/lib/calendar/datetime.ex | 16 ++++++++-------- lib/elixir/lib/calendar/duration.ex | 12 ++++++++---- lib/elixir/lib/calendar/naive_datetime.ex | 10 +++++----- lib/elixir/lib/calendar/time.ex | 8 ++++---- 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index a529795367f..9c245cc96ee 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -766,7 +766,7 @@ defmodule Date do @doc """ Shifts given `date` by `duration` according to its calendar. - Available units are: `:year, :month, :week, :day`. + Allowed units are: `:year, :month, :week, :day`. When used with the default calendar `Calendar.ISO`: @@ -798,8 +798,8 @@ defmodule Date do {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} end - def shift(date, duration_units) do - shift(date, Duration.new(duration_units)) + def shift(date, duration) do + shift(date, Duration.new(duration)) end @doc """ @@ -815,8 +815,8 @@ defmodule Date do """ @doc since: "1.7.0" @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: t - def shift!(date, duration_units) do - case shift(date, duration_units) do + def shift!(date, duration) do + case shift(date, duration) do {:ok, date} -> date diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index ac63a23a731..9dfa12dce15 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1679,7 +1679,7 @@ defmodule DateTime do @doc """ Shifts given `datetime` by `duration` according to its calendar. - Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. + Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. First the datetime is converted to a naive datetime. After the shift was applied it is converted back to a datetime using its original time zone and time zone database, @@ -1761,8 +1761,8 @@ defmodule DateTime do ) end - def shift(datetime, duration_units, time_zone_database) do - shift(datetime, Duration.new(duration_units), time_zone_database) + def shift(datetime, duration, time_zone_database) do + shift(datetime, Duration.new(duration), time_zone_database) end @doc """ @@ -1778,26 +1778,26 @@ defmodule DateTime do """ @doc since: "1.7.0" @spec shift!(Calendar.datetime(), Duration.t() | [Duration.unit()]) :: t - def shift!(%{time_zone: time_zone} = datetime, duration_units) do - case shift(datetime, duration_units) do + def shift!(%{time_zone: time_zone} = datetime, duration) do + case shift(datetime, duration) do {:ok, datetime} -> datetime {:ambiguous, dt1, dt2} -> raise ArgumentError, - "cannot shift datetime #{inspect(datetime)} by #{inspect(duration_units)} because the result " <> + "cannot shift datetime #{inspect(datetime)} by #{inspect(duration)} because the result " <> "is ambiguous in time zone #{time_zone} as there is an overlap " <> "between #{inspect(dt1)} and #{inspect(dt2)}" {:gap, dt1, dt2} -> raise ArgumentError, - "cannot shift datetime #{inspect(datetime)} by #{inspect(duration_units)} because the result " <> + "cannot shift datetime #{inspect(datetime)} by #{inspect(duration)} because the result " <> "does not exist in time zone #{time_zone} as there is a gap " <> "between #{inspect(dt1)} and #{inspect(dt2)}" {:error, reason} -> raise ArgumentError, - "cannot shift datetime #{inspect(datetime)} by #{inspect(duration_units)}, reason: #{inspect(reason)}" + "cannot shift datetime #{inspect(datetime)} by #{inspect(duration)}, reason: #{inspect(reason)}" end end diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index eccbf8d751a..a8aab0ca1b8 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -1,10 +1,14 @@ defmodule Duration do @moduledoc """ - The Duration type implements the concept of duration applicable to all calendar types. + Struct and functions for handling durations. - A `Duration` has time scale units represented as integers with - the exception of microseconds, which are represented as a tuple `{microsecond, precision}`, - to be compatible with other calendar types implementing time, such as `Time`, `DateTime` and `NaiveDateTime`. + A `Duration` struct represents a collection of time scale units, + allowing for manipulation and calculation of durations. + + Date and time scale units are represented as integers, allowing for both positive and negative values. + + Microseconds are represented using a tuple `{microsecond, precision}`. + This ensures compatibility with other calendar types implementing time, such as `Time`, `DateTime`, and `NaiveDateTime`. """ defstruct year: 0, diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 5d9f17ba1c0..f6b828fa6d9 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -576,7 +576,7 @@ defmodule NaiveDateTime do @doc """ Shifts given `naive_datetime` by `duration` according to its calendar. - Available units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. + Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. When used with the default calendar `Calendar.ISO`: @@ -636,8 +636,8 @@ defmodule NaiveDateTime do }} end - def shift(naive_datetime, duration_units) do - shift(naive_datetime, Duration.new(duration_units)) + def shift(naive_datetime, duration) do + shift(naive_datetime, Duration.new(duration)) end @doc """ @@ -653,8 +653,8 @@ defmodule NaiveDateTime do """ @doc since: "1.7.0" @spec shift!(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: t - def shift!(naive_datetime, duration_units) do - case shift(naive_datetime, duration_units) do + def shift!(naive_datetime, duration) do + case shift(naive_datetime, duration) do {:ok, naive_datetime} -> naive_datetime diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 8400823f128..b20bbbc3700 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -603,8 +603,8 @@ defmodule Time do }} end - def shift(time, duration_units) do - shift(time, Duration.new(duration_units)) + def shift(time, duration) do + shift(time, Duration.new(duration)) end @doc """ @@ -620,8 +620,8 @@ defmodule Time do """ @doc since: "1.7.0" @spec shift!(Calendar.time(), Duration.t() | [Duration.unit()]) :: t - def shift!(time, duration_units) do - case shift(time, duration_units) do + def shift!(time, duration) do + case shift(time, duration) do {:ok, time} -> time From 4536554487ac908fe9348bb2824c6f9fa3a202ce Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Fri, 8 Mar 2024 14:42:44 +0100 Subject: [PATCH 64/97] datetime shift time zone docs --- lib/elixir/lib/calendar/datetime.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 9dfa12dce15..de4ad684a42 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1681,11 +1681,13 @@ defmodule DateTime do Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - First the datetime is converted to a naive datetime. After the shift was applied - it is converted back to a datetime using its original time zone and time zone database, - potentially resulting in an ambiguous or gap DateTime result tuple. + When it comes to time zones, this function is equivalent to shifting the wall time + (the time a person at said timezone would see on a clock) by the given duration. + Then we validate if the shifted wall time is valid. For example, in time zones that + observe "Daylight Saving Time", a particular wall time may not exist when moving + the clock forward or be ambiguous when moving the clock back. - Check `from_naive/3` for more information on ambiguous datetimes. + Check `from_naive/3` for more information on the possible result types. When used with the default calendar `Calendar.ISO`: From a6fcc6e14e229964fe315dfb57e694811ed8b35a Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Fri, 8 Mar 2024 14:55:05 +0100 Subject: [PATCH 65/97] test Calendar.ISO.shift_time/5 --- lib/elixir/test/elixir/calendar/iso_test.exs | 68 ++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 8ec0d67bb96..e8d5cde02c4 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -583,5 +583,73 @@ defmodule Calendar.ISOTest do {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_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}} + + 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(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(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: {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, {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, 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}} end end From 7adf6fc7f16a00234d2ba2c4819bc15eed5935ad Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 9 Mar 2024 09:32:40 +0100 Subject: [PATCH 66/97] drop redundant clause in shift_months/2 --- lib/elixir/lib/calendar/iso.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 4af3c3f06d5..ab217476940 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1567,7 +1567,6 @@ defmodule Calendar.ISO do new_month = case rem(total_months, months_in_year) + 1 do - 0 -> months_in_year new_month when new_month < 1 -> new_month + months_in_year new_month -> new_month end From b5142cd08a91d00a70f1f6b46863489cefef2906 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 9 Mar 2024 09:34:51 +0100 Subject: [PATCH 67/97] consistent annotation --- lib/elixir/lib/calendar/iso.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index ab217476940..b593527abf8 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1469,8 +1469,8 @@ defmodule Calendar.ISO do iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new(year: 4, day: 1)) {2020, 2, 1} """ - @spec shift_date(year, month, day, Duration.t()) :: {year, month, day} @impl true + @spec shift_date(year, month, day, Duration.t()) :: {year, month, day} def shift_date(year, month, day, duration) do shift_options = shift_date_options(duration) @@ -1498,6 +1498,7 @@ defmodule Calendar.ISO do iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(microsecond: {100, 6})) {2016, 1, 3, 0, 0, 0, {100, 6}} """ + @impl true @spec shift_naive_datetime( year, month, @@ -1508,7 +1509,6 @@ defmodule Calendar.ISO do microsecond, Duration.t() ) :: {year, month, day, hour, minute, second, microsecond} - @impl true def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do shift_options = shift_datetime_options(duration) @@ -1535,9 +1535,9 @@ defmodule Calendar.ISO do iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(microsecond: {100, 6})) {13, 0, 0, {100, 6}} """ + @impl true @spec shift_time(hour, minute, second, microsecond, Duration.t()) :: {hour, minute, second, microsecond} - @impl true def shift_time(hour, minute, second, microsecond, duration) do shift_options = shift_time_options(duration) From 1696f3c48c3498979ffd0877de4aa07687c7560a Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 9 Mar 2024 10:47:34 +0100 Subject: [PATCH 68/97] concise hint on add/2 --- lib/elixir/lib/calendar/date.ex | 3 +-- lib/elixir/lib/calendar/datetime.ex | 3 +-- lib/elixir/lib/calendar/naive_datetime.ex | 3 +-- lib/elixir/lib/calendar/time.ex | 3 +-- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 9c245cc96ee..681863cb5ae 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -690,8 +690,7 @@ defmodule Date do The days are counted as Gregorian days. The date is returned in the same calendar as it was given in. - To shift a date by a complex duration supporting various units including years, - months, weeks and days, you can use `Date.shift/2`. + To shift a date by a `Duration`, use `Date.shift/2`. ## Examples diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index de4ad684a42..a9dd6efa37d 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1610,8 +1610,7 @@ defmodule DateTime do iex> result.microsecond {21000, 3} - To shift a datetime by a complex duration supporting various units including years, - months, weeks, days as well as time units, you can use `DateTime.shift/2`. + To shift a datetime by a `Duration`, use `DateTime.shift/3`. """ @doc since: "1.8.0" @spec add( diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index f6b828fa6d9..e4f2fe4f461 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -448,8 +448,7 @@ defmodule NaiveDateTime do iex> NaiveDateTime.add(dt, 21, :second) ~N[2000-02-29 23:00:28] - To shift a naive datetime by a complex duration supporting various units including years, - months, weeks, days and time units, you can use `NaiveDateTime.shift/2`. + To shift a naive datetime by a `Duration`, use `NaiveDateTime.shift/2`. """ @doc since: "1.4.0" @spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index b20bbbc3700..341100ef28b 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -500,8 +500,7 @@ defmodule Time do iex> result.microsecond {21000, 3} - To shift time by a complex duration supporting all time units and precisions, - you can use `Time.shift/2`. + To shift a time by a `Duration`, use `Time.shift/2`. """ @doc since: "1.6.0" @spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t From 86f088bd19f68c6424c7e10774aa0539d341619e Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 11 Mar 2024 10:59:14 +0100 Subject: [PATCH 69/97] DateTime.shift/3 as coordinated universal time --- lib/elixir/lib/calendar/datetime.ex | 23 ++++++++++------ .../test/elixir/calendar/datetime_test.exs | 27 ++++++++++++------- lib/elixir/test/elixir/calendar/fakes.exs | 18 +++++++++++++ 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index a9dd6efa37d..34d2aa6f8f1 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1722,7 +1722,11 @@ defmodule DateTime do | :utc_only_time_zone_database} def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) - def shift(%{calendar: calendar} = datetime, %Duration{} = duration, time_zone_database) do + def shift( + %{calendar: calendar, time_zone: original_time_zone} = datetime, + %Duration{} = duration, + time_zone_database + ) do %{ year: year, month: month, @@ -1730,9 +1734,8 @@ defmodule DateTime do hour: hour, minute: minute, second: second, - microsecond: microsecond, - time_zone: time_zone - } = datetime + microsecond: microsecond + } = shift_zone!(datetime, "Etc/UTC", time_zone_database) {year, month, day, hour, minute, second, microsecond} = calendar.shift_naive_datetime( @@ -1746,8 +1749,8 @@ defmodule DateTime do duration ) - from_naive( - %NaiveDateTime{ + shift_zone( + %DateTime{ calendar: calendar, year: year, month: month, @@ -1755,9 +1758,13 @@ defmodule DateTime do hour: hour, minute: minute, second: second, - microsecond: microsecond + microsecond: microsecond, + std_offset: 0, + utc_offset: 0, + time_zone: "Etc/UTC", + zone_abbr: "UTC" }, - time_zone, + original_time_zone, time_zone_database ) end diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index 69e968d1324..da4caa83c9a 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1098,15 +1098,24 @@ defmodule DateTimeTest do {:ok, ~U[1999-12-31 00:00:00Z]} datetime = - DateTime.new!( - Date.new!(2018, 10, 27), - Time.new!(2, 30, 0, {0, 0}), - "Europe/Copenhagen", - FakeTimeZoneDatabase - ) - - assert {:ambiguous, %DateTime{}, %DateTime{}} = - DateTime.shift(datetime, [day: 1], FakeTimeZoneDatabase) + DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase) + + assert DateTime.shift(datetime, [hour: 1], FakeTimeZoneDatabase) == + {:ok, + %DateTime{ + calendar: Calendar.ISO, + year: 2019, + month: 03, + day: 31, + hour: 3, + minute: 00, + second: 0, + microsecond: {0, 0}, + time_zone: "Europe/Copenhagen", + std_offset: 3600, + utc_offset: 3600, + zone_abbr: "CEST" + }} assert_raise KeyError, ~s/key :months not found/, fn -> DateTime.shift(~U[2012-01-01 00:00:00Z], months: 12) diff --git a/lib/elixir/test/elixir/calendar/fakes.exs b/lib/elixir/test/elixir/calendar/fakes.exs index 3da151e0ac4..d6428fe2126 100644 --- a/lib/elixir/test/elixir/calendar/fakes.exs +++ b/lib/elixir/test/elixir/calendar/fakes.exs @@ -113,6 +113,15 @@ defmodule FakeTimeZoneDatabase do }} end + defp time_zone_periods_from_utc("Etc/UTC", _erl_datetime) do + {:ok, + %{ + std_offset: 0, + utc_offset: 0, + zone_abbr: "UTC" + }} + end + defp time_zone_periods_from_utc(time_zone, _) when time_zone != "Europe/Copenhagen" do {:error, :time_zone_not_found} end @@ -171,6 +180,15 @@ defmodule FakeTimeZoneDatabase do }} end + defp time_zone_periods_from_wall("Etc/UTC", _erl_datetime) do + {:ok, + %{ + std_offset: 0, + utc_offset: 0, + zone_abbr: "UTC" + }} + end + defp time_zone_periods_from_wall(time_zone, _) when time_zone != "Europe/Copenhagen" do {:error, :time_zone_not_found} end From c8e904ac1d62f077b382212c7511a6cdd93c739d Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Mon, 11 Mar 2024 14:16:48 +0100 Subject: [PATCH 70/97] specs --- lib/elixir/lib/calendar/datetime.ex | 108 +++++++++++----------------- 1 file changed, 43 insertions(+), 65 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 34d2aa6f8f1..e7cadc2dcba 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1680,13 +1680,9 @@ defmodule DateTime do Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - When it comes to time zones, this function is equivalent to shifting the wall time - (the time a person at said timezone would see on a clock) by the given duration. - Then we validate if the shifted wall time is valid. For example, in time zones that - observe "Daylight Saving Time", a particular wall time may not exist when moving - the clock forward or be ambiguous when moving the clock back. - - Check `from_naive/3` for more information on the possible result types. + This function considers "Daylight Saving Time" and other time gaps across time zones + by converting the given datetime to Etc/UTC before executing the shift. Finally, + the datetime is converted back to its original time zone. When used with the default calendar `Calendar.ISO`: @@ -1712,14 +1708,7 @@ defmodule DateTime do Calendar.datetime(), Duration.t() | [Duration.unit()], Calendar.time_zone_database() - ) :: - {:ok, t} - | {:ambiguous, first_datetime :: t, second_datetime :: t} - | {:gap, t, t} - | {:error, - :incompatible_calendars - | :time_zone_not_found - | :utc_only_time_zone_database} + ) :: {:ok, t} | {:error, :time_zone_not_found | :utc_only_time_zone_database} def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) def shift( @@ -1727,46 +1716,48 @@ defmodule DateTime do %Duration{} = duration, time_zone_database ) do - %{ - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - } = shift_zone!(datetime, "Etc/UTC", time_zone_database) - - {year, month, day, hour, minute, second, microsecond} = - calendar.shift_naive_datetime( - year, - month, - day, - hour, - minute, - second, - microsecond, - duration - ) - - shift_zone( - %DateTime{ - calendar: calendar, + with {:ok, datetime} <- shift_zone(datetime, "Etc/UTC", time_zone_database) do + %{ year: year, month: month, day: day, hour: hour, minute: minute, second: second, - microsecond: microsecond, - std_offset: 0, - utc_offset: 0, - time_zone: "Etc/UTC", - zone_abbr: "UTC" - }, - original_time_zone, - time_zone_database - ) + microsecond: microsecond + } = datetime + + {year, month, day, hour, minute, second, microsecond} = + calendar.shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + duration + ) + + shift_zone( + %DateTime{ + calendar: calendar, + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond, + std_offset: 0, + utc_offset: 0, + time_zone: "Etc/UTC", + zone_abbr: "UTC" + }, + original_time_zone, + time_zone_database + ) + end end def shift(datetime, duration, time_zone_database) do @@ -1786,26 +1777,13 @@ defmodule DateTime do """ @doc since: "1.7.0" @spec shift!(Calendar.datetime(), Duration.t() | [Duration.unit()]) :: t - def shift!(%{time_zone: time_zone} = datetime, duration) do + def shift!(datetime, duration) do case shift(datetime, duration) do {:ok, datetime} -> datetime - {:ambiguous, dt1, dt2} -> - raise ArgumentError, - "cannot shift datetime #{inspect(datetime)} by #{inspect(duration)} because the result " <> - "is ambiguous in time zone #{time_zone} as there is an overlap " <> - "between #{inspect(dt1)} and #{inspect(dt2)}" - - {:gap, dt1, dt2} -> - raise ArgumentError, - "cannot shift datetime #{inspect(datetime)} by #{inspect(duration)} because the result " <> - "does not exist in time zone #{time_zone} as there is a gap " <> - "between #{inspect(dt1)} and #{inspect(dt2)}" - - {:error, reason} -> - raise ArgumentError, - "cannot shift datetime #{inspect(datetime)} by #{inspect(duration)}, reason: #{inspect(reason)}" + reason -> + raise RuntimeError, "cannot shift datetime, reason: #{inspect(reason)}" end end From 1200e2796788a87ddca72cfb2ed13fe5a2f6b60a Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Tue, 12 Mar 2024 12:17:40 +0100 Subject: [PATCH 71/97] apply offset after wall clock shift --- lib/elixir/lib/calendar/datetime.ex | 72 +++++++------------ .../test/elixir/calendar/datetime_test.exs | 20 ++++++ lib/elixir/test/elixir/calendar/fakes.exs | 21 ++++-- 3 files changed, 62 insertions(+), 51 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index e7cadc2dcba..64e319205d0 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1711,53 +1711,35 @@ defmodule DateTime do ) :: {:ok, t} | {:error, :time_zone_not_found | :utc_only_time_zone_database} def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) - def shift( - %{calendar: calendar, time_zone: original_time_zone} = datetime, - %Duration{} = duration, - time_zone_database - ) do - with {:ok, datetime} <- shift_zone(datetime, "Etc/UTC", time_zone_database) do - %{ - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - } = datetime - - {year, month, day, hour, minute, second, microsecond} = - calendar.shift_naive_datetime( - year, - month, - day, - hour, - minute, - second, - microsecond, - duration - ) + def shift(%{calendar: calendar} = datetime, %Duration{} = duration, time_zone_database) do + %{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: {_, precision} = microsecond, + std_offset: std_offset, + utc_offset: utc_offset, + time_zone: time_zone + } = datetime - shift_zone( - %DateTime{ - calendar: calendar, - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond, - std_offset: 0, - utc_offset: 0, - time_zone: "Etc/UTC", - zone_abbr: "UTC" - }, - original_time_zone, - time_zone_database + {year, month, day, hour, minute, second, microsecond} = + calendar.shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + duration ) - end + + calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) + |> apply_tz_offset(utc_offset + std_offset) + |> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database) end def shift(datetime, duration, time_zone_database) do diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index da4caa83c9a..24ff46612ba 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1097,6 +1097,26 @@ defmodule DateTimeTest do assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1, day: -29) == {:ok, ~U[1999-12-31 00:00:00Z]} + datetime = + DateTime.new!(~D[2018-06-01], ~T[03:00:00], "America/Los_Angeles", FakeTimeZoneDatabase) + + assert DateTime.shift(datetime, [month: -1], FakeTimeZoneDatabase) == + {:ok, + %DateTime{ + calendar: Calendar.ISO, + year: 2018, + month: 5, + day: 1, + hour: 3, + minute: 00, + second: 0, + microsecond: {0, 0}, + time_zone: "America/Los_Angeles", + std_offset: 3600, + utc_offset: -28800, + zone_abbr: "PDT" + }} + datetime = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase) diff --git a/lib/elixir/test/elixir/calendar/fakes.exs b/lib/elixir/test/elixir/calendar/fakes.exs index d6428fe2126..9c611d51f4a 100644 --- a/lib/elixir/test/elixir/calendar/fakes.exs +++ b/lib/elixir/test/elixir/calendar/fakes.exs @@ -53,6 +53,14 @@ defmodule FakeTimeZoneDatabase do until_wall: ~N[2019-10-27 03:00:00] } + @time_zone_period_usla_summer_2018 %{ + std_offset: 3600, + utc_offset: -28800, + zone_abbr: "PDT", + from_wall: ~N[2018-03-11 10:00:00], + until_wall: ~N[2018-11-04 09:00:00] + } + @spec time_zone_period_from_utc_iso_days(Calendar.iso_days(), Calendar.time_zone()) :: {:ok, TimeZoneDatabase.time_zone_period()} | {:error, :time_zone_not_found} @impl true @@ -105,12 +113,7 @@ defmodule FakeTimeZoneDatabase do defp time_zone_periods_from_utc("America/Los_Angeles", erl_datetime) when erl_datetime >= {{2018, 3, 11}, {10, 0, 0}} and erl_datetime < {{2018, 11, 4}, {9, 0, 0}} do - {:ok, - %{ - std_offset: 3600, - utc_offset: -28800, - zone_abbr: "PDT" - }} + {:ok, @time_zone_period_usla_summer_2018} end defp time_zone_periods_from_utc("Etc/UTC", _erl_datetime) do @@ -158,6 +161,12 @@ defmodule FakeTimeZoneDatabase do {:ok, @time_zone_period_cph_summer_2019} end + defp time_zone_periods_from_wall("America/Los_Angeles", erl_datetime) + when erl_datetime >= {{2018, 3, 11}, {10, 0, 0}} and + erl_datetime < {{2018, 11, 4}, {9, 0, 0}} do + {:ok, @time_zone_period_usla_summer_2018} + end + defp time_zone_periods_from_wall("Europe/Copenhagen", erl_datetime) when erl_datetime >= {{2015, 3, 29}, {3, 0, 0}} and erl_datetime < {{2015, 10, 25}, {3, 0, 0}} do From c0b421718522c76c6a067c4ef84092f784e98cf1 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Tue, 12 Mar 2024 12:38:47 +0100 Subject: [PATCH 72/97] respect duration precision in DateTime.shift/3 --- lib/elixir/lib/calendar/datetime.ex | 4 ++-- lib/elixir/test/elixir/calendar/datetime_test.exs | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 64e319205d0..a41fe94b6e0 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1719,13 +1719,13 @@ defmodule DateTime do hour: hour, minute: minute, second: second, - microsecond: {_, precision} = microsecond, + microsecond: microsecond, std_offset: std_offset, utc_offset: utc_offset, time_zone: time_zone } = datetime - {year, month, day, hour, minute, second, microsecond} = + {year, month, day, hour, minute, second, {_, precision} = microsecond} = calendar.shift_naive_datetime( year, month, diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index 24ff46612ba..cb576949ace 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1085,6 +1085,9 @@ defmodule DateTimeTest do assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 2, day: 29) == {:ok, ~U[2000-03-30 00:00:00Z]} + assert DateTime.shift(~U[2000-01-01 00:00:00Z], microsecond: {4000, 4}) == + {:ok, ~U[2000-01-01 00:00:00.0040Z]} + assert DateTime.shift(~U[2000-02-29 00:00:00Z], year: -1) == {:ok, ~U[1999-02-28 00:00:00Z]} assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1) == {:ok, ~U[2000-01-29 00:00:00Z]} From ebf48b1106470aabf8b1c9be79b97e11fd6ed041 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 13 Mar 2024 20:42:59 +0100 Subject: [PATCH 73/97] return calendar type instead of tuple --- lib/elixir/lib/calendar/date.ex | 37 ++------ lib/elixir/lib/calendar/datetime.ex | 50 ++++------- lib/elixir/lib/calendar/naive_datetime.ex | 54 ++++-------- lib/elixir/lib/calendar/time.ex | 50 +++-------- lib/elixir/test/elixir/calendar/date_test.exs | 24 +++--- .../test/elixir/calendar/datetime_test.exs | 86 +++++++++---------- .../elixir/calendar/naive_datetime_test.exs | 47 +++++----- lib/elixir/test/elixir/calendar/time_test.exs | 14 +-- 8 files changed, 134 insertions(+), 228 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 681863cb5ae..bd8300a071e 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -52,7 +52,7 @@ defmodule Date do ~D[2010-04-17] iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2) - {:ok, ~D[2010-04-17]} + ~D[2010-04-17] Those functions are optimized to deal with common epochs, such as the Unix Epoch above or the Gregorian Epoch (0000-01-01). @@ -780,50 +780,27 @@ defmodule Date do ## Examples iex> Date.shift(~D[2016-01-03], month: 2) - {:ok, ~D[2016-03-03]} + ~D[2016-03-03] iex> Date.shift(~D[2016-01-30], month: 1) - {:ok, ~D[2016-02-29]} + ~D[2016-02-29] iex> Date.shift(~D[2016-01-31], year: 4, day: 1) - {:ok, ~D[2020-02-01]} + ~D[2020-02-01] iex> Date.shift(~D[2016-01-03], Duration.new(month: 2)) - {:ok, ~D[2016-03-03]} + ~D[2016-03-03] """ @doc since: "1.7.0" - @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: {:ok, t} + @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: t def shift(%{calendar: calendar} = date, %Duration{} = duration) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) - {:ok, %Date{calendar: calendar, year: year, month: month, day: day}} + %Date{calendar: calendar, year: year, month: month, day: day} end def shift(date, duration) do shift(date, Duration.new(duration)) end - @doc """ - Shifts given `date` by `duration` according to its calendar. - - Same as `shift/2` but raises RuntimeError. - - ## Examples - - iex> Date.shift!(~D[2016-01-03], month: 2) - ~D[2016-03-03] - - """ - @doc since: "1.7.0" - @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: t - def shift!(date, duration) do - case shift(date, duration) do - {:ok, date} -> - date - - reason -> - raise RuntimeError, "cannot shift date, reason: #{inspect(reason)}" - end - 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}} diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index a41fe94b6e0..736fa28284e 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1695,20 +1695,20 @@ defmodule DateTime do ## Examples iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2) - {:ok, ~U[2016-03-01 00:00:00Z]} + ~U[2016-03-01 00:00:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4) - {:ok, ~U[2017-01-29 00:00:00Z]} + ~U[2017-01-29 00:00:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 25) - {:ok, ~U[2016-01-01 00:25:00Z]} + ~U[2016-01-01 00:25:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4}) - {:ok, ~U[2016-01-01 00:05:00.0005Z]} + ~U[2016-01-01 00:05:00.0005Z] """ @doc since: "1.7.0" @spec shift( Calendar.datetime(), Duration.t() | [Duration.unit()], Calendar.time_zone_database() - ) :: {:ok, t} | {:error, :time_zone_not_found | :utc_only_time_zone_database} + ) :: t def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) def shift(%{calendar: calendar} = datetime, %Duration{} = duration, time_zone_database) do @@ -1737,38 +1737,26 @@ defmodule DateTime do duration ) - calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) - |> apply_tz_offset(utc_offset + std_offset) - |> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database) + result = + calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond) + |> apply_tz_offset(utc_offset + std_offset) + |> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database) + + case result do + {:ok, result_datetime} -> + result_datetime + + {:error, error} -> + raise ArgumentError, + "cannot shift #{inspect(datetime)} to #{inspect(duration)} (with time zone " <> + "database #{inspect(time_zone_database)}), reason: #{inspect(error)}" + end end def shift(datetime, duration, time_zone_database) do shift(datetime, Duration.new(duration), time_zone_database) end - @doc """ - Shifts given `datetime` by `duration` according to its calendar. - - Same as `shift/2` but raises ArgumentError. - - ## Examples - - iex> DateTime.shift!(~U[2016-01-01 00:00:00Z], month: 2) - ~U[2016-03-01 00:00:00Z] - - """ - @doc since: "1.7.0" - @spec shift!(Calendar.datetime(), Duration.t() | [Duration.unit()]) :: t - def shift!(datetime, duration) do - case shift(datetime, duration) do - {:ok, datetime} -> - datetime - - reason -> - raise RuntimeError, "cannot shift datetime, reason: #{inspect(reason)}" - end - end - @doc """ Returns the given datetime with the microsecond field truncated to the given precision (`:microsecond`, `:millisecond` or `:second`). diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index e4f2fe4f461..0d0946d9853 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -588,17 +588,17 @@ defmodule NaiveDateTime do ## Examples iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) - {:ok, ~N[2016-02-29 00:00:00]} + ~N[2016-02-29 00:00:00] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1) - {:ok, ~N[2020-02-01 00:00:00]} + ~N[2020-02-01 00:00:00] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45) - {:ok, ~N[2016-01-31 00:00:45]} + ~N[2016-01-31 00:00:45] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6}) - {:ok, ~N[2016-01-31 00:00:00.000100]} + ~N[2016-01-31 00:00:00.000100] """ @doc since: "1.7.0" - @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: {:ok, t} + @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: t def shift(%{calendar: calendar} = naive_datetime, %Duration{} = duration) do %{ year: year, @@ -622,46 +622,22 @@ defmodule NaiveDateTime do duration ) - {:ok, - %NaiveDateTime{ - calendar: calendar, - year: year, - month: month, - day: day, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - }} + %NaiveDateTime{ + calendar: calendar, + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + } end def shift(naive_datetime, duration) do shift(naive_datetime, Duration.new(duration)) end - @doc """ - Shifts given `naive_datetime` by `duration` according to its calendar. - - Same as `shift/2` but raises RuntimeError. - - ## Examples - - iex> NaiveDateTime.shift!(~N[2016-01-31 00:00:00], month: 1) - ~N[2016-02-29 00:00:00] - - """ - @doc since: "1.7.0" - @spec shift!(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: t - def shift!(naive_datetime, duration) do - case shift(naive_datetime, duration) do - {:ok, naive_datetime} -> - naive_datetime - - reason -> - raise RuntimeError, "cannot shift naive_datetime, reason: #{inspect(reason)}" - end - end - @doc """ Returns the given naive datetime with the microsecond field truncated to the given precision (`:microsecond`, `:millisecond` or `:second`). diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 341100ef28b..2070fae7a29 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -573,62 +573,38 @@ defmodule Time do ## Examples iex> Time.shift(~T[01:00:15], hour: 12) - {:ok, ~T[13:00:15]} + ~T[13:00:15] iex> Time.shift(~T[01:15:00], hour: 6, minute: 15) - {:ok, ~T[07:30:00]} + ~T[07:30:00] iex> Time.shift(~T[01:15:00], second: 125) - {:ok, ~T[01:17:05]} + ~T[01:17:05] iex> Time.shift(~T[01:00:15], microsecond: {100, 6}) - {:ok, ~T[01:00:15.000100]} + ~T[01:00:15.000100] iex> Time.shift(~T[01:15:00], Duration.new(second: 65)) - {:ok, ~T[01:16:05]} + ~T[01:16:05] """ @doc since: "1.7.0" - @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: {:ok, t} + @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: t def shift(%{calendar: calendar} = time, %Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time {hour, minute, second, microsecond} = calendar.shift_time(hour, minute, second, microsecond, duration) - {:ok, - %Time{ - calendar: calendar, - hour: hour, - minute: minute, - second: second, - microsecond: microsecond - }} + %Time{ + calendar: calendar, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + } end def shift(time, duration) do shift(time, Duration.new(duration)) end - @doc """ - Shifts given `time` by `duration` according to its calendar. - - Same as `shift/2` but raises RuntimeError. - - ## Examples - - iex> Time.shift!(~T[01:00:15], hour: 12) - ~T[13:00:15] - - """ - @doc since: "1.7.0" - @spec shift!(Calendar.time(), Duration.t() | [Duration.unit()]) :: t - def shift!(time, duration) do - case shift(time, duration) do - {:ok, time} -> - time - - reason -> - raise RuntimeError, "cannot shift time, reason: #{inspect(reason)}" - end - end - @doc """ Compares two time structs. diff --git a/lib/elixir/test/elixir/calendar/date_test.exs b/lib/elixir/test/elixir/calendar/date_test.exs index d7ed944986b..68d1ca8d05e 100644 --- a/lib/elixir/test/elixir/calendar/date_test.exs +++ b/lib/elixir/test/elixir/calendar/date_test.exs @@ -181,18 +181,18 @@ defmodule DateTest do end test "shift/2" do - assert Date.shift(~D[2012-02-29], day: -1) == {:ok, ~D[2012-02-28]} - assert Date.shift(~D[2012-02-29], month: -1) == {:ok, ~D[2012-01-29]} - assert Date.shift(~D[2012-02-29], week: -9) == {:ok, ~D[2011-12-28]} - assert Date.shift(~D[2012-02-29], month: 1) == {:ok, ~D[2012-03-29]} - assert Date.shift(~D[2012-02-29], year: -1) == {:ok, ~D[2011-02-28]} - assert Date.shift(~D[2012-02-29], year: 4) == {:ok, ~D[2016-02-29]} - assert Date.shift(~D[0000-01-01], day: -1) == {:ok, ~D[-0001-12-31]} - assert Date.shift(~D[0000-01-01], month: -1) == {:ok, ~D[-0001-12-01]} - assert Date.shift(~D[0000-01-01], year: -1) == {:ok, ~D[-0001-01-01]} - assert Date.shift(~D[0000-01-01], year: -1) == {:ok, ~D[-0001-01-01]} - assert Date.shift(~D[2000-01-01], month: 12) == {:ok, ~D[2001-01-01]} - assert Date.shift(~D[0000-01-01], day: 2, year: 1, month: 37) == {:ok, ~D[0004-02-03]} + assert Date.shift(~D[2012-02-29], day: -1) == ~D[2012-02-28] + assert Date.shift(~D[2012-02-29], month: -1) == ~D[2012-01-29] + assert Date.shift(~D[2012-02-29], week: -9) == ~D[2011-12-28] + assert Date.shift(~D[2012-02-29], month: 1) == ~D[2012-03-29] + assert Date.shift(~D[2012-02-29], year: -1) == ~D[2011-02-28] + assert Date.shift(~D[2012-02-29], year: 4) == ~D[2016-02-29] + assert Date.shift(~D[0000-01-01], day: -1) == ~D[-0001-12-31] + assert Date.shift(~D[0000-01-01], month: -1) == ~D[-0001-12-01] + assert Date.shift(~D[0000-01-01], year: -1) == ~D[-0001-01-01] + assert Date.shift(~D[0000-01-01], year: -1) == ~D[-0001-01-01] + 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, ~s/cannot shift date by time units/, fn -> Date.shift(~D[2012-02-29], second: 86400) diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index cb576949ace..ed7f2eb5b0e 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1073,72 +1073,64 @@ defmodule DateTimeTest do end test "shift/2" do - assert DateTime.shift(~U[2000-01-01 00:00:00Z], year: 1) == {:ok, ~U[2001-01-01 00:00:00Z]} - assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1) == {:ok, ~U[2000-02-01 00:00:00Z]} - - assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1, day: 28) == - {:ok, ~U[2000-02-29 00:00:00Z]} - - assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1, day: 30) == - {:ok, ~U[2000-03-02 00:00:00Z]} - - assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 2, day: 29) == - {:ok, ~U[2000-03-30 00:00:00Z]} + assert DateTime.shift(~U[2000-01-01 00:00:00Z], year: 1) == ~U[2001-01-01 00:00:00Z] + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1) == ~U[2000-02-01 00:00:00Z] + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1, day: 28) == ~U[2000-02-29 00:00:00Z] + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 1, day: 30) == ~U[2000-03-02 00:00:00Z] + assert DateTime.shift(~U[2000-01-01 00:00:00Z], month: 2, day: 29) == ~U[2000-03-30 00:00:00Z] assert DateTime.shift(~U[2000-01-01 00:00:00Z], microsecond: {4000, 4}) == - {:ok, ~U[2000-01-01 00:00:00.0040Z]} + ~U[2000-01-01 00:00:00.0040Z] - assert DateTime.shift(~U[2000-02-29 00:00:00Z], year: -1) == {:ok, ~U[1999-02-28 00:00:00Z]} - assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1) == {:ok, ~U[2000-01-29 00:00:00Z]} + assert DateTime.shift(~U[2000-02-29 00:00:00Z], year: -1) == ~U[1999-02-28 00:00:00Z] + assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1) == ~U[2000-01-29 00:00:00Z] assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1, day: -28) == - {:ok, ~U[2000-01-01 00:00:00Z]} + ~U[2000-01-01 00:00:00Z] assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1, day: -30) == - {:ok, ~U[1999-12-30 00:00:00Z]} + ~U[1999-12-30 00:00:00Z] assert DateTime.shift(~U[2000-02-29 00:00:00Z], month: -1, day: -29) == - {:ok, ~U[1999-12-31 00:00:00Z]} + ~U[1999-12-31 00:00:00Z] datetime = DateTime.new!(~D[2018-06-01], ~T[03:00:00], "America/Los_Angeles", FakeTimeZoneDatabase) assert DateTime.shift(datetime, [month: -1], FakeTimeZoneDatabase) == - {:ok, - %DateTime{ - calendar: Calendar.ISO, - year: 2018, - month: 5, - day: 1, - hour: 3, - minute: 00, - second: 0, - microsecond: {0, 0}, - time_zone: "America/Los_Angeles", - std_offset: 3600, - utc_offset: -28800, - zone_abbr: "PDT" - }} + %DateTime{ + calendar: Calendar.ISO, + year: 2018, + month: 5, + day: 1, + hour: 3, + minute: 00, + second: 0, + microsecond: {0, 0}, + time_zone: "America/Los_Angeles", + std_offset: 3600, + utc_offset: -28800, + zone_abbr: "PDT" + } datetime = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase) assert DateTime.shift(datetime, [hour: 1], FakeTimeZoneDatabase) == - {:ok, - %DateTime{ - calendar: Calendar.ISO, - year: 2019, - month: 03, - day: 31, - hour: 3, - minute: 00, - second: 0, - microsecond: {0, 0}, - time_zone: "Europe/Copenhagen", - std_offset: 3600, - utc_offset: 3600, - zone_abbr: "CEST" - }} + %DateTime{ + calendar: Calendar.ISO, + year: 2019, + month: 03, + day: 31, + hour: 3, + minute: 00, + second: 0, + microsecond: {0, 0}, + time_zone: "Europe/Copenhagen", + std_offset: 3600, + utc_offset: 3600, + zone_abbr: "CEST" + } assert_raise KeyError, ~s/key :months not found/, fn -> DateTime.shift(~U[2012-01-01 00:00:00Z], months: 12) diff --git a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs index 9e553de54d3..8f7e754575e 100644 --- a/lib/elixir/test/elixir/calendar/naive_datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/naive_datetime_test.exs @@ -394,39 +394,36 @@ defmodule NaiveDateTimeTest do describe "shift/2" do naive_datetime = ~N[2000-01-01 00:00:00] - assert NaiveDateTime.shift(naive_datetime, year: 1) == {:ok, ~N[2001-01-01 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, month: 1) == {:ok, ~N[2000-02-01 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, week: 3) == {:ok, ~N[2000-01-22 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, day: 2) == {:ok, ~N[2000-01-03 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, hour: 6) == {:ok, ~N[2000-01-01 06:00:00]} - assert NaiveDateTime.shift(naive_datetime, minute: 30) == {:ok, ~N[2000-01-01 00:30:00]} - assert NaiveDateTime.shift(naive_datetime, second: 45) == {:ok, ~N[2000-01-01 00:00:45]} - - assert NaiveDateTime.shift(naive_datetime, year: -1) == {:ok, ~N[1999-01-01 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, month: -1) == {:ok, ~N[1999-12-01 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, week: -1) == {:ok, ~N[1999-12-25 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, day: -1) == {:ok, ~N[1999-12-31 00:00:00]} - assert NaiveDateTime.shift(naive_datetime, hour: -12) == {:ok, ~N[1999-12-31 12:00:00]} - assert NaiveDateTime.shift(naive_datetime, minute: -45) == {:ok, ~N[1999-12-31 23:15:00]} - assert NaiveDateTime.shift(naive_datetime, second: -30) == {:ok, ~N[1999-12-31 23:59:30]} + 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}) == - {:ok, ~N[1999-12-31 23:59:59.999500]} + ~N[1999-12-31 23:59:59.999500] assert NaiveDateTime.shift(naive_datetime, microsecond: {500, 6}) == - {:ok, ~N[2000-01-01 00:00:00.000500]} + ~N[2000-01-01 00:00:00.000500] assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 6}) == - {:ok, ~N[2000-01-01 00:00:00.000100]} + ~N[2000-01-01 00:00:00.000100] assert NaiveDateTime.shift(naive_datetime, microsecond: {100, 4}) == - {:ok, ~N[2000-01-01 00:00:00.0001]} - - assert NaiveDateTime.shift(naive_datetime, year: 1, month: 2) == - {:ok, ~N[2001-03-01 00:00:00]} + ~N[2000-01-01 00:00:00.0001] assert NaiveDateTime.shift(naive_datetime, month: 2, day: 3, hour: 6, minute: 15) == - {:ok, ~N[2000-03-04 06:15:00]} + ~N[2000-03-04 06:15:00] assert NaiveDateTime.shift(naive_datetime, year: 1, @@ -437,7 +434,7 @@ defmodule NaiveDateTimeTest do minute: 6, second: 7, microsecond: {8, 6} - ) == {:ok, ~N[2001-03-26 05:06:07.000008]} + ) == ~N[2001-03-26 05:06:07.000008] assert NaiveDateTime.shift(naive_datetime, year: -1, @@ -448,7 +445,7 @@ defmodule NaiveDateTimeTest do minute: -6, second: -7, microsecond: {-8, 6} - ) == {:ok, ~N[1998-10-06 18:53:52.999992]} + ) == ~N[1998-10-06 18:53:52.999992] assert_raise KeyError, ~s/key :months not found/, fn -> NaiveDateTime.shift(naive_datetime, months: 12) diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 2cf4ae4c4b1..7cf9e31d25d 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -105,13 +105,13 @@ defmodule TimeTest do test "shift/2" do time = ~T[00:00:00.0] - assert Time.shift(time, hour: 1) == {:ok, ~T[01:00:00.0]} - assert Time.shift(time, hour: 25) == {:ok, ~T[01:00:00.0]} - assert Time.shift(time, minute: 25) == {:ok, ~T[00:25:00.0]} - assert Time.shift(time, second: 50) == {:ok, ~T[00:00:50.0]} - assert Time.shift(time, microsecond: {150, 6}) == {:ok, ~T[00:00:00.000150]} - assert Time.shift(time, microsecond: {1000, 4}) == {:ok, ~T[00:00:00.0010]} - assert Time.shift(time, hour: 2, minute: 65, second: 5) == {:ok, ~T[03:05:05.0]} + assert Time.shift(time, hour: 1) == ~T[01:00:00.0] + assert Time.shift(time, hour: 25) == ~T[01:00:00.0] + assert Time.shift(time, minute: 25) == ~T[00:25:00.0] + assert Time.shift(time, second: 50) == ~T[00:00:50.0] + assert Time.shift(time, microsecond: {150, 6}) == ~T[00:00:00.000150] + 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, ~s/cannot shift time by date units/, fn -> Time.shift(time, day: 1) From f522fd959fa123fcd7966dd598ce814bcb608e8f Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Thu, 14 Mar 2024 09:58:27 +0100 Subject: [PATCH 74/97] docs --- lib/elixir/lib/calendar/datetime.ex | 9 +++++---- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 736fa28284e..911fd638c7c 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1680,9 +1680,10 @@ defmodule DateTime do Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - This function considers "Daylight Saving Time" and other time gaps across time zones - by converting the given datetime to Etc/UTC before executing the shift. Finally, - the datetime is converted back to its original time zone. + When it comes to non UTC time zones, this function is equivalent to shifting the wall time + (the time a person at said time zone would see on a clock) by the given duration. + After shifting the wall-clock the original offsets are being re-applied and the + datetime is shifted into its original time zone. When used with the default calendar `Calendar.ISO`: @@ -1690,7 +1691,7 @@ defmodule DateTime do - when shifting by 1 year and 2 months the date is actually shifted by 14 months - weeks, days and smaller units are collapsed into seconds and microseconds - Durations are applied in order of the size of the unit: `month > day > second`. + Durations are applied in order of the size of the unit: `month > second > microsecond`. ## Examples diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 0d0946d9853..8625650ca1b 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -583,7 +583,7 @@ defmodule NaiveDateTime do - when shifting by 1 year and 2 months the date is actually shifted by 14 months - weeks, days and smaller units are collapsed into seconds and microseconds - Durations are applied in order of the size of the unit: `month > day > second`. + Durations are applied in order of the size of the unit: `month > second > microsecond`. ## Examples From 6337efaac1b511a6e1634e9252f686e674b5c428 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Thu, 14 Mar 2024 09:58:53 +0100 Subject: [PATCH 75/97] separate DateTime.shift/3 clause for UTC --- lib/elixir/lib/calendar/datetime.ex | 42 +++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 911fd638c7c..c9dd659f677 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1712,6 +1712,48 @@ defmodule DateTime do ) :: t def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) + def shift( + %{calendar: calendar, time_zone: "Etc/UTC"} = datetime, + %Duration{} = duration, + _time_zone_database + ) do + %{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond + } = datetime + + {year, month, day, hour, minute, second, microsecond} = + calendar.shift_naive_datetime( + year, + month, + day, + hour, + minute, + second, + microsecond, + duration + ) + + %DateTime{ + year: year, + month: month, + day: day, + hour: hour, + minute: minute, + second: second, + microsecond: microsecond, + time_zone: "Etc/UTC", + zone_abbr: "UTC", + std_offset: 0, + utc_offset: 0 + } + end + def shift(%{calendar: calendar} = datetime, %Duration{} = duration, time_zone_database) do %{ year: year, From 9ba2a04983ebdeb8520acbadf881556851472981 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 14 Mar 2024 20:06:59 +0100 Subject: [PATCH 76/97] actually test PDT and PST --- .../test/elixir/calendar/datetime_test.exs | 27 +++++++++++++--- lib/elixir/test/elixir/calendar/fakes.exs | 32 +++++++++++++++---- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index ed7f2eb5b0e..c7869f5d6e6 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1095,15 +1095,15 @@ defmodule DateTimeTest do ~U[1999-12-31 00:00:00Z] datetime = - DateTime.new!(~D[2018-06-01], ~T[03:00:00], "America/Los_Angeles", FakeTimeZoneDatabase) + DateTime.new!(~D[2018-11-04], ~T[03:00:00], "America/Los_Angeles", FakeTimeZoneDatabase) assert DateTime.shift(datetime, [month: -1], FakeTimeZoneDatabase) == %DateTime{ calendar: Calendar.ISO, year: 2018, - month: 5, - day: 1, - hour: 3, + month: 10, + day: 4, + hour: 4, minute: 00, second: 0, microsecond: {0, 0}, @@ -1113,6 +1113,25 @@ defmodule DateTimeTest do zone_abbr: "PDT" } + datetime = + DateTime.new!(~D[2018-11-04], ~T[00:00:00], "America/Los_Angeles", FakeTimeZoneDatabase) + + assert DateTime.shift(datetime, [hour: 2], FakeTimeZoneDatabase) == + %DateTime{ + calendar: Calendar.ISO, + year: 2018, + month: 11, + day: 4, + hour: 1, + minute: 00, + second: 0, + microsecond: {0, 0}, + time_zone: "America/Los_Angeles", + std_offset: 0, + utc_offset: -28800, + zone_abbr: "PST" + } + datetime = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase) diff --git a/lib/elixir/test/elixir/calendar/fakes.exs b/lib/elixir/test/elixir/calendar/fakes.exs index 9c611d51f4a..490a25f5949 100644 --- a/lib/elixir/test/elixir/calendar/fakes.exs +++ b/lib/elixir/test/elixir/calendar/fakes.exs @@ -57,8 +57,16 @@ defmodule FakeTimeZoneDatabase do std_offset: 3600, utc_offset: -28800, zone_abbr: "PDT", - from_wall: ~N[2018-03-11 10:00:00], - until_wall: ~N[2018-11-04 09:00:00] + from_wall: ~N[2018-03-11 02:00:00], + until_wall: ~N[2018-11-04 02:00:00] + } + + @time_zone_period_usla_winter_2018_2019 %{ + std_offset: 0, + utc_offset: -28800, + zone_abbr: "PST", + from_wall: ~N[2018-11-04 02:00:00], + until_wall: ~N[2019-03-10 03:00:00] } @spec time_zone_period_from_utc_iso_days(Calendar.iso_days(), Calendar.time_zone()) :: @@ -111,11 +119,17 @@ defmodule FakeTimeZoneDatabase do end defp time_zone_periods_from_utc("America/Los_Angeles", erl_datetime) - when erl_datetime >= {{2018, 3, 11}, {10, 0, 0}} and - erl_datetime < {{2018, 11, 4}, {9, 0, 0}} do + when erl_datetime >= {{2018, 3, 11}, {2, 0, 0}} and + erl_datetime < {{2018, 11, 4}, {2, 0, 0}} do {:ok, @time_zone_period_usla_summer_2018} end + defp time_zone_periods_from_utc("America/Los_Angeles", erl_datetime) + when erl_datetime >= {{2018, 11, 4}, {2, 0, 0}} and + erl_datetime < {{2019, 3, 10}, {3, 0, 0}} do + {:ok, @time_zone_period_usla_winter_2018_2019} + end + defp time_zone_periods_from_utc("Etc/UTC", _erl_datetime) do {:ok, %{ @@ -162,11 +176,17 @@ defmodule FakeTimeZoneDatabase do end defp time_zone_periods_from_wall("America/Los_Angeles", erl_datetime) - when erl_datetime >= {{2018, 3, 11}, {10, 0, 0}} and - erl_datetime < {{2018, 11, 4}, {9, 0, 0}} do + when erl_datetime >= {{2018, 3, 11}, {2, 0, 0}} and + erl_datetime < {{2018, 11, 4}, {2, 0, 0}} do {:ok, @time_zone_period_usla_summer_2018} end + defp time_zone_periods_from_wall("America/Los_Angeles", erl_datetime) + when erl_datetime >= {{2018, 11, 4}, {3, 0, 0}} and + erl_datetime < {{2019, 3, 10}, {3, 0, 0}} do + {:ok, @time_zone_period_usla_winter_2018_2019} + end + defp time_zone_periods_from_wall("Europe/Copenhagen", erl_datetime) when erl_datetime >= {{2015, 3, 29}, {3, 0, 0}} and erl_datetime < {{2015, 10, 25}, {3, 0, 0}} do From 4867289faec105bf865696740b7aad6f70e4f888 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 23 Mar 2024 00:54:32 +0100 Subject: [PATCH 77/97] DateTime.shift/3 docs --- lib/elixir/lib/calendar/datetime.ex | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index c9dd659f677..c035aae462e 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1680,10 +1680,15 @@ defmodule DateTime do Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - When it comes to non UTC time zones, this function is equivalent to shifting the wall time - (the time a person at said time zone would see on a clock) by the given duration. - After shifting the wall-clock the original offsets are being re-applied and the - datetime is shifted into its original time zone. + When dealing with non-UTC time zones, this function shifts the wall time + (the time displayed on a clock in the given time zone) by the specified duration. + After shifting the wall time, the original time zone offsets are re-applied, + adjusting the datetime to its original time zone. + + Shifting datetimes in time zones that observe "Daylight Saving Time" across + summer/winter time will always add/remove one hour from the resulting datetime. + + Consistently applying the offsets, ensures `shift/3` always returns a valid datetime. When used with the default calendar `Calendar.ISO`: From d3cee298f0354dfca08e2513d5a69df16c15c8b2 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 27 Mar 2024 20:41:56 +0100 Subject: [PATCH 78/97] correct "since" annotations --- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/datetime.ex | 2 +- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- lib/elixir/lib/calendar/time.ex | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index bd8300a071e..fc744e2363a 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -789,7 +789,7 @@ defmodule Date do ~D[2016-03-03] """ - @doc since: "1.7.0" + @doc since: "1.17.0" @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: t def shift(%{calendar: calendar} = date, %Duration{} = duration) do %{year: year, month: month, day: day} = date diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index c035aae462e..3891f841e17 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1709,7 +1709,7 @@ defmodule DateTime do iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4}) ~U[2016-01-01 00:05:00.0005Z] """ - @doc since: "1.7.0" + @doc since: "1.17.0" @spec shift( Calendar.datetime(), Duration.t() | [Duration.unit()], diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 8625650ca1b..623eb598d54 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -597,7 +597,7 @@ defmodule NaiveDateTime do ~N[2016-01-31 00:00:00.000100] """ - @doc since: "1.7.0" + @doc since: "1.17.0" @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: t def shift(%{calendar: calendar} = naive_datetime, %Duration{} = duration) do %{ diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 2070fae7a29..792503f142e 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -584,7 +584,7 @@ defmodule Time do ~T[01:16:05] """ - @doc since: "1.7.0" + @doc since: "1.17.0" @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: t def shift(%{calendar: calendar} = time, %Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time From e4d74fa3f4066d3388d24f9ffe2ed70854839c03 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 28 Mar 2024 02:34:47 +0100 Subject: [PATCH 79/97] ensure two-element tuple for Duration.new/1 microsecond --- lib/elixir/lib/calendar/duration.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index a8aab0ca1b8..58c44752068 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -58,7 +58,7 @@ defmodule Duration do nil -> :noop - ms when is_tuple(ms) -> + {ms, precision} when is_integer(ms) and is_integer(precision) -> :noop _ -> From 6641b4d351cc3f061be581d2ccc5043b57053395 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 28 Mar 2024 02:48:54 +0100 Subject: [PATCH 80/97] validate all duration units in Duration.new/1 --- lib/elixir/lib/calendar/duration.ex | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 58c44752068..ddf70d33aba 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -54,7 +54,9 @@ defmodule Duration do """ @spec new([unit]) :: t def new(units) do - case Keyword.get(units, :microsecond) do + {microsecond_unit, duration_units} = Keyword.split(units, [:microsecond]) + + case Keyword.get(microsecond_unit, :microsecond) do nil -> :noop @@ -62,7 +64,13 @@ defmodule Duration do :noop _ -> - raise "microseconds must be a tuple {ms, precision}" + raise ArgumentError, "microsecond unit must be a tuple {ms, precision}" + end + + for {unit, value} <- duration_units do + unless is_integer(value) do + raise ArgumentError, "duration unit must be an integer, got #{unit}: #{value}" + end end struct!(Duration, units) From 8f4fd81cf0887b4442259a03f4e777c474bdf9a9 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 28 Mar 2024 03:53:43 +0100 Subject: [PATCH 81/97] keep validation simple --- lib/elixir/lib/calendar/duration.ex | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index ddf70d33aba..f7434683274 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -67,10 +67,8 @@ defmodule Duration do raise ArgumentError, "microsecond unit must be a tuple {ms, precision}" end - for {unit, value} <- duration_units do - unless is_integer(value) do - raise ArgumentError, "duration unit must be an integer, got #{unit}: #{value}" - end + unless Enum.all?(duration_units, fn {_unit, value} -> is_integer(value) end) do + raise ArgumentError, "duration units must be integers" end struct!(Duration, units) From d2a016bc81d068de9709450e11fa65cca2242f28 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 28 Mar 2024 08:50:16 +0100 Subject: [PATCH 82/97] cleanup Duration.new/1 validation --- lib/elixir/lib/calendar/duration.ex | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index f7434683274..3c4db8eeac3 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -54,24 +54,25 @@ defmodule Duration do """ @spec new([unit]) :: t def new(units) do - {microsecond_unit, duration_units} = Keyword.split(units, [:microsecond]) - - case Keyword.get(microsecond_unit, :microsecond) do - nil -> - :noop + Enum.each(units, &validate_duration_unit!/1) + struct!(Duration, units) + end - {ms, precision} when is_integer(ms) and is_integer(precision) -> - :noop + defp validate_duration_unit!({:microsecond, {ms, precision}}) + when is_integer(ms) and is_integer(precision) do + :ok + end - _ -> - raise ArgumentError, "microsecond unit must be a tuple {ms, precision}" - end + defp validate_duration_unit!({:microsecond, microsecond}) do + raise ArgumentError, "expected a tuple {ms, precision} for microsecond, got #{microsecond}" + end - unless Enum.all?(duration_units, fn {_unit, value} -> is_integer(value) end) do - raise ArgumentError, "duration units must be integers" - end + defp validate_duration_unit!({_unit, value}) when is_integer(value) do + :ok + end - struct!(Duration, units) + defp validate_duration_unit!({unit, value}) do + raise ArgumentError, "expected an integer for #{unit}, got #{value}" end @doc """ From 8799b15c70dc207be0a5bdc654c688531a8efbb9 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 28 Mar 2024 09:05:22 +0100 Subject: [PATCH 83/97] cleanup datetime test --- lib/elixir/test/elixir/calendar/datetime_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/test/elixir/calendar/datetime_test.exs b/lib/elixir/test/elixir/calendar/datetime_test.exs index c7869f5d6e6..eacddc15b32 100644 --- a/lib/elixir/test/elixir/calendar/datetime_test.exs +++ b/lib/elixir/test/elixir/calendar/datetime_test.exs @@ -1104,7 +1104,7 @@ defmodule DateTimeTest do month: 10, day: 4, hour: 4, - minute: 00, + minute: 0, second: 0, microsecond: {0, 0}, time_zone: "America/Los_Angeles", @@ -1123,7 +1123,7 @@ defmodule DateTimeTest do month: 11, day: 4, hour: 1, - minute: 00, + minute: 0, second: 0, microsecond: {0, 0}, time_zone: "America/Los_Angeles", @@ -1142,7 +1142,7 @@ defmodule DateTimeTest do month: 03, day: 31, hour: 3, - minute: 00, + minute: 0, second: 0, microsecond: {0, 0}, time_zone: "Europe/Copenhagen", From 3d148ddcd33b8726366b72f7dac5a76ffebe31e6 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Fri, 29 Mar 2024 19:22:24 +0100 Subject: [PATCH 84/97] leaner docs --- lib/elixir/lib/calendar/date.ex | 6 ++---- lib/elixir/lib/calendar/datetime.ex | 14 +++----------- lib/elixir/lib/calendar/naive_datetime.ex | 4 +--- lib/elixir/lib/calendar/time.ex | 8 +++----- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index fc744e2363a..eff1b3b3d8f 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -765,9 +765,7 @@ defmodule Date do @doc """ Shifts given `date` by `duration` according to its calendar. - Allowed units are: `:year, :month, :week, :day`. - - When used with the default calendar `Calendar.ISO`: + Allowed units are: `:year`, `:month`, `:week`, `:day`. Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months @@ -775,7 +773,7 @@ defmodule Date do Durations are applied in order of the size of the unit: `month > day`. - Raises ArgumentError when called with time scale units. + Raises an `ArgumentError` when called with time scale units. ## Examples diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 3891f841e17..cbab0fa4471 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1678,19 +1678,11 @@ defmodule DateTime do @doc """ Shifts given `datetime` by `duration` according to its calendar. - Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - - When dealing with non-UTC time zones, this function shifts the wall time - (the time displayed on a clock in the given time zone) by the specified duration. - After shifting the wall time, the original time zone offsets are re-applied, - adjusting the datetime to its original time zone. + Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`. Shifting datetimes in time zones that observe "Daylight Saving Time" across - summer/winter time will always add/remove one hour from the resulting datetime. - - Consistently applying the offsets, ensures `shift/3` always returns a valid datetime. - - When used with the default calendar `Calendar.ISO`: + summer/winter time will add/remove one hour from the resulting datetime. + This ensures `shift/3` always returns a valid datetime. Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 623eb598d54..ad74acda60f 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -575,9 +575,7 @@ defmodule NaiveDateTime do @doc """ Shifts given `naive_datetime` by `duration` according to its calendar. - Allowed units are: `:year, :month, :week, :day, :hour, :minute, :second, :microsecond`. - - When used with the default calendar `Calendar.ISO`: + Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`. Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 792503f142e..8661af7b315 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -562,13 +562,11 @@ defmodule Time do @doc """ Shifts given `time` by `duration` according to its calendar. - Available duration units are: `:hour, :minute, :second, :microsecond`. + Available duration units are: `:hour`, `:minute`, `:second`, `:microsecond`. - When used with the default calendar `Calendar.ISO`: + Duration units are collapsed to seconds and microseconds before they are applied. - All duration units are collapsed to seconds and microseconds before they are applied. - - Raises ArgumentError when called with date scale units. + Raises an `ArgumentError` when called with date scale units. ## Examples From b77101665e41b76eb1769ff2dc091ff4a460f49b Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Wed, 3 Apr 2024 09:32:17 +0200 Subject: [PATCH 85/97] duration validation --- lib/elixir/lib/calendar/duration.ex | 7 ++++--- lib/elixir/test/elixir/calendar/duration_test.exs | 13 +++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 3c4db8eeac3..a7393d582ef 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -59,12 +59,13 @@ defmodule Duration do end defp validate_duration_unit!({:microsecond, {ms, precision}}) - when is_integer(ms) and is_integer(precision) do + when is_integer(ms) and precision in 0..6 do :ok end defp validate_duration_unit!({:microsecond, microsecond}) do - raise ArgumentError, "expected a tuple {ms, precision} for microsecond, got #{microsecond}" + raise ArgumentError, + "expected a tuple {ms, precision} for microsecond where precision is an integer from 0 to 6, got #{inspect(microsecond)}" end defp validate_duration_unit!({_unit, value}) when is_integer(value) do @@ -72,7 +73,7 @@ defmodule Duration do end defp validate_duration_unit!({unit, value}) do - raise ArgumentError, "expected an integer for #{unit}, got #{value}" + raise ArgumentError, "expected an integer for #{inspect(unit)}, got #{inspect(value)}" end @doc """ diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 69611ca78a0..c68758723ea 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -6,10 +6,23 @@ defmodule DurationTest do test "new/1" do assert Duration.new(year: 2, month: 1, week: 3) == %Duration{year: 2, month: 1, week: 3} + assert Duration.new(microsecond: {20000, 2}) == %Duration{microsecond: {20000, 2}} assert_raise KeyError, ~s/key :months not found/, fn -> Duration.new(months: 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}/, + 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}/, + fn -> + Duration.new(microsecond: {100, 7}) + end end test "add/2" do From 5e9d6fbe5f25e3c5a263f825aa7266c5190f2742 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 08:54:27 +0200 Subject: [PATCH 86/97] add examples for DateTime.shift/3 --- lib/elixir/lib/calendar/datetime.ex | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index cbab0fa4471..3440b477b8e 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1684,6 +1684,14 @@ defmodule DateTime do summer/winter time will add/remove one hour from the resulting datetime. This ensures `shift/3` always returns a valid datetime. + dt = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen") + DateTime.shift(dt, hour: 1) + #=> #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen> + + dt = DateTime.new!(~D[2018-11-04], ~T[00:00:00], "America/Los_Angeles") + DateTime.shift(dt, hour: 2) + #=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles> + Durations are collapsed before they are applied: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - weeks, days and smaller units are collapsed into seconds and microseconds @@ -1700,6 +1708,13 @@ defmodule DateTime do ~U[2016-01-01 00:25:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4}) ~U[2016-01-01 00:05:00.0005Z] + + # leap years + iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 1) + ~U[2025-02-28 00:00:00Z] + iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 4) + ~U[2028-02-29 00:00:00Z] + """ @doc since: "1.17.0" @spec shift( From d605e0fe08eeac195d19db9b34b72f0cbdbfed89 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 09:08:41 +0200 Subject: [PATCH 87/97] add more examples to Duration --- lib/elixir/lib/calendar/duration.ex | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index a7393d582ef..38d03e797f4 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -11,6 +11,8 @@ defmodule Duration do This ensures compatibility with other calendar types implementing time, such as `Time`, `DateTime`, and `NaiveDateTime`. """ + @moduledoc since: "1.17.0" + defstruct year: 0, month: 0, week: 0, @@ -28,7 +30,7 @@ defmodule Duration do hour: integer, minute: integer, second: integer, - microsecond: {integer, integer} + microsecond: {integer, 0..6} } @type unit :: @@ -39,7 +41,7 @@ defmodule Duration do | {:hour, integer} | {:minute, integer} | {:second, integer} - | {:microsecond, {integer, integer}} + | {:microsecond, {integer, 0..6}} @doc """ Creates a new `Duration` struct from given `units`. @@ -48,6 +50,10 @@ defmodule Duration do ## Examples + iex> Duration.new(year: 1, week: 3, hour: 4, second: 1) + %Duration{year: 1, week: 3, hour: 4, second: 1} + iex> Duration.new(second: 1, microsecond: {1000, 6}) + %Duration{second: 1, microsecond: {1000, 6}} iex> Duration.new(month: 2) %Duration{month: 2} @@ -85,6 +91,8 @@ defmodule Duration do iex> Duration.add(%Duration{week: 2, day: 1}, %Duration{day: 2}) %Duration{week: 2, day: 3} + iex> Duration.add(%Duration{microsecond: {400, 3}}, %Duration{microsecond: {600, 6}}) + %Duration{microsecond: {1000, 6}} """ @spec add(t, t) :: t @@ -113,6 +121,8 @@ defmodule Duration do iex> Duration.subtract(%Duration{week: 2, day: 1}, %Duration{day: 2}) %Duration{week: 2, day: -1} + iex> Duration.subtract(%Duration{microsecond: {400, 6}}, %Duration{microsecond: {600, 3}}) + %Duration{microsecond: {-200, 6}} """ @spec subtract(t, t) :: t @@ -139,6 +149,8 @@ defmodule Duration do iex> Duration.multiply(%Duration{day: 1, minute: 15, second: -10}, 3) %Duration{day: 3, minute: 45, second: -30} + iex> Duration.multiply(%Duration{microsecond: {200, 4}}, 3) + %Duration{microsecond: {600, 4}} """ @spec multiply(t, integer) :: t @@ -158,11 +170,12 @@ defmodule Duration do @doc """ Negates `duration` units. - ## Examples iex> Duration.negate(%Duration{day: 1, minute: 15, second: -10}) %Duration{day: -1, minute: -15, second: 10} + iex> Duration.negate(%Duration{microsecond: {500000, 4}}) + %Duration{microsecond: {-500000, 4}} """ @spec negate(t) :: t From 0f8957ee45e58245ba20bd8f8aef382a3c0e93ce Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 09:12:32 +0200 Subject: [PATCH 88/97] add leap year examples to Date and NaiveDateTime --- lib/elixir/lib/calendar/date.ex | 6 ++++++ lib/elixir/lib/calendar/naive_datetime.ex | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index eff1b3b3d8f..fc6241d523c 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -786,6 +786,12 @@ defmodule Date do iex> Date.shift(~D[2016-01-03], Duration.new(month: 2)) ~D[2016-03-03] + # leap years + iex> Date.shift(~D[2024-02-29], year: 1) + ~D[2025-02-28] + iex> Date.shift(~D[2024-02-29], year: 4) + ~D[2028-02-29] + """ @doc since: "1.17.0" @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: t diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index ad74acda60f..ab65363fccd 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -594,6 +594,12 @@ defmodule NaiveDateTime do iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6}) ~N[2016-01-31 00:00:00.000100] + # leap years + iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 1) + ~N[2025-02-28 00:00:00] + iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 4) + ~N[2028-02-29 00:00:00] + """ @doc since: "1.17.0" @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: t From bbce34528b518d6d289123541212fec095f225df Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 09:13:25 +0200 Subject: [PATCH 89/97] quote KeyError in docs --- lib/elixir/lib/calendar/duration.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 38d03e797f4..4133180f359 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -46,7 +46,7 @@ defmodule Duration do @doc """ Creates a new `Duration` struct from given `units`. - Raises a KeyError when called with invalid units. + Raises a `KeyError` when called with invalid units. ## Examples From 92b5eac8fcf49d4485d21336e6f99a5de9619cc3 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 10:06:46 +0200 Subject: [PATCH 90/97] consistent note on shift in add docs --- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/datetime.ex | 3 ++- lib/elixir/lib/calendar/naive_datetime.ex | 3 ++- lib/elixir/lib/calendar/time.ex | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index fc6241d523c..fd7c2d18d9b 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -690,7 +690,7 @@ defmodule Date do The days are counted as Gregorian days. The date is returned in the same calendar as it was given in. - To shift a date by a `Duration`, use `Date.shift/2`. + To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`. ## Examples diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 3440b477b8e..4631679dac5 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1610,7 +1610,8 @@ defmodule DateTime do iex> result.microsecond {21000, 3} - To shift a datetime by a `Duration`, use `DateTime.shift/3`. + To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`. + """ @doc since: "1.8.0" @spec add( diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index ab65363fccd..bc6e9b2176f 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -448,7 +448,8 @@ defmodule NaiveDateTime do iex> NaiveDateTime.add(dt, 21, :second) ~N[2000-02-29 23:00:28] - To shift a naive datetime by a `Duration`, use `NaiveDateTime.shift/2`. + To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`. + """ @doc since: "1.4.0" @spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 8661af7b315..a7b88259233 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -500,7 +500,8 @@ defmodule Time do iex> result.microsecond {21000, 3} - To shift a time by a `Duration`, use `Time.shift/2`. + To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`. + """ @doc since: "1.6.0" @spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t From 454f054a2966410d6555a176148e7e7f79230edb Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 10:10:49 +0200 Subject: [PATCH 91/97] consolidate shift docs to mention the default calendar --- lib/elixir/lib/calendar/date.ex | 5 ++--- lib/elixir/lib/calendar/datetime.ex | 5 ++--- lib/elixir/lib/calendar/naive_datetime.ex | 5 ++--- lib/elixir/lib/calendar/time.ex | 3 ++- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index fd7c2d18d9b..e7e3d47dc11 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -767,12 +767,11 @@ defmodule Date do Allowed units are: `:year`, `:month`, `:week`, `:day`. - Durations are collapsed before they are applied: + When using the default ISO calendar, durations are collapsed and + applied in the order of months and then days: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - when shifting by 2 weeks and 3 days the date is shifted by 17 days - Durations are applied in order of the size of the unit: `month > day`. - Raises an `ArgumentError` when called with time scale units. ## Examples diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 4631679dac5..75e27d9caf2 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1693,12 +1693,11 @@ defmodule DateTime do DateTime.shift(dt, hour: 2) #=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles> - Durations are collapsed before they are applied: + When using the default ISO calendar, durations are collapsed and + applied in the order of months, then seconds and microseconds: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - weeks, days and smaller units are collapsed into seconds and microseconds - Durations are applied in order of the size of the unit: `month > second > microsecond`. - ## Examples iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2) diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index bc6e9b2176f..45b0f9fee7f 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -578,12 +578,11 @@ defmodule NaiveDateTime do Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`. - Durations are collapsed before they are applied: + When using the default ISO calendar, durations are collapsed and + applied in the order of months, then seconds and microseconds: - when shifting by 1 year and 2 months the date is actually shifted by 14 months - weeks, days and smaller units are collapsed into seconds and microseconds - Durations are applied in order of the size of the unit: `month > second > microsecond`. - ## Examples iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index a7b88259233..0dbedfca0f4 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -565,7 +565,8 @@ defmodule Time do Available duration units are: `:hour`, `:minute`, `:second`, `:microsecond`. - Duration units are collapsed to seconds and microseconds before they are applied. + When using the default ISO calendar, durations are collapsed to seconds and + microseconds before they are applied. Raises an `ArgumentError` when called with date scale units. From 3e02be7713b3514ff59f1d18b5772bf30f2944d9 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 10:16:00 +0200 Subject: [PATCH 92/97] improve docs for DateTime.shift/3 --- lib/elixir/lib/calendar/datetime.ex | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 75e27d9caf2..352a6154c5c 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1681,9 +1681,14 @@ defmodule DateTime do Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`. - Shifting datetimes in time zones that observe "Daylight Saving Time" across - summer/winter time will add/remove one hour from the resulting datetime. - This ensures `shift/3` always returns a valid datetime. + This operation is equivalent to shifting the datetime wall clock (in other words, + the values as we see them printed), then applying the time zone offset before + computing the new time zone. This ensures `shift/3` always returns a valid + datetime. + + On other other hand, time zones that observe "Daylight Saving Time" + or other changes, across summer/winter time will add/remove hours + from the resulting datetime: dt = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen") DateTime.shift(dt, hour: 1) @@ -1693,6 +1698,12 @@ defmodule DateTime do DateTime.shift(dt, hour: 2) #=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles> + In case you don't want these changes to happen automatically or you + want to surface timezone conflicts to the user, you can shift + the datetime as a naive datetime and then use `from_naive/2`: + + dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone) + When using the default ISO calendar, durations are collapsed and applied in the order of months, then seconds and microseconds: - when shifting by 1 year and 2 months the date is actually shifted by 14 months From 7ecbc48f840f11b2669cc3c4edcf614c9d7ae9a0 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 10:20:25 +0200 Subject: [PATCH 93/97] add shift examples with negative duration units --- lib/elixir/lib/calendar/date.ex | 4 ++-- lib/elixir/lib/calendar/datetime.ex | 4 ++-- lib/elixir/lib/calendar/naive_datetime.ex | 2 ++ lib/elixir/lib/calendar/time.ex | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index e7e3d47dc11..ec419d4bb7f 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -778,8 +778,8 @@ defmodule Date do iex> Date.shift(~D[2016-01-03], month: 2) ~D[2016-03-03] - iex> Date.shift(~D[2016-01-30], month: 1) - ~D[2016-02-29] + iex> Date.shift(~D[2016-01-30], month: -1) + ~D[2015-12-30] iex> Date.shift(~D[2016-01-31], year: 4, day: 1) ~D[2020-02-01] iex> Date.shift(~D[2016-01-03], Duration.new(month: 2)) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 352a6154c5c..5f185b92684 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1715,8 +1715,8 @@ defmodule DateTime do ~U[2016-03-01 00:00:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4) ~U[2017-01-29 00:00:00Z] - iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 25) - ~U[2016-01-01 00:25:00Z] + iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: -25) + ~U[2015-12-31 23:35:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4}) ~U[2016-01-01 00:05:00.0005Z] diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 45b0f9fee7f..7b411fa0edc 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -589,6 +589,8 @@ defmodule NaiveDateTime do ~N[2016-02-29 00:00:00] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1) ~N[2020-02-01 00:00:00] + iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: -2, day: 1) + ~N[2014-02-01 00:00:00] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45) ~N[2016-01-31 00:00:45] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6}) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 0dbedfca0f4..b9af0b8a934 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -574,8 +574,8 @@ defmodule Time do iex> Time.shift(~T[01:00:15], hour: 12) ~T[13:00:15] - iex> Time.shift(~T[01:15:00], hour: 6, minute: 15) - ~T[07:30:00] + iex> Time.shift(~T[01:35:00], hour: 6, minute: -15) + ~T[07:20:00] iex> Time.shift(~T[01:15:00], second: 125) ~T[01:17:05] iex> Time.shift(~T[01:00:15], microsecond: {100, 6}) From 8f9c1f4ea02429eb7adcc0f105303198e4fbd89b Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 10:22:46 +0200 Subject: [PATCH 94/97] rename Duration.unit type to Duration.unit_pair --- lib/elixir/lib/calendar/date.ex | 2 +- lib/elixir/lib/calendar/datetime.ex | 2 +- lib/elixir/lib/calendar/duration.ex | 4 ++-- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- lib/elixir/lib/calendar/time.ex | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index ec419d4bb7f..185e6b315a8 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -793,7 +793,7 @@ defmodule Date do """ @doc since: "1.17.0" - @spec shift(Calendar.date(), Duration.t() | [Duration.unit()]) :: t + @spec shift(Calendar.date(), Duration.t() | [Duration.unit_pair()]) :: t def shift(%{calendar: calendar} = date, %Duration{} = duration) do %{year: year, month: month, day: day} = date {year, month, day} = calendar.shift_date(year, month, day, duration) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 5f185b92684..729d257e4c3 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1730,7 +1730,7 @@ defmodule DateTime do @doc since: "1.17.0" @spec shift( Calendar.datetime(), - Duration.t() | [Duration.unit()], + Duration.t() | [Duration.unit_pair()], Calendar.time_zone_database() ) :: t def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database()) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 4133180f359..16fa32a3e08 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -33,7 +33,7 @@ defmodule Duration do microsecond: {integer, 0..6} } - @type unit :: + @type unit_pair :: {:year, integer} | {:month, integer} | {:week, integer} @@ -58,7 +58,7 @@ defmodule Duration do %Duration{month: 2} """ - @spec new([unit]) :: t + @spec new([unit_pair]) :: t def new(units) do Enum.each(units, &validate_duration_unit!/1) struct!(Duration, units) diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 7b411fa0edc..4d615d4237a 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -604,7 +604,7 @@ defmodule NaiveDateTime do """ @doc since: "1.17.0" - @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit()]) :: t + @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit_pair()]) :: t def shift(%{calendar: calendar} = naive_datetime, %Duration{} = duration) do %{ year: year, diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index b9af0b8a934..52ec48cef95 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -585,7 +585,7 @@ defmodule Time do """ @doc since: "1.17.0" - @spec shift(Calendar.time(), Duration.t() | [Duration.unit()]) :: t + @spec shift(Calendar.time(), Duration.t() | [Duration.unit_pair()]) :: t def shift(%{calendar: calendar} = time, %Duration{} = duration) do %{hour: hour, minute: minute, second: second, microsecond: microsecond} = time From 14b1314dc62476da1fa24038682ca535b1d36b51 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 10:31:23 +0200 Subject: [PATCH 95/97] document rounding behaviour when shifting by month --- lib/elixir/lib/calendar/date.ex | 6 ++++++ lib/elixir/lib/calendar/datetime.ex | 6 ++++++ lib/elixir/lib/calendar/naive_datetime.ex | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 185e6b315a8..10161027078 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -772,6 +772,8 @@ defmodule Date do - when shifting by 1 year and 2 months the date is actually shifted by 14 months - when shifting by 2 weeks and 3 days the date is shifted by 17 days + When shifting by month, days are rounded down to the nearest valid date. + Raises an `ArgumentError` when called with time scale units. ## Examples @@ -791,6 +793,10 @@ defmodule Date do iex> Date.shift(~D[2024-02-29], year: 4) ~D[2028-02-29] + # rounding down + iex> Date.shift(~D[2015-01-31], month: 1) + ~D[2015-02-28] + """ @doc since: "1.17.0" @spec shift(Calendar.date(), Duration.t() | [Duration.unit_pair()]) :: t diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 729d257e4c3..a06829ded7a 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1709,6 +1709,8 @@ defmodule DateTime do - when shifting by 1 year and 2 months the date is actually shifted by 14 months - weeks, days and smaller units are collapsed into seconds and microseconds + When shifting by month, days are rounded down to the nearest valid date. + ## Examples iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2) @@ -1726,6 +1728,10 @@ defmodule DateTime do iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 4) ~U[2028-02-29 00:00:00Z] + # rounding down + iex> DateTime.shift(~U[2015-01-31 00:00:00Z], month: 1) + ~U[2015-02-28 00:00:00Z] + """ @doc since: "1.17.0" @spec shift( diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 4d615d4237a..0aa3279ac93 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -583,6 +583,8 @@ defmodule NaiveDateTime do - when shifting by 1 year and 2 months the date is actually shifted by 14 months - weeks, days and smaller units are collapsed into seconds and microseconds + When shifting by month, days are rounded down to the nearest valid date. + ## Examples iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) @@ -602,6 +604,10 @@ defmodule NaiveDateTime do iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 4) ~N[2028-02-29 00:00:00] + # rounding down + iex> NaiveDateTime.shift(~N[2015-01-31 00:00:00], month: 1) + ~N[2015-02-28 00:00:00] + """ @doc since: "1.17.0" @spec shift(Calendar.naive_datetime(), Duration.t() | [Duration.unit_pair()]) :: t From 369f9536d7215d91d581d8fccef9efc94a767e53 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 11:19:26 +0200 Subject: [PATCH 96/97] rename Duration.new/1 to Duration.new!/1 --- lib/elixir/lib/calendar/date.ex | 4 +- lib/elixir/lib/calendar/datetime.ex | 2 +- lib/elixir/lib/calendar/duration.ex | 24 ++-- lib/elixir/lib/calendar/iso.ex | 18 +-- lib/elixir/lib/calendar/naive_datetime.ex | 2 +- lib/elixir/lib/calendar/time.ex | 4 +- .../test/elixir/calendar/duration_test.exs | 12 +- lib/elixir/test/elixir/calendar/iso_test.exs | 106 +++++++++--------- 8 files changed, 87 insertions(+), 85 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 10161027078..bcff3c379f0 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -784,7 +784,7 @@ defmodule Date do ~D[2015-12-30] iex> Date.shift(~D[2016-01-31], year: 4, day: 1) ~D[2020-02-01] - iex> Date.shift(~D[2016-01-03], Duration.new(month: 2)) + iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2)) ~D[2016-03-03] # leap years @@ -807,7 +807,7 @@ defmodule Date do end def shift(date, duration) do - shift(date, Duration.new(duration)) + shift(date, Duration.new!(duration)) end @doc false diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index a06829ded7a..ce423dddc74 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1826,7 +1826,7 @@ defmodule DateTime do end def shift(datetime, duration, time_zone_database) do - shift(datetime, Duration.new(duration), time_zone_database) + shift(datetime, Duration.new!(duration), time_zone_database) end @doc """ diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 16fa32a3e08..99994e6bf02 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -7,8 +7,8 @@ defmodule Duration do Date and time scale units are represented as integers, allowing for both positive and negative values. - Microseconds are represented using a tuple `{microsecond, precision}`. - This ensures compatibility with other calendar types implementing time, such as `Time`, `DateTime`, and `NaiveDateTime`. + Microseconds are represented using a tuple `{microsecond, precision}`. This ensures compatibility with + other calendar types implementing time, such as `Time`, `DateTime`, and `NaiveDateTime`. """ @moduledoc since: "1.17.0" @@ -44,24 +44,26 @@ defmodule Duration do | {:microsecond, {integer, 0..6}} @doc """ - Creates a new `Duration` struct from given `units`. + Creates a new `Duration` struct from given `unit_pairs`. - Raises a `KeyError` when called with invalid units. + Raises a `KeyError` when called with invalid unit keys. + + Raises an `ArgumentError` when called with invalid unit values. ## Examples - iex> Duration.new(year: 1, week: 3, hour: 4, second: 1) + iex> Duration.new!(year: 1, week: 3, hour: 4, second: 1) %Duration{year: 1, week: 3, hour: 4, second: 1} - iex> Duration.new(second: 1, microsecond: {1000, 6}) + iex> Duration.new!(second: 1, microsecond: {1000, 6}) %Duration{second: 1, microsecond: {1000, 6}} - iex> Duration.new(month: 2) + iex> Duration.new!(month: 2) %Duration{month: 2} """ - @spec new([unit_pair]) :: t - def new(units) do - Enum.each(units, &validate_duration_unit!/1) - struct!(Duration, units) + @spec new!([unit_pair]) :: t + def new!(unit_pairs) do + Enum.each(unit_pairs, &validate_duration_unit!/1) + struct!(Duration, unit_pairs) end defp validate_duration_unit!({:microsecond, {ms, precision}}) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index b593527abf8..564a203120e 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -1460,13 +1460,13 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new(month: 2)) + iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new!(month: 2)) {2016, 3, 3} - iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new(month: 1)) + iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new!(month: 1)) {2016, 3, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new(month: 1)) + iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(month: 1)) {2016, 2, 29} - iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new(year: 4, day: 1)) + iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(year: 4, day: 1)) {2020, 2, 1} """ @impl true @@ -1491,11 +1491,11 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(hour: 1)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 1)) {2016, 1, 3, 1, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(hour: 30)) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 30)) {2016, 1, 4, 6, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new(microsecond: {100, 6})) + iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6})) {2016, 1, 3, 0, 0, 0, {100, 6}} """ @impl true @@ -1530,9 +1530,9 @@ defmodule Calendar.ISO do ## Examples - iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(hour: 2)) + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(hour: 2)) {15, 0, 0, {0, 0}} - iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new(microsecond: {100, 6})) + iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6})) {13, 0, 0, {100, 6}} """ @impl true diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index 0aa3279ac93..44a250b1542 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -647,7 +647,7 @@ defmodule NaiveDateTime do end def shift(naive_datetime, duration) do - shift(naive_datetime, Duration.new(duration)) + shift(naive_datetime, Duration.new!(duration)) end @doc """ diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 52ec48cef95..981b8a96f01 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -580,7 +580,7 @@ defmodule Time do ~T[01:17:05] iex> Time.shift(~T[01:00:15], microsecond: {100, 6}) ~T[01:00:15.000100] - iex> Time.shift(~T[01:15:00], Duration.new(second: 65)) + iex> Time.shift(~T[01:15:00], Duration.new!(second: 65)) ~T[01:16:05] """ @@ -602,7 +602,7 @@ defmodule Time do end def shift(time, duration) do - shift(time, Duration.new(duration)) + shift(time, Duration.new!(duration)) end @doc """ diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index c68758723ea..1ecbbff9dc2 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -4,24 +4,24 @@ defmodule DurationTest do use ExUnit.Case, async: true doctest Duration - test "new/1" do - assert Duration.new(year: 2, month: 1, week: 3) == %Duration{year: 2, month: 1, week: 3} - assert Duration.new(microsecond: {20000, 2}) == %Duration{microsecond: {20000, 2}} + test "new!/1" do + assert Duration.new!(year: 2, month: 1, week: 3) == %Duration{year: 2, month: 1, week: 3} + assert Duration.new!(microsecond: {20000, 2}) == %Duration{microsecond: {20000, 2}} assert_raise KeyError, ~s/key :months not found/, fn -> - Duration.new(months: 1) + Duration.new!(months: 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}/, fn -> - Duration.new(microsecond: {1, 2, 3}) + 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}/, fn -> - Duration.new(microsecond: {100, 7}) + Duration.new!(microsecond: {100, 7}) end end diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index e8d5cde02c4..25bc13fea8d 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -430,36 +430,36 @@ defmodule Calendar.ISOTest do end test "shift_date/2" do - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new([])) == {2024, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(year: 1)) == {2025, 3, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(month: 2)) == {2024, 5, 2} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(week: 3)) == {2024, 3, 23} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(day: 5)) == {2024, 3, 7} - - assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(month: 1)) == {0, 2, 1} - assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(year: 1)) == {1, 1, 1} - assert Calendar.ISO.shift_date(0, 1, 1, Duration.new(year: -2, month: 2)) == {-2, 3, 1} - assert Calendar.ISO.shift_date(-4, 1, 1, Duration.new(year: -1)) == {-5, 1, 1} - - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(year: 1, month: 2, week: 3, day: 5)) == + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!([])) == {2024, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: 1)) == {2025, 3, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(month: 2)) == {2024, 5, 2} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(week: 3)) == {2024, 3, 23} + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(day: 5)) == {2024, 3, 7} + + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(month: 1)) == {0, 2, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(year: 1)) == {1, 1, 1} + assert Calendar.ISO.shift_date(0, 1, 1, Duration.new!(year: -2, month: 2)) == {-2, 3, 1} + assert Calendar.ISO.shift_date(-4, 1, 1, Duration.new!(year: -1)) == {-5, 1, 1} + + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: 1, month: 2, week: 3, day: 5)) == {2025, 5, 28} - assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new(year: -1, month: -2, week: -3)) == + assert Calendar.ISO.shift_date(2024, 3, 2, Duration.new!(year: -1, month: -2, week: -3)) == {2022, 12, 12} - assert Calendar.ISO.shift_date(2020, 2, 28, Duration.new(day: 1)) == {2020, 2, 29} - assert Calendar.ISO.shift_date(2020, 2, 29, Duration.new(year: 1)) == {2021, 2, 28} - assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new(month: -1)) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new(month: -2)) == {2024, 1, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 1)) == {2024, 2, 29} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 2)) == {2024, 3, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 3)) == {2024, 4, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 4)) == {2024, 5, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 5)) == {2024, 6, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 6)) == {2024, 7, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 7)) == {2024, 8, 31} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 8)) == {2024, 9, 30} - assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new(month: 9)) == {2024, 10, 31} + assert Calendar.ISO.shift_date(2020, 2, 28, Duration.new!(day: 1)) == {2020, 2, 29} + assert Calendar.ISO.shift_date(2020, 2, 29, Duration.new!(year: 1)) == {2021, 2, 28} + assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new!(month: -1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 3, 31, Duration.new!(month: -2)) == {2024, 1, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 1)) == {2024, 2, 29} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 2)) == {2024, 3, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 3)) == {2024, 4, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 4)) == {2024, 5, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 5)) == {2024, 6, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 6)) == {2024, 7, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 7)) == {2024, 8, 31} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 8)) == {2024, 9, 30} + assert Calendar.ISO.shift_date(2024, 1, 31, Duration.new!(month: 9)) == {2024, 10, 31} end test "shift_naive_datetime/2" do @@ -471,7 +471,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new([]) + Duration.new!([]) ) == {2024, 3, 2, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -482,7 +482,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(year: 1) + Duration.new!(year: 1) ) == {2001, 1, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -493,7 +493,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: 1) + Duration.new!(month: 1) ) == {2000, 2, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -504,7 +504,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: 1, day: 28) + Duration.new!(month: 1, day: 28) ) == {2000, 2, 29, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -515,7 +515,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: 1, day: 30) + Duration.new!(month: 1, day: 30) ) == {2000, 3, 2, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -526,7 +526,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: 2, day: 29) + Duration.new!(month: 2, day: 29) ) == {2000, 3, 30, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -537,7 +537,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(year: -1) + Duration.new!(year: -1) ) == {1999, 2, 28, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -548,7 +548,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: -1) + Duration.new!(month: -1) ) == {2000, 1, 29, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -559,7 +559,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: -1, day: -28) + Duration.new!(month: -1, day: -28) ) == {2000, 1, 1, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -570,7 +570,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: -1, day: -30) + Duration.new!(month: -1, day: -30) ) == {1999, 12, 30, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -581,7 +581,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(month: -1, day: -29) + Duration.new!(month: -1, day: -29) ) == {1999, 12, 31, 0, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -592,7 +592,7 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(hour: 12) + Duration.new!(hour: 12) ) == {2000, 1, 1, 12, 0, 0, {0, 0}} assert Calendar.ISO.shift_naive_datetime( @@ -603,45 +603,45 @@ defmodule Calendar.ISOTest do 0, 0, {0, 0}, - Duration.new(minute: -65) + Duration.new!(minute: -65) ) == {1999, 12, 31, 22, 55, 0, {0, 0}} 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}} + 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)) == + 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)) == + 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)) == + 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)) == + 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})) == + 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})) == + 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})) == + 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})) == + 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})) == + 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)) == + 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)) == + assert Calendar.ISO.shift_time(0, 0, 0, {3500, 6}, Duration.new!(hour: 4)) == {4, 0, 0, {3500, 6}} assert Calendar.ISO.shift_time( @@ -649,7 +649,7 @@ defmodule Calendar.ISOTest do 59, 59, {999_900, 6}, - Duration.new(hour: 4, microsecond: {100, 6}) + Duration.new!(hour: 4, microsecond: {100, 6}) ) == {4, 0, 0, {0, 6}} end end From f69baf1277e4587d491224dde8363d8c950f766a Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Thu, 4 Apr 2024 11:21:28 +0200 Subject: [PATCH 97/97] inspect only non-default values on %Duration{} --- lib/elixir/lib/calendar/duration.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 99994e6bf02..a49206ca408 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -13,6 +13,7 @@ defmodule Duration do @moduledoc since: "1.17.0" + @derive {Inspect, optional: [:year, :month, :week, :day, :hour, :minute, :second, :microsecond]} defstruct year: 0, month: 0, week: 0,