Skip to content

Commit

Permalink
Support negative period prefix in Calendar.ISO.parse_duration/1 (#1…
Browse files Browse the repository at this point in the history
  • Loading branch information
tfiedlerdejanze authored and josevalim committed May 27, 2024
1 parent bb77922 commit b84dd54
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 8 deletions.
19 changes: 12 additions & 7 deletions lib/elixir/lib/calendar/duration.ex
Original file line number Diff line number Diff line change
Expand Up @@ -285,20 +285,23 @@ defmodule Duration do
end

@doc """
Parses an ISO 8601 formatted duration string to a `Duration` struct.
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
A decimal fraction may be specified for seconds only, using either a comma or a full stop.
- 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`.
- A duration string may be prefixed with a minus sign to negate it: `-P10DT4H`.
- Individual units may be prefixed with a minus sign: `P-10DT4H`.
- Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
## Examples
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
iex> Duration.from_iso8601("PT10H30M")
{:ok, %Duration{hour: 10, minute: 30, second: 0}}
iex> Duration.from_iso8601("P3Y-2MT3H")
{:ok, %Duration{year: 3, month: -2, hour: 3}}
iex> Duration.from_iso8601("P1YT4.650S")
{:ok, %Duration{year: 1, second: 4, microsecond: {650000, 3}}}
iex> Duration.from_iso8601("-PT10H-30M")
{:ok, %Duration{hour: -10, minute: 30}}
iex> Duration.from_iso8601("PT4.650S")
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
"""
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
Expand All @@ -313,12 +316,14 @@ defmodule Duration do
end

@doc """
Same as `from_iso8601/1` but raises an ArgumentError.
Same as `from_iso8601/1` but raises an `ArgumentError`.
## Examples
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
iex> Duration.from_iso8601!("P10D")
%Duration{day: 10}
"""
@spec from_iso8601!(String.t()) :: t
Expand Down
12 changes: 11 additions & 1 deletion lib/elixir/lib/calendar/iso.ex
Original file line number Diff line number Diff line change
Expand Up @@ -670,11 +670,21 @@ defmodule Calendar.ISO do
See `Duration.from_iso8601/1`.
"""
@doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom()}
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end

def parse_duration("-P" <> string) when byte_size(string) > 0 do
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
end

def parse_duration(_) do
{:error, :invalid_duration}
end
Expand Down
3 changes: 3 additions & 0 deletions lib/elixir/test/elixir/calendar/duration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ defmodule DurationTest do
assert Duration.from_iso8601!("PT6S") == %Duration{second: 6}
assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}}
assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}}
assert Duration.from_iso8601!("-P10DT4H") == %Duration{day: -10, hour: -4}
assert Duration.from_iso8601!("-P10DT-4H") == %Duration{day: -10, hour: 4}
assert Duration.from_iso8601!("P-10D") == %Duration{day: -10}

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

0 comments on commit b84dd54

Please sign in to comment.