defmodule PgInterop.Interval.ISO8601Parser do @moduledoc """ This module parses ISO-8601 duration strings into Interval structs. Implementation taken from https://github.com/bitwalker/timex/blob/3.7.9/lib/parse/duration/parsers/iso8601.ex and adapted to fill a different structure + support negatives. """ alias PgInterop.Interval @numeric ~c"-.0123456789" @doc """ Parses an ISO-8601 formatted duration string into a Interval struct. The parse result is wrapped in a :ok/:error tuple. ## Examples iex> parse("P15Y3M2DT1H14M37.25S") {:ok, Interval.parse!("P15Y3M2DT1H14M37.25S")} iex> parse("P15Y3M2D") {:ok, Interval.parse!("P15Y3M2D")} iex> parse("PT3H12M25.001S") {:ok, Interval.parse!("PT3H12M25.001S")} iex> parse("P2W1D") {:ok, Interval.parse!("P15D")} iex> parse("") {:error, "input string cannot be empty"} iex> parse("P15YT3D") {:error, "invalid use of date component after time separator"} iex> parse("P15Y3H") {:error, "missing T separator between date and time components"} iex> parse("P15YTT3H") {:error, "encountered duplicate time separator T"} iex> parse("P1O") {:error, "unexpected token O"} iex> parse("P1-1D") {:error, "invalid number `1-1`"} iex> parse("P1") {:error, "unexpected end of input at 1"} iex> parse("P11") {:error, "unexpected end of input at 1"} iex> parse("PT") {:error, "unexpected end of input at T"} iex> parse("PO") {:error, "expected numeric, but got `O`"} iex> parse("O") {:error, "expected P, got `O`"} """ @spec parse(String.t()) :: {:ok, Interval.t()} | {:error, term} def parse(<<>>), do: {:error, "input string cannot be empty"} def parse(<>) do case parse_components(rest, []) do {:error, _} = err -> err components when is_list(components) -> result = Enum.reduce(components, {false, Interval.zero()}, fn _, {:error, _} = err -> err {?Y, y}, {false, d} -> {false, Interval.add(d, Interval.from_months(12 * y))} {?M, m}, {false, d} -> {false, Interval.add(d, Interval.from_months(m))} {?D, dd}, {false, d} -> {false, Interval.add(d, Interval.from_days(dd))} {?W, w}, {false, d} -> {false, Interval.add(d, Interval.from_days(7 * w))} ?T, {false, d} -> {true, d} ?T, {true, _d} -> {:error, "encountered duplicate time separator T"} {?H, h}, {true, d} -> {true, Interval.add(d, Interval.from_hours(h))} {?M, m}, {true, d} -> {true, Interval.add(d, Interval.from_minutes(m))} {?S, s}, {true, d} -> {true, Interval.add(d, Interval.from_seconds(s))} {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 `#{<>}`"} @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, "expected numeric, but got `#{<>}`"} @spec parse_component(binary, {:float | :integer, binary}) :: {integer, number, binary} | {:error, msg :: binary()} defp parse_component(<>, {type, acc}) when c in ~c"WYMDHS" do case cast_number(type, acc) do {n, ""} -> {c, n, rest} _ -> {:error, "invalid number `#{acc}`"} end end defp parse_component(<>, _acc) when c in @numeric, do: {:error, "unexpected end of input at #{<>}"} 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(<>, _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