Current section
Files
Jump to
Current section
Files
lib/sigils.ex
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], from_open: false, to_open: true}
iex> ~I(2021-03-30 → 2021-03-31)d
%Tempus.Slot{from: ~U[2021-03-30 00:00:00.000000Z], to: ~U[2021-04-01 00:00:00.000000Z], from_open: false, to_open: true}
iex> ~I(2021-03-30)d
%Tempus.Slot{from: ~U[2021-03-30 00:00:00.000000Z], to: ~U[2021-03-31 00:00:00.000000Z], from_open: false, to_open: true}
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], from_open: false, to_open: true}
iex> ~I(|2021-03-30 06:36:00Z)g
%Tempus.Slot{from: nil, to: ~U[2021-03-30 06:36:00Z], from_open: true, to_open: true}
iex> ~I(2021-03-30 06:36:00Z|)g
%Tempus.Slot{from: ~U[2021-03-30 06:36:00Z], to: nil, from_open: false, to_open: true}
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(iso_mapper(DateTime), 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
[] -> List.duplicate(DateTime, 2)
[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(&iso_mapper/1)
do_ast(binary, from_iso, __CALLER__)
end
defp iso_mapper(mod) do
fn val ->
val = String.replace(val, " ", "T")
case mod.from_iso8601(val) do
{:ok, result, _} -> {:ok, result}
{:ok, result} -> {:ok, result}
{:error, _} -> Tempus.guess(val)
end
end
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], from_open: false, to_open: true}}
iex> Tempus.Sigils.parse("foo|bar")
{:error, :invalid_format}
"""
@spec parse(input :: binary()) :: {:ok, Slot.t()} | {:error, any()}
def parse(input) do
{from_open, to_open, clean_input} = extract_bounds(input)
parts = do_split(clean_input)
case Enum.map(parts, &Tempus.guess/1) do
[{:ok, s1}, {:ok, s2}] ->
s1 = Slot.wrap(s1)
s2 = Slot.wrap(s2)
slot = %Slot{
from: s1.from,
to: s2.to || s2.from,
from_open: if(is_nil(from_open), do: false, else: from_open),
to_open: if(is_nil(to_open), do: true, else: to_open)
}
{:ok, slot}
[{:ok, s1}] ->
%Slot{} = s1 = Slot.wrap(s1)
slot = %Slot{
s1
| from_open: if(is_nil(from_open), do: s1.from_open, else: from_open),
to_open: if(is_nil(to_open), do: s1.to_open, else: to_open)
}
{:ok, slot}
_ ->
{:error, :invalid_format}
end
end
defp do_ast(input, from_iso, caller) do
{from_open, to_open, clean_input} = extract_bounds(input)
result =
clean_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, to_open: default_to_op}] ->
%Slot{
from: nil,
to: to,
from_open: true,
to_open: if(is_nil(to_open), do: default_to_op, else: to_open)
}
[%Slot{from: from, from_open: default_from_op}, nil] ->
%Slot{
from: from,
to: nil,
from_open: if(is_nil(from_open), do: default_from_op, else: from_open),
to_open: true
}
[%Slot{from: f}, %Slot{to: t_to, from: t_from}] ->
to = if is_nil(t_to), do: t_from, else: t_to
%Slot{
from: f,
to: to,
from_open:
if(is_nil(f), do: true, else: if(is_nil(from_open), do: false, else: from_open)),
to_open: if(is_nil(to), do: true, else: if(is_nil(to_open), do: true, else: to_open))
}
[%Slot{} = s] ->
%Slot{
s
| from_open:
if(is_nil(s.from),
do: true,
else: if(is_nil(from_open), do: s.from_open, else: from_open)
),
to_open:
if(is_nil(s.to),
do: true,
else: if(is_nil(to_open), do: s.to_open, else: to_open)
)
}
end
|> Macro.escape()
quote(generated: true, location: :keep, do: unquote(result))
end
defp extract_bounds(input) do
input = String.trim(input)
len = String.length(input)
{from_open, input} =
cond do
String.starts_with?(input, "]") or String.starts_with?(input, "(") ->
{true, String.slice(input, 1, len - 1)}
String.starts_with?(input, "[") ->
{false, String.slice(input, 1, len - 1)}
true ->
{nil, input}
end
len = String.length(input)
{to_open, input} =
cond do
String.ends_with?(input, "[") or String.ends_with?(input, ")") ->
{true, String.slice(input, 0, len - 1)}
String.ends_with?(input, "]") ->
{false, String.slice(input, 0, len - 1)}
true ->
{nil, input}
end
{from_open, to_open, String.trim(input)}
end
defp do_split(input), do: Regex.split(~r{\s*(?:\||→|\->|\.\.)\s*}, input)
end