defmodule Tempus.Sigils do @moduledoc "Handy sigils to instantiate `Tempus.Slot` and `Tempus.Slots`" alias Tempus.Slot defmodule NilParser do @moduledoc false def from_iso8601(nil), do: {:ok, nil} def from_iso8601(""), do: {:ok, nil} def from_iso8601("∞"), do: {:ok, nil} def from_iso8601(_other), do: {:error, :not_a_nil} end @doc ~S""" Handles the sigil `~I` for `Tempus.Slot`. It returns a slot without interpolations and without escape characters, except for the escaping of the closing sigil character itself. _Allowed separators:_ `|`, `→`, `->`, `..` (might be surrounded by spaces) _Allowed modifiers:_ - _none_ — expects two instances of `DateTime` - _single letter_, one of `d`, `t`, `u` — both values are expected to be of that type - _any combination_ of two letters `d`, `t`, `u` — the values are treated respectively - `g` — the value types are to be guessed ## Examples iex> import Tempus.Sigils iex> ~I(2021-03-30T06:35:40Z|2021-03-30T06:36:00Z) %Tempus.Slot{from: ~U[2021-03-30 06:35:40Z], to: ~U[2021-03-30 06:36:00Z]} iex> ~I(2021-03-30 → 2021-03-31)d %Tempus.Slot{from: ~U[2021-03-30 00:00:00.000000Z], to: ~U[2021-03-31 23:59:59.999999Z]} iex> ~I(2021-03-30)d %Tempus.Slot{from: ~U[2021-03-30 00:00:00.000000Z], to: ~U[2021-03-30 23:59:59.999999Z]} iex> ~I(2021-03-30 06:35:40Z .. 2021-03-30 06:36:00Z)g %Tempus.Slot{from: ~U[2021-03-30 06:35:40Z], to: ~U[2021-03-30 06:36:00Z]} iex> ~I(|2021-03-30 06:36:00Z)g %Tempus.Slot{from: ~U[2021-03-30 06:36:00Z], to: ~U[2021-03-30 06:36:00Z]} iex> ~I(2021-03-30 06:36:00Z|)g %Tempus.Slot{from: ~U[2021-03-30 06:36:00Z], to: ~U[2021-03-30 06:36:00Z]} iex> Code.compile_quoted(quote do: ~I(foo|bar)g) ** (CompileError) nofile: `~I` sigil input is malformed, error: :invalid_format """ defmacro sigil_I({:<<>>, _, [binary]}, ~c"g") when is_binary(binary) do from_iso = List.duplicate(&Tempus.guess/1, 2) do_ast(binary, from_iso, __CALLER__) end defmacro sigil_I({:<<>>, _, [binary]}, modifiers) when is_binary(binary) do by_mod = fn ?d -> Date ?t -> Time ?u -> DateTime ?n -> NilParser end from_iso = modifiers |> case do [] -> [DateTime, DateTime] [ft] when ft in ~c"dtun" -> List.duplicate(by_mod.(ft), 2) [f, t] = mods when f in ~c"dtun" and t in ~c"dtun" -> Enum.map(mods, by_mod) end |> Enum.map(&Function.capture(&1, :from_iso8601, 1)) do_ast(binary, from_iso, __CALLER__) end @doc """ Parses the sigil-like binary representation of a `Tempus.Slot`. ## Examples iex> Tempus.Sigils.parse("2021-03-30T06:35:40Z|2021-03-30T06:36:00Z") {:ok, %Tempus.Slot{from: ~U[2021-03-30 06:35:40Z], to: ~U[2021-03-30 06:36:00Z]}} iex> Tempus.Sigils.parse("foo|bar") {:error, :invalid_format} """ @spec parse(input :: binary()) :: {:ok, Slot.t()} | {:error, any()} def parse(input) do result = input |> do_split() |> Enum.reduce_while([], fn s, acc -> s |> Tempus.guess() |> case do {:ok, slot} -> {:cont, [Slot.join([Slot.wrap(slot) | acc])]} {:error, reason} -> {:halt, {:error, reason}} end end) with [%Slot{} = slot] <- result, do: {:ok, slot} end defp do_ast(input, from_iso, caller) do result = input |> do_split() |> Enum.zip(from_iso) |> Enum.map(fn {value, mapper} -> case mapper.(value) do {:ok, nil} -> nil {:ok, result} -> Slot.wrap(result) {:ok, result, _} -> Slot.wrap(result) {:error, error} -> raise CompileError, file: caller.file, line: caller.line, description: "`~I` sigil input is malformed, error: " <> inspect(error) end end) |> case do [nil, %Slot{to: to}] -> %Slot{from: nil, to: to} [%Slot{from: from}, nil] -> %Slot{from: from, to: nil} slots -> Slot.join(slots) end |> Macro.escape() quote(generated: true, location: :keep, do: unquote(result)) end defp do_split(input), do: Regex.split(~r{\s*(?:\||→|\->|\.\.)\s*}, input) end