Implement Duration.to_iso8601/1 (#13604)

This commit is contained in:
Tangui
2024-05-28 23:52:17 +02:00
committed by GitHub
parent 29fb214f14
commit fcc7ea20ef
2 changed files with 136 additions and 0 deletions
+95
View File
@@ -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
@@ -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