@@ -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