Skip to content

Commit a7c5c26

Browse files
committed
Follow ISO 80000-3
1 parent 511415c commit a7c5c26

File tree

4 files changed

+106
-56
lines changed

4 files changed

+106
-56
lines changed

lib/elixir/lib/calendar/datetime.ex

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,9 +1030,12 @@ defmodule DateTime do
10301030
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
10311031
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
10321032
1033-
Only supports converting datetimes which are in the ISO calendar,
1034-
attempting to convert datetimes from other calendars will raise.
10351033
You can also optionally specify an offset for the formatted string.
1034+
If none is given, the one in the given `datetime` is used.
1035+
1036+
Only supports converting datetimes which are in the ISO calendar.
1037+
If another calendar is given, it is automatically converted to ISO.
1038+
It raises if not possible.
10361039
10371040
WARNING: the ISO 8601 datetime format does not contain the time zone nor
10381041
its abbreviation, which means information is lost when converting to such
@@ -1346,6 +1349,11 @@ defmodule DateTime do
13461349
@doc """
13471350
Converts the given `datetime` to a string according to its calendar.
13481351
1352+
Unfortunately, there is no standard that specifies rendering of a
1353+
datetime with its complete time zone information, so Elixir uses a
1354+
custom (but relatively common) representation which appends the time
1355+
zone abbreviation and full name to the datetime.
1356+
13491357
### Examples
13501358
13511359
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",

lib/elixir/lib/calendar/duration.ex

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -358,76 +358,109 @@ defmodule Duration do
358358
@doc """
359359
Converts the given `duration` to a human readable representation.
360360
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` - how to separate the distinct components
377+
361378
## Examples
362379
363-
iex> Duration.to_string(Duration.new!(year: 3))
364-
"3 years"
365-
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
366-
"40 days, 12 hours, 42 minutes, 12 seconds"
367380
iex> Duration.to_string(Duration.new!(second: 30))
368-
"30 seconds"
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":
369399
370400
iex> Duration.to_string(Duration.new!([]))
371-
"0 seconds"
401+
"0s"
402+
403+
Microseconds are rendered as part of seconds with the appropriate precision:
372404
373405
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
374-
"1.002 seconds"
406+
"1.002s"
375407
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
376-
"-0.2000 seconds"
408+
"-0.2000s"
377409
378410
"""
379411
@doc since: "1.18.0"
380-
def to_string(%Duration{} = duration) do
381-
case to_string_year(duration, []) do
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
382417
[] ->
383-
"0 seconds"
418+
"0" <> Keyword.get(units, :second, "s")
384419

385420
[part] ->
386421
IO.iodata_to_binary(part)
387422

388423
parts ->
389-
parts |> Enum.reduce(&[&1, ", " | &2]) |> IO.iodata_to_binary()
424+
parts |> Enum.reduce(&[&1, separator | &2]) |> IO.iodata_to_binary()
390425
end
391426
end
392427

393-
defp to_string_part(0, _singular, _plural, acc), do: acc
394-
defp to_string_part(1, singular, _plural, acc), do: [["1" | singular] | acc]
395-
defp to_string_part(x, _singular, plural, acc), do: [[Integer.to_string(x) | plural] | acc]
428+
defp to_string_part(0, _units, _key, _default, acc),
429+
do: acc
396430

397-
defp to_string_year(%{year: year} = duration, acc) do
398-
to_string_month(duration, to_string_part(year, " year", " years", acc))
399-
end
431+
defp to_string_part(x, units, key, default, acc),
432+
do: [[Integer.to_string(x) | Keyword.get(units, key, default)] | acc]
400433

401-
defp to_string_month(%{month: month} = duration, acc) do
402-
to_string_week(duration, to_string_part(month, " month", " months", acc))
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)
403436
end
404437

405-
defp to_string_week(%{week: week} = duration, acc) do
406-
to_string_day(duration, to_string_part(week, " week", " weeks", acc))
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)
407440
end
408441

409-
defp to_string_day(%{day: day} = duration, acc) do
410-
to_string_hour(duration, to_string_part(day, " day", " days", acc))
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)
411444
end
412445

413-
defp to_string_hour(%{hour: hour} = duration, acc) do
414-
to_string_minute(duration, to_string_part(hour, " hour", " hours", acc))
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)
415448
end
416449

417-
defp to_string_minute(%{minute: minute} = duration, acc) do
418-
to_string_second(duration, to_string_part(minute, " minute", " minutes", acc))
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)
419452
end
420453

421-
defp to_string_second(%{second: 1, microsecond: {0, _}}, acc) do
422-
["1 second" | acc]
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)
423456
end
424457

425-
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc) do
458+
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc, _units) do
426459
acc
427460
end
428461

429-
defp to_string_second(%{second: s, microsecond: {ms, p}}, acc) do
430-
[[second_component(s, ms, p) | " seconds"] | acc]
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]
431464
end
432465

433466
@doc """

lib/elixir/lib/calendar/naive_datetime.ex

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,6 +759,9 @@ defmodule NaiveDateTime do
759759
@doc """
760760
Converts the given naive datetime to a string according to its calendar.
761761
762+
For redability, this function follows the RFC3339 suggestion of removing
763+
the "T" separator between the date and time components.
764+
762765
### Examples
763766
764767
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])

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

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -361,66 +361,72 @@ defmodule DurationTest do
361361

362362
test "to_string/1" do
363363
assert Duration.to_string(%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}) ==
364-
"1 year, 2 months, 3 days, 4 hours, 5 minutes, 6 seconds"
364+
"1a 2mo 3d 4h 5min 6s"
365365

366366
assert Duration.to_string(%Duration{week: 3, hour: 5, minute: 3}) ==
367-
"3 weeks, 5 hours, 3 minutes"
367+
"3wk 5h 3min"
368368

369369
assert Duration.to_string(%Duration{hour: 5, minute: 3}) ==
370-
"5 hours, 3 minutes"
370+
"5h 3min"
371371

372372
assert Duration.to_string(%Duration{year: 1, month: 2, day: 3}) ==
373-
"1 year, 2 months, 3 days"
373+
"1a 2mo 3d"
374374

375375
assert Duration.to_string(%Duration{hour: 4, minute: 5, second: 6}) ==
376-
"4 hours, 5 minutes, 6 seconds"
376+
"4h 5min 6s"
377377

378378
assert Duration.to_string(%Duration{year: 1, month: 2}) ==
379-
"1 year, 2 months"
379+
"1a 2mo"
380380

381381
assert Duration.to_string(%Duration{day: 3}) ==
382-
"3 days"
382+
"3d"
383383

384384
assert Duration.to_string(%Duration{hour: 4, minute: 5}) ==
385-
"4 hours, 5 minutes"
385+
"4h 5min"
386386

387387
assert Duration.to_string(%Duration{second: 6}) ==
388-
"6 seconds"
388+
"6s"
389389

390390
assert Duration.to_string(%Duration{second: 1, microsecond: {600_000, 1}}) ==
391-
"1.6 seconds"
391+
"1.6s"
392392

393393
assert Duration.to_string(%Duration{second: -1, microsecond: {-600_000, 1}}) ==
394-
"-1.6 seconds"
394+
"-1.6s"
395395

396396
assert Duration.to_string(%Duration{second: -1, microsecond: {-234_567, 6}}) ==
397-
"-1.234567 seconds"
397+
"-1.234567s"
398398

399399
assert Duration.to_string(%Duration{second: 1, microsecond: {123_456, 6}}) ==
400-
"1.123456 seconds"
400+
"1.123456s"
401401

402402
assert Duration.to_string(%Duration{year: 3, week: 4, day: -3, second: -6}) ==
403-
"3 years, 4 weeks, -3 days, -6 seconds"
403+
"3a 4wk -3d -6s"
404404

405405
assert Duration.to_string(%Duration{second: -4, microsecond: {-230_000, 2}}) ==
406-
"-4.23 seconds"
406+
"-4.23s"
407407

408408
assert Duration.to_string(%Duration{second: -4, microsecond: {230_000, 2}}) ==
409-
"-3.77 seconds"
409+
"-3.77s"
410410

411411
assert Duration.to_string(%Duration{second: 2, microsecond: {-1_200_000, 4}}) ==
412-
"0.8000 seconds"
412+
"0.8000s"
413413

414414
assert Duration.to_string(%Duration{second: 1, microsecond: {-1_200_000, 3}}) ==
415-
"-0.200 seconds"
415+
"-0.200s"
416416

417417
assert Duration.to_string(%Duration{microsecond: {-800_000, 2}}) ==
418-
"-0.80 seconds"
418+
"-0.80s"
419419

420420
assert Duration.to_string(%Duration{microsecond: {-800_000, 0}}) ==
421-
"0 seconds"
421+
"0s"
422422

423423
assert Duration.to_string(%Duration{microsecond: {-1_200_000, 2}}) ==
424-
"-1.20 seconds"
424+
"-1.20s"
425+
426+
assert Duration.to_string(%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6},
427+
units: [year: "year", month: "month", day: "day"],
428+
separator: "-"
429+
) ==
430+
"1year-2month-3day-4h-5min-6s"
425431
end
426432
end

0 commit comments

Comments
 (0)