Implement Duration.to_iso8601/1 (#13604)
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user