Packages
electric
1.2.2
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.16
1.4.16-beta-1
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
retired
1.1.4
retired
1.1.3
retired
1.1.2
1.1.1
1.1.0
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.15
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-beta.23
1.0.0-beta.22
1.0.0-beta.20
1.0.0-beta.19
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.2
1.0.0-beta.1
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.5.2
0.4.4
Postgres sync engine. Sync little subsets of your Postgres data into local apps and services.
Current section
Files
Jump to
Current section
Files
lib/pg_interop/interval/iso8601_parser.ex
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(<<?P, rest::binary>>) 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(<<c::utf8, _::binary>>), do: {:error, "expected P, got `#{<<c::utf8>>}`"}
@spec parse_components(binary, [{integer, number}]) ::
[{integer, number}] | {:error, String.t()}
defp parse_components(<<>>, acc),
do: Enum.reverse(acc)
defp parse_components(<<?T>>, _acc),
do: {:error, "unexpected end of input at T"}
defp parse_components(<<?T, rest::binary>>, acc),
do: parse_components(rest, [?T | acc])
defp parse_components(<<c::utf8>>, _acc) when c in @numeric,
do: {:error, "unexpected end of input at #{<<c::utf8>>}"}
defp parse_components(<<c::utf8, rest::binary>>, acc) when c in @numeric do
case parse_component(rest, {:integer, <<c::utf8>>}) do
{:error, _} = err -> err
{u, n, rest} -> parse_components(rest, [{u, n} | acc])
end
end
defp parse_components(<<c::utf8, _::binary>>, _acc),
do: {:error, "expected numeric, but got `#{<<c::utf8>>}`"}
@spec parse_component(binary, {:float | :integer, binary}) ::
{integer, number, binary} | {:error, msg :: binary()}
defp parse_component(<<c::utf8, rest::binary>>, {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(<<c::utf8>>, _acc) when c in @numeric,
do: {:error, "unexpected end of input at #{<<c::utf8>>}"}
defp parse_component(<<".", rest::binary>>, {:integer, acc}) do
parse_component(rest, {:float, <<acc::binary, ".">>})
end
defp parse_component(<<c::utf8, rest::binary>>, {:integer, acc}) when c in @numeric do
parse_component(rest, {:integer, <<acc::binary, c::utf8>>})
end
defp parse_component(<<c::utf8, rest::binary>>, {:float, acc}) when c in @numeric do
parse_component(rest, {:float, <<acc::binary, c::utf8>>})
end
defp parse_component(<<c::utf8>>, _acc), do: {:error, "unexpected token #{<<c::utf8>>}"}
defp parse_component(<<c::utf8, _::binary>>, _acc),
do: {:error, "unexpected token #{<<c::utf8>>}"}
@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