Skip to content

Commit b84dd54

Browse files
tfiedlerdejanzejosevalim
authored andcommitted
Support negative period prefix in Calendar.ISO.parse_duration/1 (#13608)
1 parent bb77922 commit b84dd54

File tree

3 files changed

+26
-8
lines changed

3 files changed

+26
-8
lines changed

lib/elixir/lib/calendar/duration.ex

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,20 +285,23 @@ defmodule Duration do
285285
end
286286

287287
@doc """
288-
Parses an ISO 8601 formatted duration string to a `Duration` struct.
288+
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
289289
290-
A decimal fraction may be specified for seconds only, using either a comma or a full stop.
290+
- A duration string must be designated in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
291+
- A duration string may be prefixed with a minus sign to negate it: `-P10DT4H`.
292+
- Individual units may be prefixed with a minus sign: `P-10DT4H`.
293+
- Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
291294
292295
## Examples
293296
294297
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
295298
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
296-
iex> Duration.from_iso8601("PT10H30M")
297-
{:ok, %Duration{hour: 10, minute: 30, second: 0}}
298299
iex> Duration.from_iso8601("P3Y-2MT3H")
299300
{:ok, %Duration{year: 3, month: -2, hour: 3}}
300-
iex> Duration.from_iso8601("P1YT4.650S")
301-
{:ok, %Duration{year: 1, second: 4, microsecond: {650000, 3}}}
301+
iex> Duration.from_iso8601("-PT10H-30M")
302+
{:ok, %Duration{hour: -10, minute: 30}}
303+
iex> Duration.from_iso8601("PT4.650S")
304+
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
302305
303306
"""
304307
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
@@ -313,12 +316,14 @@ defmodule Duration do
313316
end
314317

315318
@doc """
316-
Same as `from_iso8601/1` but raises an ArgumentError.
319+
Same as `from_iso8601/1` but raises an `ArgumentError`.
317320
318321
## Examples
319322
320323
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
321324
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
325+
iex> Duration.from_iso8601!("P10D")
326+
%Duration{day: 10}
322327
323328
"""
324329
@spec from_iso8601!(String.t()) :: t

lib/elixir/lib/calendar/iso.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -670,11 +670,21 @@ defmodule Calendar.ISO do
670670
See `Duration.from_iso8601/1`.
671671
"""
672672
@doc since: "1.17.0"
673-
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()}
673+
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
674674
def parse_duration("P" <> string) when byte_size(string) > 0 do
675675
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
676676
end
677677

678+
def parse_duration("-P" <> string) when byte_size(string) > 0 do
679+
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
680+
{:ok,
681+
Enum.map(fields, fn
682+
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
683+
{unit, value} -> {unit, -value}
684+
end)}
685+
end
686+
end
687+
678688
def parse_duration(_) do
679689
{:error, :invalid_duration}
680690
end

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,9 @@ defmodule DurationTest do
262262
assert Duration.from_iso8601!("PT6S") == %Duration{second: 6}
263263
assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}}
264264
assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}}
265+
assert Duration.from_iso8601!("-P10DT4H") == %Duration{day: -10, hour: -4}
266+
assert Duration.from_iso8601!("-P10DT-4H") == %Duration{day: -10, hour: 4}
267+
assert Duration.from_iso8601!("P-10D") == %Duration{day: -10}
265268

266269
assert Duration.from_iso8601!("PT-1.234567S") == %Duration{
267270
second: -1,

0 commit comments

Comments
 (0)