Skip to content

Commit

Permalink
fix: improve postgres-style interval parsing (#177)
Browse files Browse the repository at this point in the history
Inspired by some examples in #176
  • Loading branch information
icehaunter authored Jul 24, 2024
1 parent 04f305f commit bbb377e
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/old-lamps-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@core/sync-service": patch
---

fix: correctly parse larger set of Postgres intervals with signs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,30 @@ defmodule PgInterop.Interval.PostgresAndSQLParser do
alias PgInterop.Interval

@parse_part_regexes [
unmarked_end: ~r/(?<=\s|^)(?<second>\d+(?:\.\d+)?)(?=\s*$)/,
microsecond: ~r/(?<=\s|^)(?<microsecond>\d+(?:\.\d+)?)\s*(?:us|usecs?|microseconds?)(?=\s|$)/,
millisecond: ~r/(?<=\s|^)(?<millisecond>\d+(?:\.\d+)?)\s*(?:ms|msecs?|milliseconds?)(?=\s|$)/,
second: ~r/(?<=\s|^)(?<second>\d+(?:\.\d+)?)\s*(?:s|secs?|seconds?)(?=\s|$)/,
minute: ~r/(?<=\s|^)(?<minute>\d+(?:\.\d+)?)\s*(?:m|mins?|minutes?)(?=\s|$)/,
hour: ~r/(?<=\s|^)(?<hour>\d+(?:\.\d+)?)\s*(?:h|hours?)(?=\s|$)/,
day: ~r/(?<=\s|^)(?<day>\d+(?:\.\d+)?)\s*(?:d|days?)(?=\s|$)/,
week: ~r/(?<=\s|^)(?<week>\d+(?:\.\d+)?)\s*(?:w|weeks?)(?=\s|$)/,
month: ~r/(?<=\s|^)(?<month>\d+(?:\.\d+)?)\s*(?:m|mons?|months?)(?=\s|$)/,
year: ~r/(?<=\s|^)(?<year>\d+(?:\.\d+)?)\s*(?:y|years?)(?=\s|$)/,
decade: ~r/(?<=\s|^)(?<decade>\d+(?:\.\d+)?)\s*(?:decs?|decades?)(?=\s|$)/,
century: ~r/(?<=\s|^)(?<century>\d+(?:\.\d+)?)\s*(?:c|cent|century|centuries)(?=\s|$)/,
millennium: ~r/(?<=\s|^)(?<millennium>\d+(?:\.\d+)?)\s*(?:mils|millenniums)(?=\s|$)/,
sql_ym: ~r/(?<=\s|^)(?<year>\d+)-(?<month>\d+)(?=\s|$)/,
sql_dhm: ~r/(?<=\s|^)(?<day>\d+(?:\.\d+)?)?\s+(?<hour>\d+):(?<minute>\d+)(?=\s|$)/,
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>\d+(?:\.\d+)?)?\s+(?<hour>\d+):(?<minute>\d+):(?<second>\d+(?:\.\d+)?)(?=\s|$)/,
sql_dms: ~r/(?<=\s|^)(?<day>\d+(?:\.\d+)?)?\s+(?<minute>\d+):(?<second>\d+\.\d+)(?=\s|$)/
~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 """
Expand All @@ -37,24 +43,39 @@ defmodule PgInterop.Interval.PostgresAndSQLParser do
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")
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")}
Expand Down Expand Up @@ -92,6 +113,7 @@ defmodule PgInterop.Interval.PostgresAndSQLParser 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))
Expand All @@ -102,10 +124,34 @@ defmodule PgInterop.Interval.PostgresAndSQLParser do
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
{float, ""} = Float.parse(v)
prepared =
case v do
"+" <> number -> String.trim(number)
"-" <> number -> "-" <> String.trim(number)
number -> String.trim(number)
end

{float, ""} = Float.parse(prepared)
float
end

Expand Down

0 comments on commit bbb377e

Please sign in to comment.