@@ -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
345440end
0 commit comments