defmodule Timex.Parse.Duration.Parsers.ISO8601Parser do @moduledoc """ This module parses ISO-8601 duration strings into Duration structs. """ use Timex.Parse.Duration.Parser @numeric ~c".0123456789" @doc """ Parses an ISO-8601 formatted duration string into a Duration struct. The parse result is wrapped in a :ok/:error tuple. ## Examples iex> {:ok, d} = #{__MODULE__}.parse("P15Y3M2DT1H14M37.25S") ...> Timex.Format.Duration.Formatter.format(d) "P15Y3M2DT1H14M37.25S" iex> {:ok, d} = #{__MODULE__}.parse("P15Y3M2D") ...> Timex.Format.Duration.Formatter.format(d) "P15Y3M2D" iex> {:ok, d} = #{__MODULE__}.parse("PT3H12M25.001S") ...> Timex.Format.Duration.Formatter.format(d) "PT3H12M25.001S" iex> {:ok, d} = #{__MODULE__}.parse("P2W") ...> Timex.Format.Duration.Formatter.format(d) "P14D" iex> #{__MODULE__}.parse("P15YT3D") {:error, "invalid use of date component after time separator"} """ @spec parse(String.t()) :: {:ok, Duration.t()} | {:error, term} def parse(<<>>), do: {:error, "input string cannot be empty"} def parse(<>) do case parse_components(rest, []) do {:error, _} = err -> err [{?W, w}] -> {:ok, Duration.from_days(7 * w)} components when is_list(components) -> result = Enum.reduce(components, {false, Duration.zero()}, fn _, {:error, _} = err -> err {?Y, y}, {false, d} -> {false, Duration.add(d, Duration.from_days(365 * y))} {?M, m}, {false, d} -> {false, Duration.add(d, Duration.from_days(30 * m))} {?D, dd}, {false, d} -> {false, Duration.add(d, Duration.from_days(dd))} ?T, {false, d} -> {true, d} ?T, {true, _d} -> {:error, "encountered duplicate time separator T"} {?H, h}, {true, d} -> {true, Duration.add(d, Duration.from_hours(h))} {?M, m}, {true, d} -> {true, Duration.add(d, Duration.from_minutes(m))} {?S, s}, {true, d} -> {true, Duration.add(d, Duration.from_seconds(s))} {?W, _w}, {_, _} -> {:error, "Found 'W', a basic format designator, but the parse indicates " <> "this is an extended format, mixing the two formats is disallowed"} {unit, _}, {true, _d} when unit in [?Y, ?D] -> {:error, "invalid use of date component after time separator"} {unit, _}, {false, _d} when unit in [?H, ?S] -> {:error, "missing T separator between date and time components"} end) case result do {:error, _} = err -> err {_, duration} -> {:ok, duration} end end end def parse(<>), do: {:error, "expected P, got #{<>}"} def parse(s) when is_binary(s), do: {:error, "unexpected end of input"} @spec parse_components(binary, [{integer, number}]) :: [{integer, number}] | {:error, String.t()} defp parse_components(<<>>, acc), do: Enum.reverse(acc) defp parse_components(<>, _acc), do: {:error, "unexpected end of input at T"} defp parse_components(<>, acc), do: parse_components(rest, [?T | acc]) defp parse_components(<>, _acc) when c in @numeric, do: {:error, "unexpected end of input at #{<>}"} defp parse_components(<>, acc) when c in @numeric do case parse_component(rest, {:integer, <>}) do {:error, _} = err -> err {u, n, rest} -> parse_components(rest, [{u, n} | acc]) end end defp parse_components(<>, _acc), do: {:error, "unexpected end of input at #{<>}"} defp parse_components(<>, _acc), do: {:error, "expected numeric, but got #{<>}"} @spec parse_component(binary, {:float | :integer, binary}) :: {integer, number, binary} | {:error, msg :: binary()} defp parse_component(<>, _acc) when c in @numeric, do: {:error, "unexpected end of input at #{<>}"} defp parse_component(<>, {type, acc}) when c in ~c"WYMDHS" do case cast_number(type, acc) do {n, _} -> {c, n, <<>>} :error -> {:error, "invalid number `#{acc}`"} end end defp parse_component(<<".", rest::binary>>, {:integer, acc}) do parse_component(rest, {:float, <>}) end defp parse_component(<>, {:integer, acc}) when c in @numeric do parse_component(rest, {:integer, <>}) end defp parse_component(<>, {:float, acc}) when c in @numeric do parse_component(rest, {:float, <>}) end defp parse_component(<>, {type, acc}) when c in ~c"WYMDHS" do case cast_number(type, acc) do {n, _} -> {c, n, rest} :error -> {:error, "invalid number `#{acc}`"} end end defp parse_component(<>, _acc), do: {:error, "unexpected token #{<>}"} defp parse_component(<>, _acc), do: {:error, "unexpected token #{<>}"} @spec cast_number(:float | :integer, binary) :: {number(), binary()} | :error defp cast_number(:integer, binary), do: Integer.parse(binary) defp cast_number(:float, binary), do: Float.parse(binary) end