diff --git a/lib/elixir/lib/calendar/iso.ex b/lib/elixir/lib/calendar/iso.ex index 69cd1d3920..5e5e53444b 100644 --- a/lib/elixir/lib/calendar/iso.ex +++ b/lib/elixir/lib/calendar/iso.ex @@ -696,48 +696,43 @@ 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) when byte_size(string) > 0 do - parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) + parse_duration_date(string, 1, [], 0) end def parse_duration("+P" <> string) when byte_size(string) > 0 do - parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) + parse_duration_date(string, 1, [], 0) end def parse_duration("-P" <> string) when byte_size(string) > 0 do - with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do - {:ok, - Enum.map(fields, fn - {:microsecond, {value, precision}} -> {:microsecond, {-value, precision}} - {unit, value} -> {unit, -value} - end)} - end + parse_duration_date(string, -1, [], 0) end def parse_duration(_) do {:error, :invalid_duration} end - defp parse_duration_date("", acc, _allowed), do: {:ok, acc} + defp parse_duration_date("", _sign, acc, _min_position), do: {:ok, acc} - 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) + defp parse_duration_date("T" <> string, sign, acc, _min_position) + when byte_size(string) > 0 do + parse_duration_time(string, sign, acc, 0) end - 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) + defp parse_duration_date(string, sign, acc, min_position) do + with {integer, <>} <- Integer.parse(string), + {key, next_min_position} <- find_date_unit(min_position, unit) do + parse_duration_date(rest, sign, [{key, integer * sign} | acc], next_min_position) else _ -> {:error, :invalid_date_component} end end - defp parse_duration_time("", acc, _allowed), do: {:ok, acc} + defp parse_duration_time("", _sign, acc, _min_position), do: {:ok, acc} - defp parse_duration_time(string, acc, allowed) do + defp parse_duration_time(string, sign, acc, min_position) do case Integer.parse(string) do {second, <> = rest} when delimiter in [?., ?,] -> - with {:second, _allowed} <- find_unit(allowed, ?S), + with {:second, _next_min_position} <- find_time_unit(min_position, ?S), {{ms, precision}, "S"} <- parse_microsecond(rest) do ms = case string do @@ -748,16 +743,19 @@ defmodule Calendar.ISO do ms end - {:ok, [second: second, microsecond: {ms, precision}] ++ acc} + {:ok, [second: second * sign, microsecond: {ms * sign, precision}] ++ acc} else _ -> {:error, :invalid_time_component} end - {integer, <>} -> - case find_unit(allowed, next) do - {key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed) - false -> {:error, :invalid_time_component} + {integer, <>} -> + case find_time_unit(min_position, unit) do + {key, next_min_position} -> + parse_duration_time(rest, sign, [{key, integer * sign} | acc], next_min_position) + + false -> + {:error, :invalid_time_component} end _ -> @@ -765,9 +763,17 @@ defmodule Calendar.ISO do end 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 + # The minimum position is the earliest unit still allowed, or one past the end. + defp find_date_unit(min_position, ?Y) when min_position <= 0, do: {:year, 1} + defp find_date_unit(min_position, ?M) when min_position <= 1, do: {:month, 2} + defp find_date_unit(min_position, ?W) when min_position <= 2, do: {:week, 3} + defp find_date_unit(min_position, ?D) when min_position <= 3, do: {:day, 4} + defp find_date_unit(_min_position, _unit), do: false + + defp find_time_unit(min_position, ?H) when min_position <= 0, do: {:hour, 1} + defp find_time_unit(min_position, ?M) when min_position <= 1, do: {:minute, 2} + defp find_time_unit(min_position, ?S) when min_position <= 2, do: {:second, 3} + defp find_time_unit(_min_position, _unit), do: false @doc """ Returns the `t:Calendar.iso_days/0` format of the specified date. diff --git a/lib/elixir/test/elixir/calendar/iso_test.exs b/lib/elixir/test/elixir/calendar/iso_test.exs index 454ba23ccf..7f956f2238 100644 --- a/lib/elixir/test/elixir/calendar/iso_test.exs +++ b/lib/elixir/test/elixir/calendar/iso_test.exs @@ -125,6 +125,25 @@ defmodule Calendar.ISOTest do Calendar.ISO.date_from_iso_days(iso_days) end + describe "parse_duration/1" do + test "combines duration and component signs" do + assert {:ok, fields} = Calendar.ISO.parse_duration("-P1Y-2MT3H-4.5S") + + assert Map.new(fields) == %{ + year: -1, + month: 2, + hour: -3, + second: 4, + microsecond: {500_000, 1} + } + end + + test "combines duration and negative zero fractional second signs" do + assert Calendar.ISO.parse_duration("-PT-0.6S") == + {:ok, [second: 0, microsecond: {600_000, 1}]} + end + end + describe "parse_date/1" do test "supports both only extended format by default" do assert Calendar.ISO.parse_date("20150123") == {:error, :invalid_format}