From fcc7ea20efc9479be8dacbedd0ca3f4be2ffd97e Mon Sep 17 00:00:00 2001 From: Tangui <29804907+tanguilp@users.noreply.github.com> Date: Wed, 29 May 2024 00:52:17 +0300 Subject: [PATCH] Implement `Duration.to_iso8601/1` (#13604) --- lib/elixir/lib/calendar/duration.ex | 95 +++++++++++++++++++ .../test/elixir/calendar/duration_test.exs | 41 ++++++++ 2 files changed, 136 insertions(+) diff --git a/lib/elixir/lib/calendar/duration.ex b/lib/elixir/lib/calendar/duration.ex index 8b523ae1be..29fa77e01c 100644 --- a/lib/elixir/lib/calendar/duration.ex +++ b/lib/elixir/lib/calendar/duration.ex @@ -124,6 +124,8 @@ defmodule Duration do """ @type duration :: t | [unit_pair] + @microseconds_per_second 1_000_000 + @doc """ Creates a new `Duration` struct from given `unit_pairs`. @@ -342,4 +344,97 @@ defmodule Duration do raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/ end end + + @doc """ + Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string. + + Note this function implements the *extension* of ISO 8601:2019. This extensions allows weeks to + appear between months and days: `P3M3W3D`, making it fully compatible with any `Duration` struct. + + ## Examples + + iex> Duration.to_iso8601(%Duration{year: 3}) + "P3Y" + iex> Duration.to_iso8601(%Duration{day: 40, hour: 12, minute: 42, second: 12}) + "P40DT12H42M12S" + iex> Duration.to_iso8601(%Duration{second: 30}) + "PT30S" + + iex> Duration.to_iso8601(%Duration{}) + "PT0S" + + iex> Duration.to_iso8601(%Duration{second: 1, microsecond: {2_200, 3}}) + "PT1.002S" + iex> Duration.to_iso8601(%Duration{second: 1, microsecond: {-1_200_000, 4}}) + "PT-0.2000S" + """ + + @spec to_iso8601(t) :: String.t() + def to_iso8601(duration) + + def to_iso8601(%Duration{ + year: 0, + month: 0, + week: 0, + day: 0, + hour: 0, + minute: 0, + second: 0, + microsecond: {0, _} + }) do + "PT0S" + end + + def to_iso8601(%Duration{} = d) do + IO.iodata_to_binary([?P, to_iso8601_duration_date(d), to_iso8601_duration_time(d)]) + end + + defp to_iso8601_duration_date(d) do + [ + if(d.year == 0, do: [], else: [Integer.to_string(d.year), ?Y]), + if(d.month == 0, do: [], else: [Integer.to_string(d.month), ?M]), + if(d.week == 0, do: [], else: [Integer.to_string(d.week), ?W]), + if(d.day == 0, do: [], else: [Integer.to_string(d.day), ?D]) + ] + end + + defp to_iso8601_duration_time(%Duration{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do + [] + end + + defp to_iso8601_duration_time(d) do + [ + ?T, + if(d.hour == 0, do: [], else: [Integer.to_string(d.hour), ?H]), + if(d.minute == 0, do: [], else: [Integer.to_string(d.minute), ?M]), + second_component(d) + ] + end + + defp second_component(%Duration{second: 0, microsecond: {0, _}}) do + [] + end + + defp second_component(%Duration{second: 0, microsecond: {_, 0}}) do + ~c"0S" + end + + defp second_component(%Duration{microsecond: {_, 0}} = d) do + [Integer.to_string(d.second), ?S] + end + + defp second_component(%Duration{microsecond: {ms, p}} = d) do + total_ms = d.second * @microseconds_per_second + ms + second = total_ms |> div(@microseconds_per_second) |> abs() + ms = total_ms |> rem(@microseconds_per_second) |> abs() + sign = if total_ms < 0, do: ?-, else: [] + + [ + sign, + Integer.to_string(second), + ?., + ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p), + ?S + ] + end end diff --git a/lib/elixir/test/elixir/calendar/duration_test.exs b/lib/elixir/test/elixir/calendar/duration_test.exs index ed6a23f27f..f3dccf3447 100644 --- a/lib/elixir/test/elixir/calendar/duration_test.exs +++ b/lib/elixir/test/elixir/calendar/duration_test.exs @@ -312,4 +312,45 @@ defmodule DurationTest do Duration.from_iso8601!("P4.5YT6S") end end + + test "to_iso8601/1" do + assert %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6} + |> Duration.to_iso8601() == "P1Y2M3DT4H5M6S" + + assert %Duration{week: 3, hour: 5, minute: 3} |> Duration.to_iso8601() == "P3WT5H3M" + assert %Duration{hour: 5, minute: 3} |> Duration.to_iso8601() == "PT5H3M" + assert %Duration{year: 1, month: 2, day: 3} |> Duration.to_iso8601() == "P1Y2M3D" + assert %Duration{hour: 4, minute: 5, second: 6} |> Duration.to_iso8601() == "PT4H5M6S" + assert %Duration{year: 1, month: 2} |> Duration.to_iso8601() == "P1Y2M" + assert %Duration{day: 3} |> Duration.to_iso8601() == "P3D" + assert %Duration{hour: 4, minute: 5} |> Duration.to_iso8601() == "PT4H5M" + assert %Duration{second: 6} |> Duration.to_iso8601() == "PT6S" + assert %Duration{second: 1, microsecond: {600_000, 1}} |> Duration.to_iso8601() == "PT1.6S" + assert %Duration{second: -1, microsecond: {-600_000, 1}} |> Duration.to_iso8601() == "PT-1.6S" + + assert %Duration{second: -1, microsecond: {-234_567, 6}} |> Duration.to_iso8601() == + "PT-1.234567S" + + assert %Duration{second: 1, microsecond: {123_456, 6}} |> Duration.to_iso8601() == + "PT1.123456S" + + assert %Duration{year: 3, week: 4, day: -3, second: -6} |> Duration.to_iso8601() == + "P3Y4W-3DT-6S" + + assert %Duration{second: -4, microsecond: {-230_000, 2}} |> Duration.to_iso8601() == + "PT-4.23S" + + assert %Duration{second: -4, microsecond: {230_000, 2}} |> Duration.to_iso8601() == + "PT-3.77S" + + assert %Duration{second: 2, microsecond: {-1_200_000, 4}} |> Duration.to_iso8601() == + "PT0.8000S" + + assert %Duration{second: 1, microsecond: {-1_200_000, 3}} |> Duration.to_iso8601() == + "PT-0.200S" + + assert %Duration{microsecond: {-800_000, 2}} |> Duration.to_iso8601() == "PT-0.80S" + assert %Duration{microsecond: {-800_000, 0}} |> Duration.to_iso8601() == "PT0S" + assert %Duration{microsecond: {-1_200_000, 2}} |> Duration.to_iso8601() == "PT-1.20S" + end end