Packages
electric
1.2.2
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/postgres_and_sql_parser.ex
defmodule PgInterop.Interval.PostgresAndSQLParser do
@moduledoc """
This module parses Postgres classic and SQL strings
"""
alias PgInterop.Interval
defp parse_part_regexes,
do: [
unmarked_end: ~r/(?<=\s|^)(?<second>(?:\+|-)?\s*\d+(?:\.\d+)?)(?=\s*$)/,
microsecond:
~r/(?<=\s|^)(?<microsecond>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:us|usecs?|microseconds?)(?=\s|$)/,
millisecond:
~r/(?<=\s|^)(?<millisecond>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:ms|msecs?|milliseconds?)(?=\s|$)/,
second: ~r/(?<=\s|^)(?<second>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:s|secs?|seconds?)(?=\s|$)/,
minute: ~r/(?<=\s|^)(?<minute>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:m|mins?|minutes?)(?=\s|$)/,
hour: ~r/(?<=\s|^)(?<hour>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:h|hours?)(?=\s|$)/,
day: ~r/(?<=\s|^)(?<day>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:d|days?)(?=\s|$)/,
week: ~r/(?<=\s|^)(?<week>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:w|weeks?)(?=\s|$)/,
month: ~r/(?<=\s|^)(?<month>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:m|mons?|months?)(?=\s|$)/,
year: ~r/(?<=\s|^)(?<year>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:y|years?)(?=\s|$)/,
decade: ~r/(?<=\s|^)(?<decade>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:decs?|decades?)(?=\s|$)/,
century:
~r/(?<=\s|^)(?<century>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:c|cent|century|centuries)(?=\s|$)/,
millennium:
~r/(?<=\s|^)(?<millennium>(?:\+|-)?\s*\d+(?:\.\d+)?)\s*(?:mils|millenniums)(?=\s|$)/,
sql_ym: ~r/(?<=\s|^)(?<sign>\+|-)?\s*(?<year>\d+)-(?<month>\-?\d+)(?=\s|$)/,
sql_dhm:
~r/(?<=\s|^)(?<day>(?:\+|-)?\s*\d+(?:\.\d+)?)?\s+(?<sign>\+|-)?\s*(?<hour>\d+):(?<minute>\d+)(?=\s|$)/,
sql_dhms:
~r/(?<=\s|^)(?<day>(?:\+|-)?\s*\d+(?:\.\d+)?)?\s+(?<sign>\+|-)?\s*(?<hour>\d+):(?<minute>\d+):(?<second>\d+(?:\.\d+)?)(?=\s|$)/,
sql_dms:
~r/(?<=\s|^)(?<day>(?:\+|-)?\s*\d+(?:\.\d+)?)?\s+(?<sign>\+|-)?\s*(?<minute>\d+):(?<second>\d+\.\d+)(?=\s|$)/
]
@doc """
Parses an Postgres classic and SQL formatted duration string into
a Interval struct. The parse result is wrapped in a :ok/:error tuple.
## Examples
iex> parse("1-2")
{:ok, Interval.parse!("P1Y2M")}
iex> parse("@ 1-2")
{:ok, Interval.parse!("P1Y2M")}
iex> parse("-1-2 +3 -4:05:06")
{:ok, Interval.parse!("P-1Y-2M3DT-4H-5M-6S")}
iex> parse("-1-2 -5:10.1")
{:ok, Interval.parse!("P-1Y-2MT-5M-10.1S")}
iex> parse("3 4:05:06")
{:ok, Interval.parse!("P3DT4H5M6S")}
iex> parse("1 year 2 months 3 days 4 hours 5 minutes 6 seconds")
{:ok, Interval.parse!("P1Y2M3DT4H5M6S")}
iex> parse("1 year 2-1 3 days +2")
{:ok, Interval.parse!("P3Y1M3DT2S")}
iex> parse("1 year 2-1 3 days -2")
{:ok, Interval.parse!("P3Y1M3DT-2S")}
iex> parse("1 year 2-1 3 days 2:2")
{:ok, Interval.parse!("P3Y1M3DT2H2M")}
iex> parse("1 year 2-1 3 days 2.2")
{:ok, Interval.parse!("P3Y1M3DT2.2S")}
iex> parse("-1-2 -5:10.1")
{:ok, Interval.parse!("P-1Y-2MT-5M-10.1S")}
iex> parse("1.3 cent 100-11 10.3")
{:ok, Interval.parse!("P230Y11MT10.3S")}
iex> parse("- 1 year -2 mons +3 days - 04:05:06")
{:ok, Interval.parse!("P-1Y-2M3DT-4H-5M-6S")}
iex> parse("0.1 mils 1 cent 1 decade 1 year 1 month 1 week 1 day 1 hour 1 minute 1 second 1000 ms 1000000 us")
{:ok, Interval.parse!("P211Y1M8DT1H1M3S")}
iex> parse("1.3 cent 100-11 10.3 1-1")
{:error, "invalid input syntax at `10.3`"}
iex> parse("10 10:01 1 10:01:10")
{:error, "invalid input syntax"}
iex> parse("1 month 1-10")
{:error, "invalid input syntax"}
iex> parse("1-13")
{:error, "invalid input syntax"}
iex> parse("1 minute 1 10:01:10")
{:error, "invalid input syntax"}
iex> parse("1 second 1 10:01:10")
{:error, "invalid input syntax"}
iex> parse("10 10:01 1")
{:error, "invalid input syntax"}
iex> parse("")
{:error, "input string cannot be empty"}
"""
@spec parse(String.t()) :: {:ok, Interval.t()} | {:error, term}
def parse(<<>>), do: {:error, "input string cannot be empty"}
def parse(<<?@, rest::binary>>), do: parse(rest)
def parse(str) do
{parsed_parts, rest} =
Enum.map_reduce(parse_part_regexes(), str, fn {key, regex}, str ->
{{key, Regex.named_captures(regex, str)}, Regex.replace(regex, str, "")}
end)
if String.trim(rest) == "" do
with :ok <- validate_parts(Map.new(parsed_parts)) do
{:ok,
parsed_parts
|> Enum.map(&apply_sign/1)
|> Keyword.values()
|> Enum.reject(&is_nil/1)
|> Enum.flat_map(&Enum.map(&1, fn {k, v} -> build_duration(k, parse_float!(v)) end))
|> Enum.reduce(Interval.zero(), &Interval.add/2)}
end
else
{:error, "invalid input syntax at `#{String.trim(rest)}`"}
end
end
defp apply_sign({k, %{"sign" => sign} = v}) when sign == "+" or sign == "",
do: {k, Map.delete(v, "sign")}
defp apply_sign({k, %{"sign" => "-"} = v}) do
v =
v
|> Map.delete("sign")
|> Map.new(fn
{"day", _} = day -> day
{part, val} -> {part, "-" <> val}
end)
{k, v}
end
defp apply_sign(no_sign), do: no_sign
defp parse_float!(""), do: 0
defp parse_float!(v) do
prepared =
case v do
"+" <> number -> String.trim(number)
"-" <> number -> "-" <> String.trim(number)
number -> String.trim(number)
end
{float, ""} = Float.parse(prepared)
float
end
defp validate_parts(%{sql_dhm: x, sql_dhms: y, sql_dms: z})
when not is_nil(x) and not is_nil(y)
when not is_nil(y) and not is_nil(z)
when not is_nil(x) and not is_nil(z),
do: {:error, "invalid input syntax"}
@valid_months ~w|1 2 3 4 5 6 7 8 9 10 11 1|
defp validate_parts(%{sql_ym: %{}, month: %{}}), do: {:error, "invalid input syntax"}
defp validate_parts(%{sql_ym: %{"month" => mon}}) when mon not in @valid_months,
do: {:error, "invalid input syntax"}
defp validate_parts(%{minute: %{}, sql_dhm: x, sql_dhms: y, sql_dms: z})
when not is_nil(x)
when not is_nil(y)
when not is_nil(z),
do: {:error, "invalid input syntax"}
defp validate_parts(%{second: %{}, sql_dhm: x, sql_dhms: y, sql_dms: z})
when not is_nil(x)
when not is_nil(y)
when not is_nil(z),
do: {:error, "invalid input syntax"}
defp validate_parts(%{unmarked_end: %{}, sql_dhm: x, sql_dhms: y, sql_dms: z})
when not is_nil(x)
when not is_nil(y)
when not is_nil(z),
do: {:error, "invalid input syntax"}
defp validate_parts(_), do: :ok
defp build_duration("microsecond", number), do: Interval.from_microseconds(floor(number))
defp build_duration("millisecond", number), do: Interval.from_milliseconds(number)
defp build_duration("second", number), do: Interval.from_seconds(number)
defp build_duration("minute", number), do: Interval.from_minutes(number)
defp build_duration("hour", number), do: Interval.from_hours(number)
defp build_duration("day", number), do: Interval.from_days(number)
defp build_duration("week", number), do: Interval.from_weeks(number)
defp build_duration("month", number), do: Interval.from_months(number)
defp build_duration("year", number), do: Interval.from_months(12 * number)
defp build_duration("decade", number), do: Interval.from_months(12 * 10 * number)
defp build_duration("century", number), do: Interval.from_months(12 * 100 * number)
defp build_duration("millennium", number), do: Interval.from_months(12 * 1000 * number)
end