@@ -124,6 +124,8 @@ defmodule Duration do
124
124
"""
125
125
@ type duration :: t | [ unit_pair ]
126
126
127
+ @ microseconds_per_second 1_000_000
128
+
127
129
@ doc """
128
130
Creates a new `Duration` struct from given `unit_pairs`.
129
131
@@ -342,4 +344,97 @@ defmodule Duration do
342
344
raise ArgumentError , ~s/ failed to parse duration "#{ string } ". reason: #{ inspect ( reason ) } /
343
345
end
344
346
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
345
440
end
0 commit comments