From 92abf16b7336eed0b3998acebfd115c4f035f2ee Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Fri, 22 Mar 2024 21:24:34 +0100 Subject: [PATCH 01/20] add Duration parser and sigil ~P --- lib/elixir/lib/calendar/date.ex | 2 + lib/elixir/lib/calendar/datetime.ex | 2 + lib/elixir/lib/calendar/duration.ex | 97 +++++++++++++++++++ lib/elixir/lib/calendar/naive_datetime.ex | 2 + lib/elixir/lib/calendar/time.ex | 2 + lib/elixir/lib/kernel.ex | 17 ++++ .../test/elixir/calendar/duration_test.exs | 86 ++++++++++++++++ 7 files changed, 208 insertions(+) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 8e6079ca50b..5500170d433 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -773,6 +773,8 @@ defmodule Date do ## Examples + iex> Date.shift(~D[2016-01-31], ~P[P4Y1D]) + ~D[2020-02-01] iex> Date.shift(~D[2016-01-03], month: 2) ~D[2016-03-03] iex> Date.shift(~D[2016-01-30], month: -1) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 3053cf5d252..5d455b2f8ca 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1711,6 +1711,8 @@ defmodule DateTime do ## Examples + iex> DateTime.shift(~U[2016-01-01 00:00:00Z], ~P[P1Y4W]) + ~U[2017-01-29 00:00:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2) ~U[2016-03-01 00:00:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index ddc8d8d09bd..e744847798f 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -197,4 +197,101 @@ defmodule Duration do microsecond: {-ms, p} } end + + @doc """ + Parses an ISO 8601-2 formatted duration string to a `Duration` struct. + + ## Examples + + iex> Duration.parse("P1Y2M3DT4H5M6S") + {:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}} + + iex> Duration.parse("PT10H30M") + {:ok, %Duration{hour: 10, minute: 30, second: 0}} + + """ + @spec parse(String.t()) :: {:ok, t} | {:error, String.t()} + def parse("P" <> duration_string) do + parse(duration_string, [], "", false) + end + + def parse(_) do + {:error, "invalid duration string"} + end + + @doc """ + Same as parse/1 but raises an ArgumentError. + + ## Examples + + iex> Duration.parse!("P1Y2M3DT4H5M6S") + %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} + + iex> Duration.parse!("PT10H30M") + %Duration{hour: 10, minute: 30, second: 0} + + """ + @spec parse!(String.t()) :: t + def parse!(duration_string) do + case parse(duration_string) do + {:ok, duration} -> + duration + + {:error, reason} -> + raise ArgumentError, "failed to parse duration. reason: #{inspect(reason)}" + end + end + + defp parse(<<>>, duration, "", _), do: {:ok, new(duration)} + + defp parse(<>, duration, buffer, is_time) when c in ?0..?9 do + parse(rest, duration, <>, is_time) + end + + defp parse(<<"Y", rest::binary>>, duration, buffer, false) do + parse(:year, rest, duration, buffer, false) + end + + defp parse(<<"M", rest::binary>>, duration, buffer, false) do + parse(:month, rest, duration, buffer, false) + end + + defp parse(<<"W", rest::binary>>, duration, buffer, false) do + parse(:week, rest, duration, buffer, false) + end + + defp parse(<<"D", rest::binary>>, duration, buffer, false) do + parse(:day, rest, duration, buffer, false) + end + + defp parse(<<"T", _::binary>>, _duration, _, true) do + {:error, "time delimiter was already provided"} + end + + defp parse(<<"T", rest::binary>>, duration, _buffer, false) do + parse(rest, duration, "", true) + end + + defp parse(<<"H", rest::binary>>, duration, buffer, true) do + parse(:hour, rest, duration, buffer, true) + end + + defp parse(<<"M", rest::binary>>, duration, buffer, true) do + parse(:minute, rest, duration, buffer, true) + end + + defp parse(<<"S", rest::binary>>, duration, buffer, true) do + parse(:second, rest, duration, buffer, true) + end + + defp parse(<>, _, _, _) do + {:error, "unexpected character: #{<>}"} + end + + defp parse(unit, string, duration, buffer, is_time) do + case Keyword.get(duration, unit) do + nil -> parse(string, Keyword.put(duration, unit, String.to_integer(buffer)), "", is_time) + _ -> {:error, "#{unit} was already provided"} + end + end end diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index b9becf88fe9..af810c31ef0 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -585,6 +585,8 @@ defmodule NaiveDateTime do ## Examples + iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], ~P[P4Y1D]) + ~N[2020-02-01 00:00:00] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) ~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 f51e600c5f2..8e963ccd5a9 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -558,6 +558,8 @@ defmodule Time do ## Examples + iex> Time.shift(~T[01:15:00], ~P[PT6H15M]) + ~T[07:30:00] iex> Time.shift(~T[01:00:15], hour: 12) ~T[13:00:15] iex> Time.shift(~T[01:35:00], hour: 6, minute: -15) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 26a41c84d29..e0ac21f678b 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6655,6 +6655,23 @@ defmodule Kernel do end end + @doc ~S""" + Handles the sigil `~P` to create a `Duration`. + + ## Examples + + iex> ~P[P1Y2M3DT4H5M6S] + %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} + + """ + defmacro sigil_P(duration_string, modifiers) + + defmacro sigil_P({:<<>>, _, [duration_string]}, []) do + quote do + Duration.parse!(unquote(duration_string)) + end + end + @doc ~S""" Handles the sigil `~w` for list of words. diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 12a2b268d8b..1a2a01a1e2e 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -217,4 +217,90 @@ defmodule DurationTest do microsecond: {0, 0} } end + + test "parse/1" do + assert Duration.parse("P1Y2M3DT4H5M6S") == + {:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}} + + assert Duration.parse("P3WT5H3M") == {:ok, %Duration{week: 3, hour: 5, minute: 3}} + assert Duration.parse("PT5H3M") == {:ok, %Duration{hour: 5, minute: 3}} + assert Duration.parse("P1Y2M3D") == {:ok, %Duration{year: 1, month: 2, day: 3}} + assert Duration.parse("PT4H5M6S") == {:ok, %Duration{hour: 4, minute: 5, second: 6}} + assert Duration.parse("P1Y2M") == {:ok, %Duration{year: 1, month: 2}} + assert Duration.parse("P3D") == {:ok, %Duration{day: 3}} + assert Duration.parse("PT4H5M") == {:ok, %Duration{hour: 4, minute: 5}} + assert Duration.parse("PT6S") == {:ok, %Duration{second: 6}} + assert Duration.parse("P5H3HT4M") == {:error, "unexpected character: H"} + assert Duration.parse("P4Y2W3Y") == {:error, "year was already provided"} + assert Duration.parse("invalid") == {:error, "invalid duration string"} + end + + test "parse!/1" do + assert Duration.parse!("P1Y2M3DT4H5M6S") == %Duration{ + year: 1, + month: 2, + day: 3, + hour: 4, + minute: 5, + second: 6 + } + + assert Duration.parse!("P3WT5H3M") == %Duration{week: 3, hour: 5, minute: 3} + assert Duration.parse!("PT5H3M") == %Duration{hour: 5, minute: 3} + assert Duration.parse!("P1Y2M3D") == %Duration{year: 1, month: 2, day: 3} + assert Duration.parse!("PT4H5M6S") == %Duration{hour: 4, minute: 5, second: 6} + assert Duration.parse!("P1Y2M") == %Duration{year: 1, month: 2} + assert Duration.parse!("P3D") == %Duration{day: 3} + assert Duration.parse!("PT4H5M") == %Duration{hour: 4, minute: 5} + assert Duration.parse!("PT6S") == %Duration{second: 6} + + assert_raise ArgumentError, + ~s/failed to parse duration. reason: "unexpected character: H"/, + fn -> + Duration.parse!("P5H3HT4M") + end + + assert_raise ArgumentError, + ~s/failed to parse duration. reason: "year was already provided"/, + fn -> + Duration.parse!("P4Y2W3Y") + end + + assert_raise ArgumentError, + ~s/failed to parse duration. reason: "invalid duration string"/, + fn -> + Duration.parse!("invalid") + end + end + + test "sigil_P" do + assert ~P[P1Y2M3DT4H5M6S] == %Duration{ + year: 1, + month: 2, + day: 3, + hour: 4, + minute: 5, + second: 6 + } + + assert ~P[PT5H3M] == %Duration{hour: 5, minute: 3} + assert ~P[P1Y2M3D] == %Duration{year: 1, month: 2, day: 3} + assert ~P[PT4H5M6S] == %Duration{hour: 4, minute: 5, second: 6} + assert ~P[P1Y2M] == %Duration{year: 1, month: 2} + assert ~P[P3D] == %Duration{day: 3} + assert ~P[PT4H5M] == %Duration{hour: 4, minute: 5} + assert ~P[PT6S] == %Duration{second: 6} + + assert_raise ArgumentError, + ~s/failed to parse duration. reason: "unexpected character: H"/, + fn -> + Code.eval_string("~P[P5H3HT4M]") + end + + assert_raise ArgumentError, + ~s/failed to parse duration. reason: "invalid duration string"/, + fn -> + Code.eval_string("~P[invalid]") + end + end end From ed46eea17f7b529723d5964bc79d973184dc2537 Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Sun, 24 Mar 2024 20:19:34 +0100 Subject: [PATCH 02/20] support fractional seconds --- lib/elixir/lib/calendar/duration.ex | 42 ++++++++++++++++--- .../test/elixir/calendar/duration_test.exs | 8 ++++ 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index e744847798f..ba1333a3908 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -212,7 +212,7 @@ defmodule Duration do """ @spec parse(String.t()) :: {:ok, t} | {:error, String.t()} def parse("P" <> duration_string) do - parse(duration_string, [], "", false) + parse(duration_string, %{}, "", false) end def parse(_) do @@ -242,9 +242,9 @@ defmodule Duration do end end - defp parse(<<>>, duration, "", _), do: {:ok, new(duration)} + defp parse(<<>>, duration, "", _), do: {:ok, new(Enum.into(duration, []))} - defp parse(<>, duration, buffer, is_time) when c in ?0..?9 do + defp parse(<>, duration, buffer, is_time) when c in ?0..?9 or c == ?. do parse(rest, duration, <>, is_time) end @@ -288,10 +288,40 @@ defmodule Duration do {:error, "unexpected character: #{<>}"} end + defp parse(unit, _string, duration, _buffer, _is_time) when is_map_key(duration, unit) do + {:error, "#{unit} was already provided"} + end + + defp parse(:second, string, duration, buffer, is_time) do + case Float.parse(buffer) do + {float_second, ""} -> + second = trunc(float_second) + + {microsecond, precision} = + case trunc((float_second - second) * 1_000_000) do + 0 -> {0, 0} + microsecond -> {microsecond, 6} + end + + duration = + duration + |> Map.put(:second, second) + |> Map.put(:microsecond, {microsecond, precision}) + + parse(string, duration, "", is_time) + + _ -> + {:error, "invalid value for second: #{buffer}"} + end + end + defp parse(unit, string, duration, buffer, is_time) do - case Keyword.get(duration, unit) do - nil -> parse(string, Keyword.put(duration, unit, String.to_integer(buffer)), "", is_time) - _ -> {:error, "#{unit} was already provided"} + case Integer.parse(buffer) do + {duration_value, ""} -> + parse(string, Map.put(duration, unit, duration_value), "", is_time) + + _ -> + {:error, "invalid value for #{unit}: #{buffer}"} end end end diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 1a2a01a1e2e..750345d5199 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -253,6 +253,8 @@ defmodule DurationTest do assert Duration.parse!("P3D") == %Duration{day: 3} assert Duration.parse!("PT4H5M") == %Duration{hour: 4, minute: 5} assert Duration.parse!("PT6S") == %Duration{second: 6} + assert Duration.parse!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 6}} + assert Duration.parse!("PT1.12345678S") == %Duration{second: 1, microsecond: {123_456, 6}} assert_raise ArgumentError, ~s/failed to parse duration. reason: "unexpected character: H"/, @@ -271,6 +273,12 @@ defmodule DurationTest do fn -> Duration.parse!("invalid") end + + assert_raise ArgumentError, + ~s/failed to parse duration. reason: "invalid value for year: 4.5"/, + fn -> + Duration.parse!("P4.5YT6S") + end end test "sigil_P" do From 187b4bfda4f68b4d8d1ecd5cfbefe4f10be67254 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Tue, 2 Apr 2024 20:40:21 +0200 Subject: [PATCH 03/20] sigil docs --- lib/elixir/lib/kernel.ex | 8 ++++++++ lib/elixir/pages/getting-started/sigils.md | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index e0ac21f678b..f3aa510a5d0 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6662,6 +6662,14 @@ defmodule Kernel do iex> ~P[P1Y2M3DT4H5M6S] %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} + iex> ~P[PT4H1.5S] + %Duration{hour: 4, second: 1, microsecond: {500000, 6}} + iex> ~P[P-1Y3WT4H] + %Duration{year: -1, week: 3, hour: 4} + iex> ~P[-P1Y3WT4H] + %Duration{year: -1, week: -3, hour: -4} + iex> ~P[-P1Y-3WT4H] + %Duration{year: -1, week: 3, hour: -4} """ defmacro sigil_P(duration_string, modifiers) diff --git a/lib/elixir/pages/getting-started/sigils.md b/lib/elixir/pages/getting-started/sigils.md index 9da5509a42d..3b13317e90f 100644 --- a/lib/elixir/pages/getting-started/sigils.md +++ b/lib/elixir/pages/getting-started/sigils.md @@ -205,6 +205,17 @@ iex> time_zone "Etc/UTC" ``` +### Duration + +A [%Duration{}](`Duration`) struct represents a collection of time scale units, The `~P` sigil allows developers to create Durations from an ISO 8601-2 formatted duration string: + +```elixir +iex> ~P[P1Y2M3DT4H5M6S] +%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} +iex> ~P[-P1Y-3WT4H-1.5S] +%Duration{year: -1, week: 3, hour: -4, second: 1, microsecond: {500000, 6}} +``` + ## Custom sigils As hinted at the beginning of this chapter, sigils in Elixir are extensible. In fact, using the sigil `~r/foo/i` is equivalent to calling `sigil_r` with a binary and a char list as the argument: From 1072450cfc280da8f52bd95358c300e58603288b Mon Sep 17 00:00:00 2001 From: Theo Fiedler Date: Sun, 24 Mar 2024 20:31:34 +0100 Subject: [PATCH 04/20] support sign prefixes --- lib/elixir/lib/calendar/duration.ex | 25 ++++++++++++++----- lib/elixir/pages/getting-started/sigils.md | 3 ++- .../test/elixir/calendar/duration_test.exs | 4 +++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index ba1333a3908..cca7414710c 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -205,9 +205,14 @@ defmodule Duration do iex> Duration.parse("P1Y2M3DT4H5M6S") {:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}} - iex> Duration.parse("PT10H30M") {:ok, %Duration{hour: 10, minute: 30, second: 0}} + iex> Duration.parse("P3Y-2MT3H") + {:ok, %Duration{year: 3, month: -2, hour: 3}} + iex> Duration.parse("-P3Y2MT3H") + {:ok, %Duration{year: -3, month: -2, hour: -3}} + iex> Duration.parse("-P3Y-2MT3H") + {:ok, %Duration{year: -3, month: 2, hour: -3}} """ @spec parse(String.t()) :: {:ok, t} | {:error, String.t()} @@ -215,6 +220,16 @@ defmodule Duration do parse(duration_string, %{}, "", false) end + def parse("-P" <> duration_string) do + case parse(duration_string, %{}, "", false) do + {:ok, duration} -> + {:ok, negate(duration)} + + error -> + error + end + end + def parse(_) do {:error, "invalid duration string"} end @@ -227,9 +242,6 @@ defmodule Duration do iex> Duration.parse!("P1Y2M3DT4H5M6S") %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} - iex> Duration.parse!("PT10H30M") - %Duration{hour: 10, minute: 30, second: 0} - """ @spec parse!(String.t()) :: t def parse!(duration_string) do @@ -242,9 +254,10 @@ defmodule Duration do end end - defp parse(<<>>, duration, "", _), do: {:ok, new(Enum.into(duration, []))} + defp parse(<<>>, duration, "", _), do: {:ok, new!(Enum.into(duration, []))} - defp parse(<>, duration, buffer, is_time) when c in ?0..?9 or c == ?. do + defp parse(<>, duration, buffer, is_time) + when c in ?0..?9 or c in [?., ?-] do parse(rest, duration, <>, is_time) end diff --git a/lib/elixir/pages/getting-started/sigils.md b/lib/elixir/pages/getting-started/sigils.md index 3b13317e90f..d079d47db8c 100644 --- a/lib/elixir/pages/getting-started/sigils.md +++ b/lib/elixir/pages/getting-started/sigils.md @@ -207,7 +207,8 @@ iex> time_zone ### Duration -A [%Duration{}](`Duration`) struct represents a collection of time scale units, The `~P` sigil allows developers to create Durations from an ISO 8601-2 formatted duration string: +A [%Duration{}](`Duration`) struct represents a collection of time scale units. +The `~P` sigil allows developers to create Durations from an ISO 8601-2 formatted duration string: ```elixir iex> ~P[P1Y2M3DT4H5M6S] diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 750345d5199..cbaa56f9c39 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -255,6 +255,10 @@ defmodule DurationTest do assert Duration.parse!("PT6S") == %Duration{second: 6} assert Duration.parse!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 6}} assert Duration.parse!("PT1.12345678S") == %Duration{second: 1, microsecond: {123_456, 6}} + assert Duration.parse!("P3Y4W-3DT-6S") == %Duration{year: 3, week: 4, day: -3, second: -6} + assert Duration.parse!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 6}} + assert Duration.parse!("-P3WT5H3M") == %Duration{week: -3, hour: -5, minute: -3} + assert Duration.parse!("-P-3WT5H3M") == %Duration{week: 3, hour: -5, minute: -3} assert_raise ArgumentError, ~s/failed to parse duration. reason: "unexpected character: H"/, From af4bffd2472097765d6b0fe498154823b9142a90 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Fri, 5 Apr 2024 01:07:06 +0200 Subject: [PATCH 05/20] rename Duration.parse/1 to Duration.from_iso8601/1 --- lib/elixir/lib/calendar/duration.ex | 26 +++---- lib/elixir/lib/kernel.ex | 2 +- .../test/elixir/calendar/duration_test.exs | 73 +++++++++++-------- 3 files changed, 56 insertions(+), 45 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index cca7414710c..cd40db44d00 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -203,24 +203,24 @@ defmodule Duration do ## Examples - iex> Duration.parse("P1Y2M3DT4H5M6S") + iex> Duration.from_iso8601("P1Y2M3DT4H5M6S") {:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}} - iex> Duration.parse("PT10H30M") + iex> Duration.from_iso8601("PT10H30M") {:ok, %Duration{hour: 10, minute: 30, second: 0}} - iex> Duration.parse("P3Y-2MT3H") + iex> Duration.from_iso8601("P3Y-2MT3H") {:ok, %Duration{year: 3, month: -2, hour: 3}} - iex> Duration.parse("-P3Y2MT3H") + iex> Duration.from_iso8601("-P3Y2MT3H") {:ok, %Duration{year: -3, month: -2, hour: -3}} - iex> Duration.parse("-P3Y-2MT3H") + iex> Duration.from_iso8601("-P3Y-2MT3H") {:ok, %Duration{year: -3, month: 2, hour: -3}} """ - @spec parse(String.t()) :: {:ok, t} | {:error, String.t()} - def parse("P" <> duration_string) do + @spec from_iso8601(String.t()) :: {:ok, t} | {:error, String.t()} + def from_iso8601("P" <> duration_string) do parse(duration_string, %{}, "", false) end - def parse("-P" <> duration_string) do + def from_iso8601("-P" <> duration_string) do case parse(duration_string, %{}, "", false) do {:ok, duration} -> {:ok, negate(duration)} @@ -230,7 +230,7 @@ defmodule Duration do end end - def parse(_) do + def from_iso8601(_) do {:error, "invalid duration string"} end @@ -239,13 +239,13 @@ defmodule Duration do ## Examples - iex> Duration.parse!("P1Y2M3DT4H5M6S") + iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S") %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} """ - @spec parse!(String.t()) :: t - def parse!(duration_string) do - case parse(duration_string) do + @spec from_iso8601!(String.t()) :: t + def from_iso8601!(duration_string) do + case from_iso8601(duration_string) do {:ok, duration} -> duration diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index f3aa510a5d0..727565f515c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6676,7 +6676,7 @@ defmodule Kernel do defmacro sigil_P({:<<>>, _, [duration_string]}, []) do quote do - Duration.parse!(unquote(duration_string)) + Duration.from_iso8601!(unquote(duration_string)) end end diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index cbaa56f9c39..7ac801a1def 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -219,24 +219,24 @@ defmodule DurationTest do end test "parse/1" do - assert Duration.parse("P1Y2M3DT4H5M6S") == + assert Duration.from_iso8601("P1Y2M3DT4H5M6S") == {:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}} - assert Duration.parse("P3WT5H3M") == {:ok, %Duration{week: 3, hour: 5, minute: 3}} - assert Duration.parse("PT5H3M") == {:ok, %Duration{hour: 5, minute: 3}} - assert Duration.parse("P1Y2M3D") == {:ok, %Duration{year: 1, month: 2, day: 3}} - assert Duration.parse("PT4H5M6S") == {:ok, %Duration{hour: 4, minute: 5, second: 6}} - assert Duration.parse("P1Y2M") == {:ok, %Duration{year: 1, month: 2}} - assert Duration.parse("P3D") == {:ok, %Duration{day: 3}} - assert Duration.parse("PT4H5M") == {:ok, %Duration{hour: 4, minute: 5}} - assert Duration.parse("PT6S") == {:ok, %Duration{second: 6}} - assert Duration.parse("P5H3HT4M") == {:error, "unexpected character: H"} - assert Duration.parse("P4Y2W3Y") == {:error, "year was already provided"} - assert Duration.parse("invalid") == {:error, "invalid duration string"} + assert Duration.from_iso8601("P3WT5H3M") == {:ok, %Duration{week: 3, hour: 5, minute: 3}} + assert Duration.from_iso8601("PT5H3M") == {:ok, %Duration{hour: 5, minute: 3}} + assert Duration.from_iso8601("P1Y2M3D") == {:ok, %Duration{year: 1, month: 2, day: 3}} + assert Duration.from_iso8601("PT4H5M6S") == {:ok, %Duration{hour: 4, minute: 5, second: 6}} + assert Duration.from_iso8601("P1Y2M") == {:ok, %Duration{year: 1, month: 2}} + assert Duration.from_iso8601("P3D") == {:ok, %Duration{day: 3}} + assert Duration.from_iso8601("PT4H5M") == {:ok, %Duration{hour: 4, minute: 5}} + assert Duration.from_iso8601("PT6S") == {:ok, %Duration{second: 6}} + assert Duration.from_iso8601("P5H3HT4M") == {:error, "unexpected character: H"} + assert Duration.from_iso8601("P4Y2W3Y") == {:error, "year was already provided"} + assert Duration.from_iso8601("invalid") == {:error, "invalid duration string"} end test "parse!/1" do - assert Duration.parse!("P1Y2M3DT4H5M6S") == %Duration{ + assert Duration.from_iso8601!("P1Y2M3DT4H5M6S") == %Duration{ year: 1, month: 2, day: 3, @@ -245,43 +245,54 @@ defmodule DurationTest do second: 6 } - assert Duration.parse!("P3WT5H3M") == %Duration{week: 3, hour: 5, minute: 3} - assert Duration.parse!("PT5H3M") == %Duration{hour: 5, minute: 3} - assert Duration.parse!("P1Y2M3D") == %Duration{year: 1, month: 2, day: 3} - assert Duration.parse!("PT4H5M6S") == %Duration{hour: 4, minute: 5, second: 6} - assert Duration.parse!("P1Y2M") == %Duration{year: 1, month: 2} - assert Duration.parse!("P3D") == %Duration{day: 3} - assert Duration.parse!("PT4H5M") == %Duration{hour: 4, minute: 5} - assert Duration.parse!("PT6S") == %Duration{second: 6} - assert Duration.parse!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 6}} - assert Duration.parse!("PT1.12345678S") == %Duration{second: 1, microsecond: {123_456, 6}} - assert Duration.parse!("P3Y4W-3DT-6S") == %Duration{year: 3, week: 4, day: -3, second: -6} - assert Duration.parse!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 6}} - assert Duration.parse!("-P3WT5H3M") == %Duration{week: -3, hour: -5, minute: -3} - assert Duration.parse!("-P-3WT5H3M") == %Duration{week: 3, hour: -5, minute: -3} + assert Duration.from_iso8601!("P3WT5H3M") == %Duration{week: 3, hour: 5, minute: 3} + assert Duration.from_iso8601!("PT5H3M") == %Duration{hour: 5, minute: 3} + assert Duration.from_iso8601!("P1Y2M3D") == %Duration{year: 1, month: 2, day: 3} + assert Duration.from_iso8601!("PT4H5M6S") == %Duration{hour: 4, minute: 5, second: 6} + assert Duration.from_iso8601!("P1Y2M") == %Duration{year: 1, month: 2} + assert Duration.from_iso8601!("P3D") == %Duration{day: 3} + assert Duration.from_iso8601!("PT4H5M") == %Duration{hour: 4, minute: 5} + assert Duration.from_iso8601!("PT6S") == %Duration{second: 6} + assert Duration.from_iso8601!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 6}} + + assert Duration.from_iso8601!("PT1.12345678S") == %Duration{ + second: 1, + microsecond: {123_456, 6} + } + + assert Duration.from_iso8601!("P3Y4W-3DT-6S") == %Duration{ + year: 3, + week: 4, + day: -3, + second: -6 + } + + assert Duration.from_iso8601!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 6}} + assert Duration.from_iso8601!("-P3WT5H3M") == %Duration{week: -3, hour: -5, minute: -3} + assert Duration.from_iso8601!("-P-3WT5H3M") == %Duration{week: 3, hour: -5, minute: -3} assert_raise ArgumentError, ~s/failed to parse duration. reason: "unexpected character: H"/, fn -> - Duration.parse!("P5H3HT4M") + Duration.from_iso8601!("P5H3HT4M") end assert_raise ArgumentError, ~s/failed to parse duration. reason: "year was already provided"/, fn -> - Duration.parse!("P4Y2W3Y") + Duration.from_iso8601!("P4Y2W3Y") end assert_raise ArgumentError, ~s/failed to parse duration. reason: "invalid duration string"/, fn -> - Duration.parse!("invalid") + Duration.from_iso8601!("invalid") end assert_raise ArgumentError, ~s/failed to parse duration. reason: "invalid value for year: 4.5"/, fn -> - Duration.parse!("P4.5YT6S") + Duration.from_iso8601!("P4.5YT6S") end end From 5aec1671f81a758c89c949e14049b3e1582859ad Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Fri, 5 Apr 2024 01:09:52 +0200 Subject: [PATCH 06/20] drop sigil_P for now --- 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 -- lib/elixir/lib/kernel.ex | 25 --------------- lib/elixir/pages/getting-started/sigils.md | 12 ------- .../test/elixir/calendar/duration_test.exs | 31 ------------------- 7 files changed, 76 deletions(-) diff --git a/lib/elixir/lib/calendar/date.ex b/lib/elixir/lib/calendar/date.ex index 5500170d433..8e6079ca50b 100644 --- a/lib/elixir/lib/calendar/date.ex +++ b/lib/elixir/lib/calendar/date.ex @@ -773,8 +773,6 @@ defmodule Date do ## Examples - iex> Date.shift(~D[2016-01-31], ~P[P4Y1D]) - ~D[2020-02-01] iex> Date.shift(~D[2016-01-03], month: 2) ~D[2016-03-03] iex> Date.shift(~D[2016-01-30], month: -1) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 5d455b2f8ca..3053cf5d252 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1711,8 +1711,6 @@ defmodule DateTime do ## Examples - iex> DateTime.shift(~U[2016-01-01 00:00:00Z], ~P[P1Y4W]) - ~U[2017-01-29 00:00:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2) ~U[2016-03-01 00:00:00Z] iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4) diff --git a/lib/elixir/lib/calendar/naive_datetime.ex b/lib/elixir/lib/calendar/naive_datetime.ex index af810c31ef0..b9becf88fe9 100644 --- a/lib/elixir/lib/calendar/naive_datetime.ex +++ b/lib/elixir/lib/calendar/naive_datetime.ex @@ -585,8 +585,6 @@ defmodule NaiveDateTime do ## Examples - iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], ~P[P4Y1D]) - ~N[2020-02-01 00:00:00] iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1) ~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 8e963ccd5a9..f51e600c5f2 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -558,8 +558,6 @@ defmodule Time do ## Examples - iex> Time.shift(~T[01:15:00], ~P[PT6H15M]) - ~T[07:30:00] iex> Time.shift(~T[01:00:15], hour: 12) ~T[13:00:15] iex> Time.shift(~T[01:35:00], hour: 6, minute: -15) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 727565f515c..26a41c84d29 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6655,31 +6655,6 @@ defmodule Kernel do end end - @doc ~S""" - Handles the sigil `~P` to create a `Duration`. - - ## Examples - - iex> ~P[P1Y2M3DT4H5M6S] - %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} - iex> ~P[PT4H1.5S] - %Duration{hour: 4, second: 1, microsecond: {500000, 6}} - iex> ~P[P-1Y3WT4H] - %Duration{year: -1, week: 3, hour: 4} - iex> ~P[-P1Y3WT4H] - %Duration{year: -1, week: -3, hour: -4} - iex> ~P[-P1Y-3WT4H] - %Duration{year: -1, week: 3, hour: -4} - - """ - defmacro sigil_P(duration_string, modifiers) - - defmacro sigil_P({:<<>>, _, [duration_string]}, []) do - quote do - Duration.from_iso8601!(unquote(duration_string)) - end - end - @doc ~S""" Handles the sigil `~w` for list of words. diff --git a/lib/elixir/pages/getting-started/sigils.md b/lib/elixir/pages/getting-started/sigils.md index d079d47db8c..9da5509a42d 100644 --- a/lib/elixir/pages/getting-started/sigils.md +++ b/lib/elixir/pages/getting-started/sigils.md @@ -205,18 +205,6 @@ iex> time_zone "Etc/UTC" ``` -### Duration - -A [%Duration{}](`Duration`) struct represents a collection of time scale units. -The `~P` sigil allows developers to create Durations from an ISO 8601-2 formatted duration string: - -```elixir -iex> ~P[P1Y2M3DT4H5M6S] -%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} -iex> ~P[-P1Y-3WT4H-1.5S] -%Duration{year: -1, week: 3, hour: -4, second: 1, microsecond: {500000, 6}} -``` - ## Custom sigils As hinted at the beginning of this chapter, sigils in Elixir are extensible. In fact, using the sigil `~r/foo/i` is equivalent to calling `sigil_r` with a binary and a char list as the argument: diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 7ac801a1def..d29dd19ba7d 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -295,35 +295,4 @@ defmodule DurationTest do Duration.from_iso8601!("P4.5YT6S") end end - - test "sigil_P" do - assert ~P[P1Y2M3DT4H5M6S] == %Duration{ - year: 1, - month: 2, - day: 3, - hour: 4, - minute: 5, - second: 6 - } - - assert ~P[PT5H3M] == %Duration{hour: 5, minute: 3} - assert ~P[P1Y2M3D] == %Duration{year: 1, month: 2, day: 3} - assert ~P[PT4H5M6S] == %Duration{hour: 4, minute: 5, second: 6} - assert ~P[P1Y2M] == %Duration{year: 1, month: 2} - assert ~P[P3D] == %Duration{day: 3} - assert ~P[PT4H5M] == %Duration{hour: 4, minute: 5} - assert ~P[PT6S] == %Duration{second: 6} - - assert_raise ArgumentError, - ~s/failed to parse duration. reason: "unexpected character: H"/, - fn -> - Code.eval_string("~P[P5H3HT4M]") - end - - assert_raise ArgumentError, - ~s/failed to parse duration. reason: "invalid duration string"/, - fn -> - Code.eval_string("~P[invalid]") - end - end end From 6ff56b89b955a3b332914fcfb4f5ea7f87e6d78d Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 25 May 2024 21:45:23 +0200 Subject: [PATCH 07/20] parse date and time separately --- lib/elixir/lib/calendar/duration.ex | 83 +++++++++++-------- .../test/elixir/calendar/duration_test.exs | 4 +- 2 files changed, 50 insertions(+), 37 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index cd40db44d00..96e290f5b77 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -217,11 +217,11 @@ defmodule Duration do """ @spec from_iso8601(String.t()) :: {:ok, t} | {:error, String.t()} def from_iso8601("P" <> duration_string) do - parse(duration_string, %{}, "", false) + date_parse(duration_string, %{}, "") end def from_iso8601("-P" <> duration_string) do - case parse(duration_string, %{}, "", false) do + case date_parse(duration_string, %{}, "") do {:ok, duration} -> {:ok, negate(duration)} @@ -254,58 +254,74 @@ defmodule Duration do end end - defp parse(<<>>, duration, "", _), do: {:ok, new!(Enum.into(duration, []))} + defp date_parse(_, {:error, error}, _), do: {:error, error} - defp parse(<>, duration, buffer, is_time) - when c in ?0..?9 or c in [?., ?-] do - parse(rest, duration, <>, is_time) + defp date_parse(<<>>, duration, ""), do: {:ok, new!(Keyword.new(duration))} + + defp date_parse(<>, duration, buffer) when c in ?0..?9 or c in [?., ?-] do + date_parse(rest, duration, <>) + end + + defp date_parse(<<"Y", rest::binary>>, duration, buffer) do + duration = buffer_parse(:year, duration, buffer) + date_parse(rest, duration, "") end - defp parse(<<"Y", rest::binary>>, duration, buffer, false) do - parse(:year, rest, duration, buffer, false) + defp date_parse(<<"M", rest::binary>>, duration, buffer) do + duration = buffer_parse(:month, duration, buffer) + date_parse(rest, duration, "") end - defp parse(<<"M", rest::binary>>, duration, buffer, false) do - parse(:month, rest, duration, buffer, false) + defp date_parse(<<"W", rest::binary>>, duration, buffer) do + duration = buffer_parse(:week, duration, buffer) + date_parse(rest, duration, "") end - defp parse(<<"W", rest::binary>>, duration, buffer, false) do - parse(:week, rest, duration, buffer, false) + defp date_parse(<<"D", rest::binary>>, duration, buffer) do + duration = buffer_parse(:day, duration, buffer) + date_parse(rest, duration, "") end - defp parse(<<"D", rest::binary>>, duration, buffer, false) do - parse(:day, rest, duration, buffer, false) + defp date_parse(<<"T", rest::binary>>, duration, _buffer) do + time_parse(rest, duration, "") end - defp parse(<<"T", _::binary>>, _duration, _, true) do - {:error, "time delimiter was already provided"} + defp date_parse(<>, _, _) do + {:error, "unexpected character: #{<>}"} end - defp parse(<<"T", rest::binary>>, duration, _buffer, false) do - parse(rest, duration, "", true) + defp time_parse(_, {:error, error}, _), do: {:error, error} + + defp time_parse(<<>>, duration, ""), do: {:ok, new!(Keyword.new(duration))} + + defp time_parse(<>, duration, buffer) when c in ?0..?9 or c in [?., ?-] do + time_parse(rest, duration, <>) end - defp parse(<<"H", rest::binary>>, duration, buffer, true) do - parse(:hour, rest, duration, buffer, true) + defp time_parse(<<"H", rest::binary>>, duration, buffer) do + duration = buffer_parse(:hour, duration, buffer) + time_parse(rest, duration, "") end - defp parse(<<"M", rest::binary>>, duration, buffer, true) do - parse(:minute, rest, duration, buffer, true) + defp time_parse(<<"M", rest::binary>>, duration, buffer) do + duration = buffer_parse(:minute, duration, buffer) + time_parse(rest, duration, "") end - defp parse(<<"S", rest::binary>>, duration, buffer, true) do - parse(:second, rest, duration, buffer, true) + defp time_parse(<<"S", rest::binary>>, duration, buffer) do + duration = buffer_parse(:second, duration, buffer) + time_parse(rest, duration, "") end - defp parse(<>, _, _, _) do + defp time_parse(<>, _, _) do {:error, "unexpected character: #{<>}"} end - defp parse(unit, _string, duration, _buffer, _is_time) when is_map_key(duration, unit) do + defp buffer_parse(unit, duration, _buffer) when is_map_key(duration, unit) do {:error, "#{unit} was already provided"} end - defp parse(:second, string, duration, buffer, is_time) do + defp buffer_parse(:second, duration, buffer) do case Float.parse(buffer) do {float_second, ""} -> second = trunc(float_second) @@ -316,22 +332,19 @@ defmodule Duration do microsecond -> {microsecond, 6} end - duration = - duration - |> Map.put(:second, second) - |> Map.put(:microsecond, {microsecond, precision}) - - parse(string, duration, "", is_time) + duration + |> Map.put(:second, second) + |> Map.put(:microsecond, {microsecond, precision}) _ -> {:error, "invalid value for second: #{buffer}"} end end - defp parse(unit, string, duration, buffer, is_time) do + defp buffer_parse(unit, duration, buffer) do case Integer.parse(buffer) do {duration_value, ""} -> - parse(string, Map.put(duration, unit, duration_value), "", is_time) + Map.put(duration, unit, duration_value) _ -> {:error, "invalid value for #{unit}: #{buffer}"} diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index d29dd19ba7d..7a07f33c936 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -218,7 +218,7 @@ defmodule DurationTest do } end - test "parse/1" do + test "from_iso8601/1" do assert Duration.from_iso8601("P1Y2M3DT4H5M6S") == {:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}} @@ -235,7 +235,7 @@ defmodule DurationTest do assert Duration.from_iso8601("invalid") == {:error, "invalid duration string"} end - test "parse!/1" do + test "from_iso8601!/1" do assert Duration.from_iso8601!("P1Y2M3DT4H5M6S") == %Duration{ year: 1, month: 2, From 82da3efd74cc954b47138f23590d7267fb224334 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 25 May 2024 22:14:16 +0200 Subject: [PATCH 08/20] error reasons as atoms --- lib/elixir/lib/calendar/duration.ex | 19 ++++++++++--------- .../test/elixir/calendar/duration_test.exs | 15 ++++++++------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 96e290f5b77..e09b31c7856 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -231,7 +231,7 @@ defmodule Duration do end def from_iso8601(_) do - {:error, "invalid duration string"} + {:error, :invalid_duration} end @doc """ @@ -250,7 +250,8 @@ defmodule Duration do duration {:error, reason} -> - raise ArgumentError, "failed to parse duration. reason: #{inspect(reason)}" + raise ArgumentError, + ~s/failed to parse duration "#{duration_string}". reason: #{inspect(reason)}/ end end @@ -286,8 +287,8 @@ defmodule Duration do time_parse(rest, duration, "") end - defp date_parse(<>, _, _) do - {:error, "unexpected character: #{<>}"} + defp date_parse(_, _, _) do + {:error, :invalid_character} end defp time_parse(_, {:error, error}, _), do: {:error, error} @@ -313,12 +314,12 @@ defmodule Duration do time_parse(rest, duration, "") end - defp time_parse(<>, _, _) do - {:error, "unexpected character: #{<>}"} + defp time_parse(_, _, _) do + {:error, :invalid_character} end defp buffer_parse(unit, duration, _buffer) when is_map_key(duration, unit) do - {:error, "#{unit} was already provided"} + {:error, :duplicate_unit} end defp buffer_parse(:second, duration, buffer) do @@ -337,7 +338,7 @@ defmodule Duration do |> Map.put(:microsecond, {microsecond, precision}) _ -> - {:error, "invalid value for second: #{buffer}"} + {:error, :invalid_unit_value} end end @@ -347,7 +348,7 @@ defmodule Duration do Map.put(duration, unit, duration_value) _ -> - {:error, "invalid value for #{unit}: #{buffer}"} + {:error, :invalid_unit_value} end end end diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 7a07f33c936..2e235f6cb71 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -230,9 +230,10 @@ defmodule DurationTest do assert Duration.from_iso8601("P3D") == {:ok, %Duration{day: 3}} assert Duration.from_iso8601("PT4H5M") == {:ok, %Duration{hour: 4, minute: 5}} assert Duration.from_iso8601("PT6S") == {:ok, %Duration{second: 6}} - assert Duration.from_iso8601("P5H3HT4M") == {:error, "unexpected character: H"} - assert Duration.from_iso8601("P4Y2W3Y") == {:error, "year was already provided"} - assert Duration.from_iso8601("invalid") == {:error, "invalid duration string"} + assert Duration.from_iso8601("P4Y2W3Y") == {:error, :duplicate_unit} + assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_character} + assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_character} + assert Duration.from_iso8601("invalid") == {:error, :invalid_duration} end test "from_iso8601!/1" do @@ -272,25 +273,25 @@ defmodule DurationTest do assert Duration.from_iso8601!("-P-3WT5H3M") == %Duration{week: 3, hour: -5, minute: -3} assert_raise ArgumentError, - ~s/failed to parse duration. reason: "unexpected character: H"/, + ~s/failed to parse duration "P5H3HT4M". reason: :invalid_character/, fn -> Duration.from_iso8601!("P5H3HT4M") end assert_raise ArgumentError, - ~s/failed to parse duration. reason: "year was already provided"/, + ~s/failed to parse duration "P4Y2W3Y". reason: :duplicate_unit/, fn -> Duration.from_iso8601!("P4Y2W3Y") end assert_raise ArgumentError, - ~s/failed to parse duration. reason: "invalid duration string"/, + ~s/failed to parse duration "invalid". reason: :invalid_duration/, fn -> Duration.from_iso8601!("invalid") end assert_raise ArgumentError, - ~s/failed to parse duration. reason: "invalid value for year: 4.5"/, + ~s/failed to parse duration "P4.5YT6S". reason: :invalid_unit_value/, fn -> Duration.from_iso8601!("P4.5YT6S") end From 9ffda985c044d23192bf5be859245e6e8cbda1a7 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sat, 25 May 2024 22:40:31 +0200 Subject: [PATCH 09/20] move to Calendar.ISO.parse_duration/1 --- lib/elixir/lib/calendar/duration.ex | 113 +++------------------------- lib/elixir/lib/calendar/iso.ex | 113 ++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 102 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index e09b31c7856..9c00bef73a8 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -215,13 +215,19 @@ defmodule Duration do {:ok, %Duration{year: -3, month: 2, hour: -3}} """ - @spec from_iso8601(String.t()) :: {:ok, t} | {:error, String.t()} - def from_iso8601("P" <> duration_string) do - date_parse(duration_string, %{}, "") + @spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom} + def from_iso8601("P" <> _ = duration_string) do + case Calendar.ISO.parse_duration(duration_string) do + {:ok, duration} -> + {:ok, new!(duration)} + + error -> + error + end end def from_iso8601("-P" <> duration_string) do - case date_parse(duration_string, %{}, "") do + case from_iso8601("P" <> duration_string) do {:ok, duration} -> {:ok, negate(duration)} @@ -235,7 +241,7 @@ defmodule Duration do end @doc """ - Same as parse/1 but raises an ArgumentError. + Same as from_iso8601/1 but raises an ArgumentError. ## Examples @@ -254,101 +260,4 @@ defmodule Duration do ~s/failed to parse duration "#{duration_string}". reason: #{inspect(reason)}/ end end - - defp date_parse(_, {:error, error}, _), do: {:error, error} - - defp date_parse(<<>>, duration, ""), do: {:ok, new!(Keyword.new(duration))} - - defp date_parse(<>, duration, buffer) when c in ?0..?9 or c in [?., ?-] do - date_parse(rest, duration, <>) - end - - defp date_parse(<<"Y", rest::binary>>, duration, buffer) do - duration = buffer_parse(:year, duration, buffer) - date_parse(rest, duration, "") - end - - defp date_parse(<<"M", rest::binary>>, duration, buffer) do - duration = buffer_parse(:month, duration, buffer) - date_parse(rest, duration, "") - end - - defp date_parse(<<"W", rest::binary>>, duration, buffer) do - duration = buffer_parse(:week, duration, buffer) - date_parse(rest, duration, "") - end - - defp date_parse(<<"D", rest::binary>>, duration, buffer) do - duration = buffer_parse(:day, duration, buffer) - date_parse(rest, duration, "") - end - - defp date_parse(<<"T", rest::binary>>, duration, _buffer) do - time_parse(rest, duration, "") - end - - defp date_parse(_, _, _) do - {:error, :invalid_character} - end - - defp time_parse(_, {:error, error}, _), do: {:error, error} - - defp time_parse(<<>>, duration, ""), do: {:ok, new!(Keyword.new(duration))} - - defp time_parse(<>, duration, buffer) when c in ?0..?9 or c in [?., ?-] do - time_parse(rest, duration, <>) - end - - defp time_parse(<<"H", rest::binary>>, duration, buffer) do - duration = buffer_parse(:hour, duration, buffer) - time_parse(rest, duration, "") - end - - defp time_parse(<<"M", rest::binary>>, duration, buffer) do - duration = buffer_parse(:minute, duration, buffer) - time_parse(rest, duration, "") - end - - defp time_parse(<<"S", rest::binary>>, duration, buffer) do - duration = buffer_parse(:second, duration, buffer) - time_parse(rest, duration, "") - end - - defp time_parse(_, _, _) do - {:error, :invalid_character} - end - - defp buffer_parse(unit, duration, _buffer) when is_map_key(duration, unit) do - {:error, :duplicate_unit} - end - - defp buffer_parse(:second, duration, buffer) do - case Float.parse(buffer) do - {float_second, ""} -> - second = trunc(float_second) - - {microsecond, precision} = - case trunc((float_second - second) * 1_000_000) do - 0 -> {0, 0} - microsecond -> {microsecond, 6} - end - - duration - |> Map.put(:second, second) - |> Map.put(:microsecond, {microsecond, precision}) - - _ -> - {:error, :invalid_unit_value} - end - end - - defp buffer_parse(unit, duration, buffer) do - case Integer.parse(buffer) do - {duration_value, ""} -> - Map.put(duration, unit, duration_value) - - _ -> - {:error, :invalid_unit_value} - end - end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 2d0bd59ceb2..123170900fd 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -663,6 +663,119 @@ defmodule Calendar.ISO do end end + @doc """ + Parses an ISO 8601-2 formatted duration string to `Duration` compabitble fields. + + """ + @doc since: "1.17.0" + @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()} + def parse_duration("P" <> duration_string) do + parse_date_duration(duration_string, %{}, "") + end + + def parse_duration(_) do + {:error, :invalid_duration} + end + + defp parse_date_duration(_, {:error, error}, _), do: {:error, error} + + defp parse_date_duration(<<>>, duration, ""), do: {:ok, Keyword.new(duration)} + + defp parse_date_duration(<>, duration, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_date_duration(rest, duration, <>) + end + + defp parse_date_duration(<<"Y", rest::binary>>, duration, buffer) do + duration = parse_buffer_duration(:year, duration, buffer) + parse_date_duration(rest, duration, "") + end + + defp parse_date_duration(<<"M", rest::binary>>, duration, buffer) do + duration = parse_buffer_duration(:month, duration, buffer) + parse_date_duration(rest, duration, "") + end + + defp parse_date_duration(<<"W", rest::binary>>, duration, buffer) do + duration = parse_buffer_duration(:week, duration, buffer) + parse_date_duration(rest, duration, "") + end + + defp parse_date_duration(<<"D", rest::binary>>, duration, buffer) do + duration = parse_buffer_duration(:day, duration, buffer) + parse_date_duration(rest, duration, "") + end + + defp parse_date_duration(<<"T", rest::binary>>, duration, _buffer) do + parse_time_duration(rest, duration, "") + end + + defp parse_date_duration(_, _, _) do + {:error, :invalid_character} + end + + defp parse_time_duration(_, {:error, error}, _), do: {:error, error} + + defp parse_time_duration(<<>>, duration, ""), do: {:ok, Keyword.new(duration)} + + defp parse_time_duration(<>, duration, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_time_duration(rest, duration, <>) + end + + defp parse_time_duration(<<"H", rest::binary>>, duration, buffer) do + duration = parse_buffer_duration(:hour, duration, buffer) + parse_time_duration(rest, duration, "") + end + + defp parse_time_duration(<<"M", rest::binary>>, duration, buffer) do + duration = parse_buffer_duration(:minute, duration, buffer) + parse_time_duration(rest, duration, "") + end + + defp parse_time_duration(<<"S", rest::binary>>, duration, buffer) do + duration = parse_buffer_duration(:second, duration, buffer) + parse_time_duration(rest, duration, "") + end + + defp parse_time_duration(_, _, _) do + {:error, :invalid_character} + end + + defp parse_buffer_duration(unit, duration, _buffer) when is_map_key(duration, unit) do + {:error, :duplicate_unit} + end + + defp parse_buffer_duration(:second, duration, buffer) do + case Float.parse(buffer) do + {float_second, ""} -> + second = trunc(float_second) + + {microsecond, precision} = + case trunc((float_second - second) * 1_000_000) do + 0 -> {0, 0} + microsecond -> {microsecond, 6} + end + + duration + |> Map.put(:second, second) + |> Map.put(:microsecond, {microsecond, precision}) + + _ -> + {:error, :invalid_unit_value} + end + end + + defp parse_buffer_duration(unit, duration, buffer) do + case Integer.parse(buffer) do + {duration_value, ""} -> + Map.put(duration, unit, duration_value) + + _ -> + {:error, :invalid_unit_value} + end + end + @doc """ Returns the `t:Calendar.iso_days/0` format of the specified date. From 2175c12f09866c1385d2f40db4ea0c3974f468f5 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 00:12:57 +0200 Subject: [PATCH 10/20] parse_microsecond --- lib/elixir/lib/calendar/iso.ex | 65 ++++++++++++++----- .../test/elixir/calendar/duration_test.exs | 4 +- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 123170900fd..6a0d2d985ab 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -746,24 +746,12 @@ defmodule Calendar.ISO do {:error, :duplicate_unit} end - defp parse_buffer_duration(:second, duration, buffer) do - case Float.parse(buffer) do - {float_second, ""} -> - second = trunc(float_second) - - {microsecond, precision} = - case trunc((float_second - second) * 1_000_000) do - 0 -> {0, 0} - microsecond -> {microsecond, 6} - end - - duration - |> Map.put(:second, second) - |> Map.put(:microsecond, {microsecond, precision}) + defp parse_buffer_duration(:second, duration, "-" <> buffer) do + parse_second_duration(duration, buffer, -1) + end - _ -> - {:error, :invalid_unit_value} - end + defp parse_buffer_duration(:second, duration, buffer) do + parse_second_duration(duration, buffer, 1) end defp parse_buffer_duration(unit, duration, buffer) do @@ -776,6 +764,49 @@ defmodule Calendar.ISO do end end + defp parse_second_duration(duration, buffer, multiplier) do + case parse_seconds(buffer, "") do + {second, ".0"} -> + Map.put(duration, :second, multiplier * second) + + {second, microsecond} -> + case parse_microsecond(microsecond) do + {{ms, precision}, ""} -> + duration + |> Map.put(:second, multiplier * second) + |> Map.put(:microsecond, {multiplier * ms, precision}) + + _ -> + {:error, :invalid_unit_value} + end + + error -> + error + end + end + + defp parse_seconds(<<>>, second), do: {parse_second(second), ".0"} + + defp parse_seconds(<>, second) when c in ?0..?9 do + parse_seconds(rest, <>) + end + + defp parse_seconds(<>, second) when c in [?., ?,] do + {parse_second(second), <>} + end + + defp parse_seconds(_, _) do + {:error, :invalid_unit_value} + end + + defp parse_second("") do + 0 + end + + defp parse_second(second) do + String.to_integer(second) + end + @doc """ Returns the `t:Calendar.iso_days/0` format of the specified date. diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 2e235f6cb71..eda9f539bf2 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -254,7 +254,7 @@ defmodule DurationTest do assert Duration.from_iso8601!("P3D") == %Duration{day: 3} assert Duration.from_iso8601!("PT4H5M") == %Duration{hour: 4, minute: 5} assert Duration.from_iso8601!("PT6S") == %Duration{second: 6} - assert Duration.from_iso8601!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 6}} + assert Duration.from_iso8601!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 1}} assert Duration.from_iso8601!("PT1.12345678S") == %Duration{ second: 1, @@ -268,7 +268,7 @@ defmodule DurationTest do second: -6 } - assert Duration.from_iso8601!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 6}} + assert Duration.from_iso8601!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 2}} assert Duration.from_iso8601!("-P3WT5H3M") == %Duration{week: -3, hour: -5, minute: -3} assert Duration.from_iso8601!("-P-3WT5H3M") == %Duration{week: 3, hour: -5, minute: -3} From f5200d8171ea86ab2f5db021662f532669611cd0 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 00:29:25 +0200 Subject: [PATCH 11/20] drop support for sign prefix extension --- lib/elixir/lib/calendar/duration.ex | 20 +------------------ .../test/elixir/calendar/duration_test.exs | 2 -- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 9c00bef73a8..7183d01fc70 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -209,14 +209,10 @@ defmodule Duration do {:ok, %Duration{hour: 10, minute: 30, second: 0}} iex> Duration.from_iso8601("P3Y-2MT3H") {:ok, %Duration{year: 3, month: -2, hour: 3}} - iex> Duration.from_iso8601("-P3Y2MT3H") - {:ok, %Duration{year: -3, month: -2, hour: -3}} - iex> Duration.from_iso8601("-P3Y-2MT3H") - {:ok, %Duration{year: -3, month: 2, hour: -3}} """ @spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom} - def from_iso8601("P" <> _ = duration_string) do + def from_iso8601(duration_string) do case Calendar.ISO.parse_duration(duration_string) do {:ok, duration} -> {:ok, new!(duration)} @@ -226,20 +222,6 @@ defmodule Duration do end end - def from_iso8601("-P" <> duration_string) do - case from_iso8601("P" <> duration_string) do - {:ok, duration} -> - {:ok, negate(duration)} - - error -> - error - end - end - - def from_iso8601(_) do - {:error, :invalid_duration} - end - @doc """ Same as from_iso8601/1 but raises an ArgumentError. diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index eda9f539bf2..dd586c94df2 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -269,8 +269,6 @@ defmodule DurationTest do } assert Duration.from_iso8601!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 2}} - assert Duration.from_iso8601!("-P3WT5H3M") == %Duration{week: -3, hour: -5, minute: -3} - assert Duration.from_iso8601!("-P-3WT5H3M") == %Duration{week: 3, hour: -5, minute: -3} assert_raise ArgumentError, ~s/failed to parse duration "P5H3HT4M". reason: :invalid_character/, From e1f207be560ad74512e9964042cd5baa1cd01dc3 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 01:10:53 +0200 Subject: [PATCH 12/20] iso docs --- lib/elixir/lib/calendar/iso.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 6a0d2d985ab..487964d67bc 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -18,7 +18,8 @@ defmodule Calendar.ISO do The standard library supports a minimal set of possible ISO 8601 features. Specifically, the parser only supports calendar dates and does not support - ordinal and week formats. + ordinal and week formats. Additionally, it supports parsing ISO 8601 + formatted durations, including negative time units and fractional seconds. By default Elixir only parses extended-formatted date/times. You can opt-in to parse basic-formatted date/times. @@ -29,7 +30,7 @@ defmodule Calendar.ISO do Elixir does not support reduced accuracy formats (for example, a date without the day component) nor decimal precisions in the lowest component (such as - `10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals. + `10:01:25,5`). #### Examples @@ -664,7 +665,7 @@ defmodule Calendar.ISO do end @doc """ - Parses an ISO 8601-2 formatted duration string to `Duration` compabitble fields. + Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs. """ @doc since: "1.17.0" From 1133d87b41d5eddc2cf4d30b326ca5a9a7883a5a Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 01:11:07 +0200 Subject: [PATCH 13/20] fractional seconds test --- lib/elixir/test/elixir/calendar/duration_test.exs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index dd586c94df2..5b2e8247e03 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -255,6 +255,8 @@ defmodule DurationTest do assert Duration.from_iso8601!("PT4H5M") == %Duration{hour: 4, minute: 5} assert Duration.from_iso8601!("PT6S") == %Duration{second: 6} assert Duration.from_iso8601!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 1}} + assert Duration.from_iso8601!("PT-.6S") == %Duration{second: 0, microsecond: {-600_000, 1}} + assert Duration.from_iso8601!("PT-1.234567S") == %Duration{second: -1, microsecond: {-234_567, 6}} assert Duration.from_iso8601!("PT1.12345678S") == %Duration{ second: 1, From 5ecb739e40c11b057230c4b9507f27f0aa15e88e Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 01:19:06 +0200 Subject: [PATCH 14/20] cleanup fractional second parsing --- lib/elixir/lib/calendar/duration.ex | 11 +++++------ lib/elixir/lib/calendar/iso.ex | 29 +++++++++++++---------------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 7183d01fc70..8a750ecf478 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -212,8 +212,8 @@ defmodule Duration do """ @spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom} - def from_iso8601(duration_string) do - case Calendar.ISO.parse_duration(duration_string) do + def from_iso8601(string) when is_binary(string) do + case Calendar.ISO.parse_duration(string) do {:ok, duration} -> {:ok, new!(duration)} @@ -232,14 +232,13 @@ defmodule Duration do """ @spec from_iso8601!(String.t()) :: t - def from_iso8601!(duration_string) do - case from_iso8601(duration_string) do + def from_iso8601!(string) when is_binary(string) do + case from_iso8601(string) do {:ok, duration} -> duration {:error, reason} -> - raise ArgumentError, - ~s/failed to parse duration "#{duration_string}". reason: #{inspect(reason)}/ + raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/ end end end diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 487964d67bc..f18838796b4 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -670,8 +670,8 @@ defmodule Calendar.ISO do """ @doc since: "1.17.0" @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()} - def parse_duration("P" <> duration_string) do - parse_date_duration(duration_string, %{}, "") + def parse_duration("P" <> string) do + parse_date_duration(string, %{}, "") end def parse_duration(_) do @@ -766,7 +766,7 @@ defmodule Calendar.ISO do end defp parse_second_duration(duration, buffer, multiplier) do - case parse_seconds(buffer, "") do + case parse_fraction_duration(buffer, "") do {second, ".0"} -> Map.put(duration, :second, multiplier * second) @@ -786,26 +786,23 @@ defmodule Calendar.ISO do end end - defp parse_seconds(<<>>, second), do: {parse_second(second), ".0"} + defp parse_fraction_duration(<<>>, ""), do: {0, ".0"} + defp parse_fraction_duration(<<>>, second), do: {String.to_integer(second), ".0"} - defp parse_seconds(<>, second) when c in ?0..?9 do - parse_seconds(rest, <>) + defp parse_fraction_duration(<>, second) when c in ?0..?9 do + parse_fraction_duration(rest, <>) end - defp parse_seconds(<>, second) when c in [?., ?,] do - {parse_second(second), <>} + defp parse_fraction_duration(<>, "") when c in [?., ?,] do + {0, <>} end - defp parse_seconds(_, _) do - {:error, :invalid_unit_value} + defp parse_fraction_duration(<>, second) when c in [?., ?,] do + {String.to_integer(second), <>} end - defp parse_second("") do - 0 - end - - defp parse_second(second) do - String.to_integer(second) + defp parse_fraction_duration(_, _) do + {:error, :invalid_unit_value} end @doc """ From 188e61e310a39932557348ab85353f4d4c68a071 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 01:25:19 +0200 Subject: [PATCH 15/20] format --- lib/elixir/test/elixir/calendar/duration_test.exs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 5b2e8247e03..d5b0116334f 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -256,7 +256,11 @@ defmodule DurationTest do assert Duration.from_iso8601!("PT6S") == %Duration{second: 6} assert Duration.from_iso8601!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 1}} assert Duration.from_iso8601!("PT-.6S") == %Duration{second: 0, microsecond: {-600_000, 1}} - assert Duration.from_iso8601!("PT-1.234567S") == %Duration{second: -1, microsecond: {-234_567, 6}} + + assert Duration.from_iso8601!("PT-1.234567S") == %Duration{ + second: -1, + microsecond: {-234_567, 6} + } assert Duration.from_iso8601!("PT1.12345678S") == %Duration{ second: 1, From 75c2b2772805982258ffc0a0fbbe1b319fb9ec86 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 21:28:20 +0200 Subject: [PATCH 16/20] rewrite iso8601 duration parser --- lib/elixir/lib/calendar/iso.ex | 151 ++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 41 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index f18838796b4..93209b05655 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -670,81 +670,150 @@ defmodule Calendar.ISO do """ @doc since: "1.17.0" @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()} - def parse_duration("P" <> string) do - parse_date_duration(string, %{}, "") + def parse_duration(string) do + with {:ok, duration, rest, buffer} <- parse_duration_period(string, %{time: false}), + {:ok, duration, rest, buffer} <- parse_duration_year(rest, duration, buffer), + {:ok, duration, rest, buffer} <- parse_duration_month(rest, duration, buffer), + {:ok, duration, rest, buffer} <- parse_duration_week(rest, duration, buffer), + {:ok, duration, rest, buffer} <- parse_duration_day(rest, duration, buffer), + {:ok, duration, rest, buffer} <- parse_duration_time(rest, duration, buffer), + {:ok, duration, rest, buffer} <- parse_duration_hour(rest, duration, buffer), + {:ok, duration, rest, buffer} <- parse_duration_minute(rest, duration, buffer), + {:ok, duration, "", ""} <- parse_duration_second(rest, duration, buffer) do + {:ok, + duration + |> Map.take([:year, :month, :week, :day, :hour, :minute, :second, :microsecond]) + |> Keyword.new()} + else + {:error, error} -> + {:error, error} + + _ -> + {:error, :invalid_duration} + end end - def parse_duration(_) do - {:error, :invalid_duration} + defp parse_duration_period(<<"P", rest::binary>>, duration) do + {:ok, duration, rest, ""} end - defp parse_date_duration(_, {:error, error}, _), do: {:error, error} + defp parse_duration_year(<>, duration, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_duration_year(rest, duration, <>) + end + + defp parse_duration_year(<<"Y", rest::binary>>, duration, year) do + duration = Map.put(duration, :year, year) + {:ok, duration, rest, ""} + end - defp parse_date_duration(<<>>, duration, ""), do: {:ok, Keyword.new(duration)} + defp parse_duration_year(rest, duration, buffer) do + {:ok, duration, rest, buffer} + end - defp parse_date_duration(<>, duration, buffer) + defp parse_duration_month(<>, duration, buffer) when c in ?0..?9 or c in [?., ?-] do - parse_date_duration(rest, duration, <>) + parse_duration_month(rest, duration, <>) + end + + defp parse_duration_month(<<"M", rest::binary>>, duration, month) do + duration = Map.put(duration, :month, month) + {:ok, duration, rest, ""} end - defp parse_date_duration(<<"Y", rest::binary>>, duration, buffer) do - duration = parse_buffer_duration(:year, duration, buffer) - parse_date_duration(rest, duration, "") + defp parse_duration_month(rest, duration, buffer) do + {:ok, duration, rest, buffer} end - defp parse_date_duration(<<"M", rest::binary>>, duration, buffer) do - duration = parse_buffer_duration(:month, duration, buffer) - parse_date_duration(rest, duration, "") + defp parse_duration_week(<>, duration, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_duration_week(rest, duration, <>) end - defp parse_date_duration(<<"W", rest::binary>>, duration, buffer) do - duration = parse_buffer_duration(:week, duration, buffer) - parse_date_duration(rest, duration, "") + defp parse_duration_week(<<"W", rest::binary>>, duration, week) do + duration = Map.put(duration, :week, week) + {:ok, duration, rest, ""} end - defp parse_date_duration(<<"D", rest::binary>>, duration, buffer) do - duration = parse_buffer_duration(:day, duration, buffer) - parse_date_duration(rest, duration, "") + defp parse_duration_week(rest, duration, buffer) do + {:ok, duration, rest, buffer} end - defp parse_date_duration(<<"T", rest::binary>>, duration, _buffer) do - parse_time_duration(rest, duration, "") + defp parse_duration_day(<>, duration, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_duration_day(rest, duration, <>) end - defp parse_date_duration(_, _, _) do - {:error, :invalid_character} + defp parse_duration_day(<<"D", rest::binary>>, duration, day) do + duration = Map.put(duration, :day, day) + {:ok, duration, rest, ""} end - defp parse_time_duration(_, {:error, error}, _), do: {:error, error} + defp parse_duration_day(rest, duration, buffer) do + {:ok, duration, rest, buffer} + end - defp parse_time_duration(<<>>, duration, ""), do: {:ok, Keyword.new(duration)} + defp parse_duration_time(<<"T", rest::binary>>, duration, "") do + duration = Map.put(duration, :time, true) + {:ok, duration, rest, ""} + end - defp parse_time_duration(<>, duration, buffer) + defp parse_duration_time(rest, duration, buffer) do + {:ok, duration, rest, buffer} + end + + defp parse_duration_hour(<>, duration, buffer) when c in ?0..?9 or c in [?., ?-] do - parse_time_duration(rest, duration, <>) + parse_duration_hour(rest, duration, <>) + end + + defp parse_duration_hour(<<"H", rest::binary>>, %{time: true} = duration, hour) do + duration = Map.put(duration, :hour, hour) + {:ok, duration, rest, ""} + end + + defp parse_duration_hour(<<"H", rest::binary>>, duration, hour) do + {:error, :missing_time_part} + end + + defp parse_duration_hour(rest, duration, buffer) do + {:ok, duration, rest, buffer} end - defp parse_time_duration(<<"H", rest::binary>>, duration, buffer) do - duration = parse_buffer_duration(:hour, duration, buffer) - parse_time_duration(rest, duration, "") + defp parse_duration_minute(<>, duration, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_duration_minute(rest, duration, <>) end - defp parse_time_duration(<<"M", rest::binary>>, duration, buffer) do - duration = parse_buffer_duration(:minute, duration, buffer) - parse_time_duration(rest, duration, "") + defp parse_duration_minute(<<"M", rest::binary>>, %{time: true} = duration, minute) do + duration = Map.put(duration, :minute, minute) + {:ok, duration, rest, ""} + end + + defp parse_duration_minute(<<"M", rest::binary>>, duration, minute) do + {:error, :missing_time_part} + end + + defp parse_duration_minute(rest, duration, buffer) do + {:ok, duration, rest, buffer} + end + + defp parse_duration_second(<>, duration, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_duration_second(rest, duration, <>) end - defp parse_time_duration(<<"S", rest::binary>>, duration, buffer) do - duration = parse_buffer_duration(:second, duration, buffer) - parse_time_duration(rest, duration, "") + defp parse_duration_second(<<"S", rest::binary>>, %{time: true} = duration, second) do + duration = Map.put(duration, :second, second) + {:ok, duration, rest, ""} end - defp parse_time_duration(_, _, _) do - {:error, :invalid_character} + defp parse_duration_second(<<"S", rest::binary>>, duration, second) do + {:error, :missing_time_part} end - defp parse_buffer_duration(unit, duration, _buffer) when is_map_key(duration, unit) do - {:error, :duplicate_unit} + defp parse_duration_second(rest, duration, buffer) do + {:ok, duration, rest, buffer} end defp parse_buffer_duration(:second, duration, "-" <> buffer) do From 8d53caf78128d5b580ec8fbc213c2d1afb6b359e Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 22:08:17 +0200 Subject: [PATCH 17/20] make duration parsing less verbose --- lib/elixir/lib/calendar/iso.ex | 222 +++++++----------- .../test/elixir/calendar/duration_test.exs | 11 +- 2 files changed, 91 insertions(+), 142 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 93209b05655..38795219407 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -671,17 +671,17 @@ defmodule Calendar.ISO do @doc since: "1.17.0" @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()} def parse_duration(string) do - with {:ok, duration, rest, buffer} <- parse_duration_period(string, %{time: false}), - {:ok, duration, rest, buffer} <- parse_duration_year(rest, duration, buffer), - {:ok, duration, rest, buffer} <- parse_duration_month(rest, duration, buffer), - {:ok, duration, rest, buffer} <- parse_duration_week(rest, duration, buffer), - {:ok, duration, rest, buffer} <- parse_duration_day(rest, duration, buffer), - {:ok, duration, rest, buffer} <- parse_duration_time(rest, duration, buffer), - {:ok, duration, rest, buffer} <- parse_duration_hour(rest, duration, buffer), - {:ok, duration, rest, buffer} <- parse_duration_minute(rest, duration, buffer), - {:ok, duration, "", ""} <- parse_duration_second(rest, duration, buffer) do + with {:ok, acc, rest, buffer} <- parse_duration(:period, string, %{time: false}), + {:ok, acc, rest, buffer} <- parse_duration(:year, rest, acc, buffer), + {:ok, acc, rest, buffer} <- parse_duration(:month, rest, acc, buffer), + {:ok, acc, rest, buffer} <- parse_duration(:week, rest, acc, buffer), + {:ok, acc, rest, buffer} <- parse_duration(:day, rest, acc, buffer), + {:ok, acc, rest, buffer} <- parse_duration(:time, rest, acc, buffer), + {:ok, acc, rest, buffer} <- parse_duration(:hour, rest, acc, buffer), + {:ok, acc, rest, buffer} <- parse_duration(:minute, rest, acc, buffer), + {:ok, acc, "", ""} <- parse_duration(:second, rest, acc, buffer) do {:ok, - duration + acc |> Map.take([:year, :month, :week, :day, :hour, :minute, :second, :microsecond]) |> Keyword.new()} else @@ -693,161 +693,99 @@ defmodule Calendar.ISO do end end - defp parse_duration_period(<<"P", rest::binary>>, duration) do - {:ok, duration, rest, ""} + defp parse_duration(:period, <<"P", rest::binary>>, acc) do + {:ok, acc, rest, ""} end - defp parse_duration_year(<>, duration, buffer) - when c in ?0..?9 or c in [?., ?-] do - parse_duration_year(rest, duration, <>) + defp parse_duration(:period, _rest, _acc) do + {:error, :invalid_duration} end - defp parse_duration_year(<<"Y", rest::binary>>, duration, year) do - duration = Map.put(duration, :year, year) - {:ok, duration, rest, ""} - end - - defp parse_duration_year(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end - - defp parse_duration_month(<>, duration, buffer) - when c in ?0..?9 or c in [?., ?-] do - parse_duration_month(rest, duration, <>) - end - - defp parse_duration_month(<<"M", rest::binary>>, duration, month) do - duration = Map.put(duration, :month, month) - {:ok, duration, rest, ""} - end - - defp parse_duration_month(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end - - defp parse_duration_week(<>, duration, buffer) - when c in ?0..?9 or c in [?., ?-] do - parse_duration_week(rest, duration, <>) - end - - defp parse_duration_week(<<"W", rest::binary>>, duration, week) do - duration = Map.put(duration, :week, week) - {:ok, duration, rest, ""} - end - - defp parse_duration_week(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end - - defp parse_duration_day(<>, duration, buffer) - when c in ?0..?9 or c in [?., ?-] do - parse_duration_day(rest, duration, <>) - end - - defp parse_duration_day(<<"D", rest::binary>>, duration, day) do - duration = Map.put(duration, :day, day) - {:ok, duration, rest, ""} - end - - defp parse_duration_day(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end - - defp parse_duration_time(<<"T", rest::binary>>, duration, "") do - duration = Map.put(duration, :time, true) - {:ok, duration, rest, ""} - end - - defp parse_duration_time(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end - - defp parse_duration_hour(<>, duration, buffer) - when c in ?0..?9 or c in [?., ?-] do - parse_duration_hour(rest, duration, <>) - end - - defp parse_duration_hour(<<"H", rest::binary>>, %{time: true} = duration, hour) do - duration = Map.put(duration, :hour, hour) - {:ok, duration, rest, ""} - end - - defp parse_duration_hour(<<"H", rest::binary>>, duration, hour) do - {:error, :missing_time_part} - end - - defp parse_duration_hour(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end - - defp parse_duration_minute(<>, duration, buffer) - when c in ?0..?9 or c in [?., ?-] do - parse_duration_minute(rest, duration, <>) - end - - defp parse_duration_minute(<<"M", rest::binary>>, %{time: true} = duration, minute) do - duration = Map.put(duration, :minute, minute) - {:ok, duration, rest, ""} - end + for {part, designator} <- [year: "Y", month: "M", day: "D", week: "W"] do + defp parse_duration(unquote(part), <>, acc, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_duration(unquote(part), rest, acc, <>) + end - defp parse_duration_minute(<<"M", rest::binary>>, duration, minute) do - {:error, :missing_time_part} - end + defp parse_duration(unquote(part), <>, acc, "-" <> buffer) do + with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer, -1) do + {:ok, acc, rest, ""} + end + end - defp parse_duration_minute(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end + defp parse_duration(unquote(part), <>, acc, buffer) do + with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer) do + {:ok, acc, rest, ""} + end + end - defp parse_duration_second(<>, duration, buffer) - when c in ?0..?9 or c in [?., ?-] do - parse_duration_second(rest, duration, <>) + defp parse_duration(unquote(part), rest, acc, buffer) do + {:ok, acc, rest, buffer} + end end - defp parse_duration_second(<<"S", rest::binary>>, %{time: true} = duration, second) do - duration = Map.put(duration, :second, second) - {:ok, duration, rest, ""} + defp parse_duration(:time, <<"T", rest::binary>>, acc, "") do + {:ok, Map.put(acc, :time, true), rest, ""} end - defp parse_duration_second(<<"S", rest::binary>>, duration, second) do - {:error, :missing_time_part} + defp parse_duration(:time, rest, acc, buffer) do + {:ok, acc, rest, buffer} end - defp parse_duration_second(rest, duration, buffer) do - {:ok, duration, rest, buffer} - end + for {part, designator} <- [hour: "H", minute: "M", second: "S"] do + defp parse_duration(unquote(part), <>, acc, buffer) + when c in ?0..?9 or c in [?., ?-] do + parse_duration(unquote(part), rest, acc, <>) + end - defp parse_buffer_duration(:second, duration, "-" <> buffer) do - parse_second_duration(duration, buffer, -1) - end + defp parse_duration( + unquote(part), + <>, + %{time: true} = acc, + "-" <> buffer + ) do + with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer, -1) do + {:ok, acc, rest, ""} + end + end - defp parse_buffer_duration(:second, duration, buffer) do - parse_second_duration(duration, buffer, 1) - end + defp parse_duration( + unquote(part), + <>, + %{time: true} = acc, + buffer + ) do + with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer) do + {:ok, acc, rest, ""} + end + end - defp parse_buffer_duration(unit, duration, buffer) do - case Integer.parse(buffer) do - {duration_value, ""} -> - Map.put(duration, unit, duration_value) + defp parse_duration(unquote(part), <>, _acc, _buffer) do + {:error, :invalid_duration} + end - _ -> - {:error, :invalid_unit_value} + defp parse_duration(unquote(part), rest, acc, buffer) do + {:ok, acc, rest, buffer} end end - defp parse_second_duration(duration, buffer, multiplier) do + defp parse_duration_buffer(unit, acc, buffer, multiplier \\ 1) + + defp parse_duration_buffer(:second, acc, buffer, multiplier) do case parse_fraction_duration(buffer, "") do {second, ".0"} -> - Map.put(duration, :second, multiplier * second) + {:ok, Map.put(acc, :second, multiplier * second)} {second, microsecond} -> case parse_microsecond(microsecond) do {{ms, precision}, ""} -> - duration - |> Map.put(:second, multiplier * second) - |> Map.put(:microsecond, {multiplier * ms, precision}) + {:ok, + acc + |> Map.put(:second, multiplier * second) + |> Map.put(:microsecond, {multiplier * ms, precision})} _ -> - {:error, :invalid_unit_value} + {:error, :invalid_duration} end error -> @@ -855,6 +793,16 @@ defmodule Calendar.ISO do end end + defp parse_duration_buffer(unit, acc, buffer, multiplier) do + case Integer.parse(buffer) do + {value, ""} -> + {:ok, Map.put(acc, unit, multiplier * value)} + + _ -> + {:error, :invalid_unit_value} + end + end + defp parse_fraction_duration(<<>>, ""), do: {0, ".0"} defp parse_fraction_duration(<<>>, second), do: {String.to_integer(second), ".0"} diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index d5b0116334f..7fbec4f232d 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -230,9 +230,10 @@ defmodule DurationTest do assert Duration.from_iso8601("P3D") == {:ok, %Duration{day: 3}} assert Duration.from_iso8601("PT4H5M") == {:ok, %Duration{hour: 4, minute: 5}} assert Duration.from_iso8601("PT6S") == {:ok, %Duration{second: 6}} - assert Duration.from_iso8601("P4Y2W3Y") == {:error, :duplicate_unit} - assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_character} - assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_character} + assert Duration.from_iso8601("P2M4Y") == {:error, :invalid_duration} + assert Duration.from_iso8601("P4Y2W3Y") == {:error, :invalid_duration} + assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_duration} + assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_duration} assert Duration.from_iso8601("invalid") == {:error, :invalid_duration} end @@ -277,13 +278,13 @@ defmodule DurationTest do assert Duration.from_iso8601!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 2}} assert_raise ArgumentError, - ~s/failed to parse duration "P5H3HT4M". reason: :invalid_character/, + ~s/failed to parse duration "P5H3HT4M". reason: :invalid_duration/, fn -> Duration.from_iso8601!("P5H3HT4M") end assert_raise ArgumentError, - ~s/failed to parse duration "P4Y2W3Y". reason: :duplicate_unit/, + ~s/failed to parse duration "P4Y2W3Y". reason: :invalid_duration/, fn -> Duration.from_iso8601!("P4Y2W3Y") end From 3e65befcb88f6fae97b227b0e3795bddef6d9bc9 Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Sun, 26 May 2024 22:41:45 +0200 Subject: [PATCH 18/20] docs and tests --- lib/elixir/lib/calendar/duration.ex | 6 +++++- lib/elixir/lib/calendar/iso.ex | 12 +++++++----- lib/elixir/test/elixir/calendar/duration_test.exs | 4 +++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 8a750ecf478..d764724cd98 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -199,7 +199,9 @@ defmodule Duration do end @doc """ - Parses an ISO 8601-2 formatted duration string to a `Duration` struct. + Parses an ISO 8601 formatted duration string to a `Duration` struct. + + A decimal fraction may be specified for seconds only, using either a comma or a full stop. ## Examples @@ -209,6 +211,8 @@ defmodule Duration do {:ok, %Duration{hour: 10, minute: 30, second: 0}} iex> Duration.from_iso8601("P3Y-2MT3H") {:ok, %Duration{year: 3, month: -2, hour: 3}} + iex> Duration.from_iso8601("P1YT4.650S") + {:ok, %Duration{year: 1, second: 4, microsecond: {650000, 3}}} """ @spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom} diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 38795219407..42165ee1402 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -667,6 +667,8 @@ defmodule Calendar.ISO do @doc """ Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs. + See `Duration.from_iso8601/1`. + """ @doc since: "1.17.0" @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()} @@ -703,7 +705,7 @@ defmodule Calendar.ISO do for {part, designator} <- [year: "Y", month: "M", day: "D", week: "W"] do defp parse_duration(unquote(part), <>, acc, buffer) - when c in ?0..?9 or c in [?., ?-] do + when c in ?0..?9 or c in [?,, ?., ?-] do parse_duration(unquote(part), rest, acc, <>) end @@ -734,7 +736,7 @@ defmodule Calendar.ISO do for {part, designator} <- [hour: "H", minute: "M", second: "S"] do defp parse_duration(unquote(part), <>, acc, buffer) - when c in ?0..?9 or c in [?., ?-] do + when c in ?0..?9 or c in [?,, ?., ?-] do parse_duration(unquote(part), rest, acc, <>) end @@ -773,7 +775,7 @@ defmodule Calendar.ISO do defp parse_duration_buffer(:second, acc, buffer, multiplier) do case parse_fraction_duration(buffer, "") do - {second, ".0"} -> + {second, ""} -> {:ok, Map.put(acc, :second, multiplier * second)} {second, microsecond} -> @@ -803,8 +805,8 @@ defmodule Calendar.ISO do end end - defp parse_fraction_duration(<<>>, ""), do: {0, ".0"} - defp parse_fraction_duration(<<>>, second), do: {String.to_integer(second), ".0"} + defp parse_fraction_duration(<<>>, ""), do: {0, ""} + defp parse_fraction_duration(<<>>, second), do: {String.to_integer(second), ""} defp parse_fraction_duration(<>, second) when c in ?0..?9 do parse_fraction_duration(rest, <>) diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 7fbec4f232d..930d03df276 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -255,7 +255,9 @@ defmodule DurationTest do assert Duration.from_iso8601!("P3D") == %Duration{day: 3} assert Duration.from_iso8601!("PT4H5M") == %Duration{hour: 4, minute: 5} assert Duration.from_iso8601!("PT6S") == %Duration{second: 6} - assert Duration.from_iso8601!("PT1.6S") == %Duration{second: 1, microsecond: {600_000, 1}} + assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}} + assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}} + assert Duration.from_iso8601!("PT.6S") == %Duration{second: 0, microsecond: {600_000, 1}} assert Duration.from_iso8601!("PT-.6S") == %Duration{second: 0, microsecond: {-600_000, 1}} assert Duration.from_iso8601!("PT-1.234567S") == %Duration{ From 6b8e4d2fe5b52fac830aaf041d2e7813ee9b763c Mon Sep 17 00:00:00 2001 From: Theodor Fiedler Date: Mon, 27 May 2024 09:27:50 +0200 Subject: [PATCH 19/20] Joses parse_duration/1 --- lib/elixir/lib/calendar/iso.ex | 161 ++++-------------- .../test/elixir/calendar/duration_test.exs | 18 +- 2 files changed, 39 insertions(+), 140 deletions(-) diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 42165ee1402..aa9a31a437d 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -668,161 +668,60 @@ defmodule Calendar.ISO do Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs. See `Duration.from_iso8601/1`. - """ @doc since: "1.17.0" @spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()} - def parse_duration(string) do - with {:ok, acc, rest, buffer} <- parse_duration(:period, string, %{time: false}), - {:ok, acc, rest, buffer} <- parse_duration(:year, rest, acc, buffer), - {:ok, acc, rest, buffer} <- parse_duration(:month, rest, acc, buffer), - {:ok, acc, rest, buffer} <- parse_duration(:week, rest, acc, buffer), - {:ok, acc, rest, buffer} <- parse_duration(:day, rest, acc, buffer), - {:ok, acc, rest, buffer} <- parse_duration(:time, rest, acc, buffer), - {:ok, acc, rest, buffer} <- parse_duration(:hour, rest, acc, buffer), - {:ok, acc, rest, buffer} <- parse_duration(:minute, rest, acc, buffer), - {:ok, acc, "", ""} <- parse_duration(:second, rest, acc, buffer) do - {:ok, - acc - |> Map.take([:year, :month, :week, :day, :hour, :minute, :second, :microsecond]) - |> Keyword.new()} - else - {:error, error} -> - {:error, error} - - _ -> - {:error, :invalid_duration} - end + def parse_duration("P" <> string) when byte_size(string) > 0 do + parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) end - defp parse_duration(:period, <<"P", rest::binary>>, acc) do - {:ok, acc, rest, ""} - end - - defp parse_duration(:period, _rest, _acc) do + def parse_duration(_) do {:error, :invalid_duration} end - for {part, designator} <- [year: "Y", month: "M", day: "D", week: "W"] do - defp parse_duration(unquote(part), <>, acc, buffer) - when c in ?0..?9 or c in [?,, ?., ?-] do - parse_duration(unquote(part), rest, acc, <>) - end - - defp parse_duration(unquote(part), <>, acc, "-" <> buffer) do - with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer, -1) do - {:ok, acc, rest, ""} - end - end - - defp parse_duration(unquote(part), <>, acc, buffer) do - with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer) do - {:ok, acc, rest, ""} - end - end - - defp parse_duration(unquote(part), rest, acc, buffer) do - {:ok, acc, rest, buffer} - end - end - - defp parse_duration(:time, <<"T", rest::binary>>, acc, "") do - {:ok, Map.put(acc, :time, true), rest, ""} - end + defp parse_duration_date("", acc, _allowed), do: {:ok, acc} - defp parse_duration(:time, rest, acc, buffer) do - {:ok, acc, rest, buffer} + defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do + parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S) end - for {part, designator} <- [hour: "H", minute: "M", second: "S"] do - defp parse_duration(unquote(part), <>, acc, buffer) - when c in ?0..?9 or c in [?,, ?., ?-] do - parse_duration(unquote(part), rest, acc, <>) - end - - defp parse_duration( - unquote(part), - <>, - %{time: true} = acc, - "-" <> buffer - ) do - with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer, -1) do - {:ok, acc, rest, ""} - end - end - - defp parse_duration( - unquote(part), - <>, - %{time: true} = acc, - buffer - ) do - with {:ok, acc} <- parse_duration_buffer(unquote(part), acc, buffer) do - {:ok, acc, rest, ""} - end - end - - defp parse_duration(unquote(part), <>, _acc, _buffer) do - {:error, :invalid_duration} - end - - defp parse_duration(unquote(part), rest, acc, buffer) do - {:ok, acc, rest, buffer} + defp parse_duration_date(string, acc, allowed) do + with {integer, <>} <- Integer.parse(string), + {key, allowed} <- find_unit(allowed, next) do + parse_duration_date(rest, [{key, integer} | acc], allowed) + else + _ -> {:error, :invalid_date_component} end end - defp parse_duration_buffer(unit, acc, buffer, multiplier \\ 1) - - defp parse_duration_buffer(:second, acc, buffer, multiplier) do - case parse_fraction_duration(buffer, "") do - {second, ""} -> - {:ok, Map.put(acc, :second, multiplier * second)} + defp parse_duration_time("", acc, _allowed), do: {:ok, acc} - {second, microsecond} -> - case parse_microsecond(microsecond) do - {{ms, precision}, ""} -> - {:ok, - acc - |> Map.put(:second, multiplier * second) - |> Map.put(:microsecond, {multiplier * ms, precision})} + defp parse_duration_time(string, acc, allowed) do + case Integer.parse(string) do + {second, <> = rest} when delimiter in [?., ?,] -> + case parse_microsecond(rest) do + {{ms, precision}, "S"} -> + ms = if second > 0, do: ms, else: -ms + {:ok, [second: second, microsecond: {ms, precision}] ++ acc} _ -> - {:error, :invalid_duration} + {:error, :invalid_time_component} end - error -> - error - end - end - - defp parse_duration_buffer(unit, acc, buffer, multiplier) do - case Integer.parse(buffer) do - {value, ""} -> - {:ok, Map.put(acc, unit, multiplier * value)} + {integer, <>} -> + case find_unit(allowed, next) do + {key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed) + false -> {:error, :invalid_time_component} + end _ -> - {:error, :invalid_unit_value} + {:error, :invalid_time_component} end end - defp parse_fraction_duration(<<>>, ""), do: {0, ""} - defp parse_fraction_duration(<<>>, second), do: {String.to_integer(second), ""} - - defp parse_fraction_duration(<>, second) when c in ?0..?9 do - parse_fraction_duration(rest, <>) - end - - defp parse_fraction_duration(<>, "") when c in [?., ?,] do - {0, <>} - end - - defp parse_fraction_duration(<>, second) when c in [?., ?,] do - {String.to_integer(second), <>} - end - - defp parse_fraction_duration(_, _) do - {:error, :invalid_unit_value} - end + defp find_unit([{key, unit} | rest], unit), do: {key, rest} + defp find_unit([_ | rest], unit), do: find_unit(rest, unit) + defp find_unit([], _unit), do: false @doc """ Returns the `t:Calendar.iso_days/0` format of the specified date. diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index 930d03df276..71635e91644 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -230,10 +230,12 @@ defmodule DurationTest do assert Duration.from_iso8601("P3D") == {:ok, %Duration{day: 3}} assert Duration.from_iso8601("PT4H5M") == {:ok, %Duration{hour: 4, minute: 5}} assert Duration.from_iso8601("PT6S") == {:ok, %Duration{second: 6}} - assert Duration.from_iso8601("P2M4Y") == {:error, :invalid_duration} - assert Duration.from_iso8601("P4Y2W3Y") == {:error, :invalid_duration} - assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_duration} - assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_duration} + assert Duration.from_iso8601("P2M4Y") == {:error, :invalid_date_component} + assert Duration.from_iso8601("P4Y2W3Y") == {:error, :invalid_date_component} + assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_date_component} + assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_date_component} + assert Duration.from_iso8601("PT1D") == {:error, :invalid_time_component} + assert Duration.from_iso8601("PT.6S") == {:error, :invalid_time_component} assert Duration.from_iso8601("invalid") == {:error, :invalid_duration} end @@ -257,8 +259,6 @@ defmodule DurationTest do assert Duration.from_iso8601!("PT6S") == %Duration{second: 6} assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}} assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}} - assert Duration.from_iso8601!("PT.6S") == %Duration{second: 0, microsecond: {600_000, 1}} - assert Duration.from_iso8601!("PT-.6S") == %Duration{second: 0, microsecond: {-600_000, 1}} assert Duration.from_iso8601!("PT-1.234567S") == %Duration{ second: -1, @@ -280,13 +280,13 @@ defmodule DurationTest do assert Duration.from_iso8601!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 2}} assert_raise ArgumentError, - ~s/failed to parse duration "P5H3HT4M". reason: :invalid_duration/, + ~s/failed to parse duration "P5H3HT4M". reason: :invalid_date_component/, fn -> Duration.from_iso8601!("P5H3HT4M") end assert_raise ArgumentError, - ~s/failed to parse duration "P4Y2W3Y". reason: :invalid_duration/, + ~s/failed to parse duration "P4Y2W3Y". reason: :invalid_date_component/, fn -> Duration.from_iso8601!("P4Y2W3Y") end @@ -298,7 +298,7 @@ defmodule DurationTest do end assert_raise ArgumentError, - ~s/failed to parse duration "P4.5YT6S". reason: :invalid_unit_value/, + ~s/failed to parse duration "P4.5YT6S". reason: :invalid_date_component/, fn -> Duration.from_iso8601!("P4.5YT6S") end From 53c9b44fdef4ee1cd137f2c742158bdfd28160f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 27 May 2024 11:46:53 +0200 Subject: [PATCH 20/20] Update lib/elixir/lib/calendar/duration.ex --- 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 d764724cd98..4b4502839d3 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -227,7 +227,7 @@ defmodule Duration do end @doc """ - Same as from_iso8601/1 but raises an ArgumentError. + Same as `from_iso8601/1` but raises an ArgumentError. ## Examples