@@ -355,6 +355,114 @@ defmodule Duration do
355355 end
356356 end
357357
358+ @ doc """
359+ Converts the given `duration` to a human readable representation.
360+
361+ ## Options
362+
363+ * `:units` - the units to be used alongside each duration component.
364+ The default units follow the ISO 80000-3 standard:
365+
366+ [
367+ year: "a",
368+ month: "mo",
369+ week: "wk",
370+ day: "d",
371+ hour: "h",
372+ minute: "min",
373+ second: "s"
374+ ]
375+
376+ * `:separator` - a string used to separate the distinct components. Defaults to `" "`.
377+
378+ ## Examples
379+
380+ iex> Duration.to_string(Duration.new!(second: 30))
381+ "30s"
382+ iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
383+ "40d 12h 42min 12s"
384+
385+ By default, this function uses ISO 80000-3 units, which uses "a" for years.
386+ But you can customize all units via the units option:
387+
388+ iex> Duration.to_string(Duration.new!(year: 3))
389+ "3a"
390+ iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
391+ "3y"
392+
393+ You may also choose the separator:
394+
395+ iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
396+ "40d, 12h, 42min, 12s"
397+
398+ A duration without components is rendered as "0s":
399+
400+ iex> Duration.to_string(Duration.new!([]))
401+ "0s"
402+
403+ Microseconds are rendered as part of seconds with the appropriate precision:
404+
405+ iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
406+ "1.002s"
407+ iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
408+ "-0.2000s"
409+
410+ """
411+ @ doc since: "1.18.0"
412+ def to_string ( % Duration { } = duration , opts \\ [ ] ) do
413+ units = Keyword . get ( opts , :units , [ ] )
414+ separator = Keyword . get ( opts , :separator , " " )
415+
416+ case to_string_year ( duration , [ ] , units ) do
417+ [ ] ->
418+ "0" <> Keyword . get ( units , :second , "s" )
419+
420+ [ part ] ->
421+ IO . iodata_to_binary ( part )
422+
423+ parts ->
424+ parts |> Enum . reduce ( & [ & 1 , separator | & 2 ] ) |> IO . iodata_to_binary ( )
425+ end
426+ end
427+
428+ defp to_string_part ( 0 , _units , _key , _default , acc ) ,
429+ do: acc
430+
431+ defp to_string_part ( x , units , key , default , acc ) ,
432+ do: [ [ Integer . to_string ( x ) | Keyword . get ( units , key , default ) ] | acc ]
433+
434+ defp to_string_year ( % { year: year } = duration , acc , units ) do
435+ to_string_month ( duration , to_string_part ( year , units , :year , "a" , acc ) , units )
436+ end
437+
438+ defp to_string_month ( % { month: month } = duration , acc , units ) do
439+ to_string_week ( duration , to_string_part ( month , units , :month , "mo" , acc ) , units )
440+ end
441+
442+ defp to_string_week ( % { week: week } = duration , acc , units ) do
443+ to_string_day ( duration , to_string_part ( week , units , :week , "wk" , acc ) , units )
444+ end
445+
446+ defp to_string_day ( % { day: day } = duration , acc , units ) do
447+ to_string_hour ( duration , to_string_part ( day , units , :day , "d" , acc ) , units )
448+ end
449+
450+ defp to_string_hour ( % { hour: hour } = duration , acc , units ) do
451+ to_string_minute ( duration , to_string_part ( hour , units , :hour , "h" , acc ) , units )
452+ end
453+
454+ defp to_string_minute ( % { minute: minute } = duration , acc , units ) do
455+ to_string_second ( duration , to_string_part ( minute , units , :minute , "min" , acc ) , units )
456+ end
457+
458+ defp to_string_second ( % { second: 0 , microsecond: { 0 , _ } } , acc , _units ) do
459+ acc
460+ end
461+
462+ defp to_string_second ( % { second: s , microsecond: { ms , p } } , acc , units ) do
463+ [ [ second_component ( s , ms , p ) | Keyword . get ( units , :second , "s" ) ] | acc ]
464+ end
465+
358466 @ doc """
359467 Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
360468
@@ -406,15 +514,15 @@ defmodule Duration do
406514 [ ]
407515 end
408516
409- defp second_component ( % { second: 0 , microsecond: { _ , 0 } } ) do
410- ~c " 0S "
517+ defp second_component ( % { second: second , microsecond: { ms , p } } ) do
518+ [ second_component ( second , ms , p ) , ?S ]
411519 end
412520
413- defp second_component ( % { second: second , microsecond: { _ , 0 } } ) do
414- [ Integer . to_string ( second ) , ?S ]
521+ defp second_component ( second , _ms , 0 ) do
522+ Integer . to_string ( second )
415523 end
416524
417- defp second_component ( % { second: second , microsecond: { ms , p } } ) do
525+ defp second_component ( second , ms , p ) do
418526 total_ms = second * @ microseconds_per_second + ms
419527 second = total_ms |> div ( @ microseconds_per_second ) |> abs ( )
420528 ms = total_ms |> rem ( @ microseconds_per_second ) |> abs ( )
@@ -424,8 +532,7 @@ defmodule Duration do
424532 sign ,
425533 Integer . to_string ( second ) ,
426534 ?. ,
427- ms |> Integer . to_string ( ) |> String . pad_leading ( 6 , "0" ) |> binary_part ( 0 , p ) ,
428- ?S
535+ ms |> Integer . to_string ( ) |> String . pad_leading ( 6 , "0" ) |> binary_part ( 0 , p )
429536 ]
430537 end
431538
0 commit comments