Skip to content

Commit f9939a7

Browse files
tanguilpjosevalim
authored andcommitted
Implement Duration.to_iso8601/1 (#13604)
1 parent 0d6ae23 commit f9939a7

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed

lib/elixir/lib/calendar/duration.ex

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ defmodule Duration do
124124
"""
125125
@type duration :: t | [unit_pair]
126126

127+
@microseconds_per_second 1_000_000
128+
127129
@doc """
128130
Creates a new `Duration` struct from given `unit_pairs`.
129131
@@ -342,4 +344,97 @@ defmodule Duration do
342344
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
343345
end
344346
end
347+
348+
@doc """
349+
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
350+
351+
Note this function implements the *extension* of ISO 8601:2019. This extensions allows weeks to
352+
appear between months and days: `P3M3W3D`, making it fully compatible with any `Duration` struct.
353+
354+
## Examples
355+
356+
iex> Duration.to_iso8601(%Duration{year: 3})
357+
"P3Y"
358+
iex> Duration.to_iso8601(%Duration{day: 40, hour: 12, minute: 42, second: 12})
359+
"P40DT12H42M12S"
360+
iex> Duration.to_iso8601(%Duration{second: 30})
361+
"PT30S"
362+
363+
iex> Duration.to_iso8601(%Duration{})
364+
"PT0S"
365+
366+
iex> Duration.to_iso8601(%Duration{second: 1, microsecond: {2_200, 3}})
367+
"PT1.002S"
368+
iex> Duration.to_iso8601(%Duration{second: 1, microsecond: {-1_200_000, 4}})
369+
"PT-0.2000S"
370+
"""
371+
372+
@spec to_iso8601(t) :: String.t()
373+
def to_iso8601(duration)
374+
375+
def to_iso8601(%Duration{
376+
year: 0,
377+
month: 0,
378+
week: 0,
379+
day: 0,
380+
hour: 0,
381+
minute: 0,
382+
second: 0,
383+
microsecond: {0, _}
384+
}) do
385+
"PT0S"
386+
end
387+
388+
def to_iso8601(%Duration{} = d) do
389+
IO.iodata_to_binary([?P, to_iso8601_duration_date(d), to_iso8601_duration_time(d)])
390+
end
391+
392+
defp to_iso8601_duration_date(d) do
393+
[
394+
if(d.year == 0, do: [], else: [Integer.to_string(d.year), ?Y]),
395+
if(d.month == 0, do: [], else: [Integer.to_string(d.month), ?M]),
396+
if(d.week == 0, do: [], else: [Integer.to_string(d.week), ?W]),
397+
if(d.day == 0, do: [], else: [Integer.to_string(d.day), ?D])
398+
]
399+
end
400+
401+
defp to_iso8601_duration_time(%Duration{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do
402+
[]
403+
end
404+
405+
defp to_iso8601_duration_time(d) do
406+
[
407+
?T,
408+
if(d.hour == 0, do: [], else: [Integer.to_string(d.hour), ?H]),
409+
if(d.minute == 0, do: [], else: [Integer.to_string(d.minute), ?M]),
410+
second_component(d)
411+
]
412+
end
413+
414+
defp second_component(%Duration{second: 0, microsecond: {0, _}}) do
415+
[]
416+
end
417+
418+
defp second_component(%Duration{second: 0, microsecond: {_, 0}}) do
419+
~c"0S"
420+
end
421+
422+
defp second_component(%Duration{microsecond: {_, 0}} = d) do
423+
[Integer.to_string(d.second), ?S]
424+
end
425+
426+
defp second_component(%Duration{microsecond: {ms, p}} = d) do
427+
total_ms = d.second * @microseconds_per_second + ms
428+
second = total_ms |> div(@microseconds_per_second) |> abs()
429+
ms = total_ms |> rem(@microseconds_per_second) |> abs()
430+
sign = if total_ms < 0, do: ?-, else: []
431+
432+
[
433+
sign,
434+
Integer.to_string(second),
435+
?.,
436+
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p),
437+
?S
438+
]
439+
end
345440
end

lib/elixir/test/elixir/calendar/duration_test.exs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,45 @@ defmodule DurationTest do
312312
Duration.from_iso8601!("P4.5YT6S")
313313
end
314314
end
315+
316+
test "to_iso8601/1" do
317+
assert %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
318+
|> Duration.to_iso8601() == "P1Y2M3DT4H5M6S"
319+
320+
assert %Duration{week: 3, hour: 5, minute: 3} |> Duration.to_iso8601() == "P3WT5H3M"
321+
assert %Duration{hour: 5, minute: 3} |> Duration.to_iso8601() == "PT5H3M"
322+
assert %Duration{year: 1, month: 2, day: 3} |> Duration.to_iso8601() == "P1Y2M3D"
323+
assert %Duration{hour: 4, minute: 5, second: 6} |> Duration.to_iso8601() == "PT4H5M6S"
324+
assert %Duration{year: 1, month: 2} |> Duration.to_iso8601() == "P1Y2M"
325+
assert %Duration{day: 3} |> Duration.to_iso8601() == "P3D"
326+
assert %Duration{hour: 4, minute: 5} |> Duration.to_iso8601() == "PT4H5M"
327+
assert %Duration{second: 6} |> Duration.to_iso8601() == "PT6S"
328+
assert %Duration{second: 1, microsecond: {600_000, 1}} |> Duration.to_iso8601() == "PT1.6S"
329+
assert %Duration{second: -1, microsecond: {-600_000, 1}} |> Duration.to_iso8601() == "PT-1.6S"
330+
331+
assert %Duration{second: -1, microsecond: {-234_567, 6}} |> Duration.to_iso8601() ==
332+
"PT-1.234567S"
333+
334+
assert %Duration{second: 1, microsecond: {123_456, 6}} |> Duration.to_iso8601() ==
335+
"PT1.123456S"
336+
337+
assert %Duration{year: 3, week: 4, day: -3, second: -6} |> Duration.to_iso8601() ==
338+
"P3Y4W-3DT-6S"
339+
340+
assert %Duration{second: -4, microsecond: {-230_000, 2}} |> Duration.to_iso8601() ==
341+
"PT-4.23S"
342+
343+
assert %Duration{second: -4, microsecond: {230_000, 2}} |> Duration.to_iso8601() ==
344+
"PT-3.77S"
345+
346+
assert %Duration{second: 2, microsecond: {-1_200_000, 4}} |> Duration.to_iso8601() ==
347+
"PT0.8000S"
348+
349+
assert %Duration{second: 1, microsecond: {-1_200_000, 3}} |> Duration.to_iso8601() ==
350+
"PT-0.200S"
351+
352+
assert %Duration{microsecond: {-800_000, 2}} |> Duration.to_iso8601() == "PT-0.80S"
353+
assert %Duration{microsecond: {-800_000, 0}} |> Duration.to_iso8601() == "PT0S"
354+
assert %Duration{microsecond: {-1_200_000, 2}} |> Duration.to_iso8601() == "PT-1.20S"
355+
end
315356
end

0 commit comments

Comments
 (0)