2024-05-25 20:03:45 +02:00
|
|
|
defmodule PhilomenaQuery.RelativeDate do
|
|
|
|
@moduledoc """
|
|
|
|
Relative date parsing, for strings like "a week ago" or "5 years from now".
|
|
|
|
"""
|
|
|
|
|
2019-11-19 04:38:22 +01:00
|
|
|
import NimbleParsec
|
|
|
|
|
2019-12-30 13:30:39 +01:00
|
|
|
number_words =
|
|
|
|
choice([
|
|
|
|
string("a") |> replace(1),
|
|
|
|
string("an") |> replace(1),
|
|
|
|
string("one") |> replace(1),
|
|
|
|
string("two") |> replace(2),
|
|
|
|
string("three") |> replace(3),
|
|
|
|
string("four") |> replace(4),
|
|
|
|
string("five") |> replace(5),
|
|
|
|
string("six") |> replace(6),
|
|
|
|
string("seven") |> replace(7),
|
|
|
|
string("eight") |> replace(8),
|
|
|
|
string("nine") |> replace(9),
|
|
|
|
string("ten") |> replace(10),
|
|
|
|
integer(min: 1)
|
|
|
|
])
|
|
|
|
|
2019-11-19 04:38:22 +01:00
|
|
|
time_specifier =
|
|
|
|
choice([
|
|
|
|
string("second") |> replace(1),
|
|
|
|
string("minute") |> replace(60),
|
|
|
|
string("hour") |> replace(3_600),
|
|
|
|
string("day") |> replace(86_400),
|
|
|
|
string("week") |> replace(604_800),
|
|
|
|
string("month") |> replace(2_592_000),
|
|
|
|
string("year") |> replace(31_536_000)
|
|
|
|
])
|
|
|
|
|> ignore(optional(string("s")))
|
|
|
|
|
|
|
|
direction_specifier =
|
|
|
|
choice([
|
|
|
|
string("ago") |> replace(-1),
|
|
|
|
string("from now") |> replace(1)
|
|
|
|
])
|
|
|
|
|
|
|
|
space = ignore(repeat(string(" ")))
|
|
|
|
|
|
|
|
moon =
|
|
|
|
space
|
|
|
|
|> string("moon")
|
|
|
|
|> concat(space)
|
|
|
|
|> eos()
|
|
|
|
|> unwrap_and_tag(:moon)
|
|
|
|
|
2019-12-14 19:28:04 +01:00
|
|
|
now =
|
|
|
|
space
|
|
|
|
|> string("now")
|
|
|
|
|> concat(space)
|
|
|
|
|> eos()
|
|
|
|
|> unwrap_and_tag(:now)
|
|
|
|
|
2019-11-19 04:38:22 +01:00
|
|
|
date =
|
|
|
|
space
|
2019-12-30 13:30:39 +01:00
|
|
|
|> concat(number_words)
|
2019-11-19 04:38:22 +01:00
|
|
|
|> concat(space)
|
|
|
|
|> concat(time_specifier)
|
|
|
|
|> concat(space)
|
|
|
|
|> concat(direction_specifier)
|
|
|
|
|> concat(space)
|
|
|
|
|> eos()
|
|
|
|
|> tag(:relative_date)
|
|
|
|
|
|
|
|
relative_date =
|
|
|
|
choice([
|
|
|
|
moon,
|
2019-12-14 19:28:04 +01:00
|
|
|
now,
|
2019-11-19 04:38:22 +01:00
|
|
|
date
|
|
|
|
])
|
|
|
|
|
2020-01-11 05:20:19 +01:00
|
|
|
defparsecp(:relative_date, relative_date)
|
2019-11-19 04:38:22 +01:00
|
|
|
|
2024-05-25 20:03:45 +02:00
|
|
|
@doc """
|
|
|
|
Parse an absolute date in valid ISO 8601 format, or an English-language relative date.
|
|
|
|
|
|
|
|
See `parse_absolute/1` and `parse_relative/1` for examples of what may be accepted
|
|
|
|
by this function.
|
|
|
|
"""
|
|
|
|
@spec parse_absolute(String.t()) :: {:ok, DateTime.t()} | {:error, any()}
|
2019-11-19 04:38:22 +01:00
|
|
|
def parse(input) do
|
|
|
|
input =
|
|
|
|
input
|
|
|
|
|> to_string()
|
|
|
|
|> String.trim()
|
|
|
|
|
|
|
|
case parse_absolute(input) do
|
|
|
|
{:ok, datetime} ->
|
|
|
|
{:ok, datetime}
|
|
|
|
|
|
|
|
_error ->
|
2021-10-24 04:12:27 +02:00
|
|
|
parse_relative(String.downcase(input))
|
2019-11-19 04:38:22 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-25 20:03:45 +02:00
|
|
|
@doc """
|
|
|
|
Parse an absolute date, given in a valid ISO 8601 format.
|
|
|
|
|
|
|
|
## Example
|
|
|
|
|
|
|
|
iex> PhilomenaQuery.RelativeDate.parse_absolute("2024-01-01T00:00:00Z")
|
|
|
|
{:ok, ~U[2024-01-01 00:00:00Z]}
|
|
|
|
|
|
|
|
iex> PhilomenaQuery.RelativeDate.parse_absolute("2024-01-01T00:00:00-01:00")
|
|
|
|
{:ok, ~U[2024-01-01 01:00:00Z]
|
|
|
|
|
|
|
|
iex> PhilomenaQuery.RelativeDate.parse_absolute("2024")
|
|
|
|
{:error, "Parse error"}
|
|
|
|
|
|
|
|
"""
|
|
|
|
@spec parse_absolute(String.t()) :: {:ok, DateTime.t()} | {:error, any()}
|
2019-11-19 04:38:22 +01:00
|
|
|
def parse_absolute(input) do
|
|
|
|
case DateTime.from_iso8601(input) do
|
|
|
|
{:ok, datetime, _offset} ->
|
|
|
|
{:ok, datetime |> DateTime.truncate(:second)}
|
|
|
|
|
|
|
|
_error ->
|
|
|
|
{:error, "Parse error"}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-05-25 20:03:45 +02:00
|
|
|
@doc """
|
|
|
|
Parse an English-language relative date. Accepts "moon" to mean 1000 years from now.
|
|
|
|
|
|
|
|
## Example
|
|
|
|
|
|
|
|
iex> PhilomenaQuery.RelativeDate.parse_relative("a year ago")
|
|
|
|
{:ok, ~U[2023-01-01 00:00:00Z]
|
|
|
|
|
|
|
|
iex> PhilomenaQuery.RelativeDate.parse_relative("three days from now")
|
|
|
|
{:ok, ~U[2024-01-04 00:00:00Z]}
|
|
|
|
|
|
|
|
iex> PhilomenaQuery.RelativeDate.parse_relative("moon")
|
|
|
|
{:ok, ~U[3024-01-01 00:00:00Z]}
|
|
|
|
|
|
|
|
iex> PhilomenaQuery.RelativeDate.parse_relative("2024")
|
|
|
|
{:error, "Parse error"}
|
|
|
|
|
|
|
|
"""
|
|
|
|
@spec parse_relative(String.t()) :: {:ok, DateTime.t()} | {:error, any()}
|
2019-11-19 04:38:22 +01:00
|
|
|
def parse_relative(input) do
|
|
|
|
case relative_date(input) do
|
|
|
|
{:ok, [moon: _moon], _1, _2, _3, _4} ->
|
2020-01-11 05:20:19 +01:00
|
|
|
{:ok,
|
|
|
|
DateTime.utc_now() |> DateTime.add(31_536_000_000, :second) |> DateTime.truncate(:second)}
|
2019-11-19 04:38:22 +01:00
|
|
|
|
2019-12-14 19:28:04 +01:00
|
|
|
{:ok, [now: _now], _1, _2, _3, _4} ->
|
|
|
|
{:ok, DateTime.utc_now() |> DateTime.truncate(:second)}
|
|
|
|
|
2019-11-19 04:38:22 +01:00
|
|
|
{:ok, [relative_date: [amount, scale, direction]], _1, _2, _3, _4} ->
|
2020-01-11 05:20:19 +01:00
|
|
|
{:ok,
|
|
|
|
DateTime.utc_now()
|
|
|
|
|> DateTime.add(amount * scale * direction, :second)
|
|
|
|
|> DateTime.truncate(:second)}
|
2019-11-19 04:38:22 +01:00
|
|
|
|
|
|
|
_error ->
|
|
|
|
{:error, "Parse error"}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|