defmodule Timex.Parse.DateTime.Tokenizers.Default do
@moduledoc """
Implements the parser for the default DateTime format strings.
"""
import Combine.Parsers.Base
import Combine.Parsers.Text
use Timex.Parse.DateTime.Tokenizer
@doc """
Tokenizes the given format string and returns an error or a list of directives.
"""
@spec tokenize(String.t()) :: {:ok, [Directive.t()]} | {:error, term}
def tokenize(<<>>), do: {:error, "Format string cannot be empty."}
def tokenize(str) do
token_parser = default_format_parser()
case Combine.parse(str, token_parser) do
results when is_list(results) ->
directives = results |> List.flatten() |> Enum.filter(fn x -> x !== nil end)
case Enum.any?(directives, fn %Directive{type: type} -> type != :literal end) do
false -> {:error, "Invalid format string, must contain at least one directive."}
true -> {:ok, directives}
end
{:error, _} = err ->
err
end
end
@doc """
Applies a given token + value to the DateTime represented by the current input string.
"""
@spec apply(DateTime.t(), atom, term) :: DateTime.t() | {:error, term} | :unrecognized
def apply(_, _, _), do: :unrecognized
@spec directives() :: (Combine.ParserState.t() -> Combine.ParserState.t())
defp directives() do
pipe(
[
option(one_of(char(), ["0", "_"])),
one_of(word_of(~r/[\-\w\:]/), [
# Years/Centuries
"YYYY",
"YY",
"C",
"WYYYY",
"WYY",
# Months
"Mshort",
"Mfull",
"M",
# Days
"Dord",
"D",
# Weeks
"Wiso",
"Wmon",
"Wsun",
"WDmon",
"WDsun",
"WDshort",
"WDfull",
# Time
"h24",
"h12",
"m",
"ss",
"s-epoch",
"s",
"am",
"AM",
# Timezones
"Zname",
"Zabbr",
"Z::",
"Z:",
"Z",
# Compound
"ISOord",
"ISOweek-day",
"ISOweek",
"ISOdate",
"ISOtime",
"ISOz",
"ISO",
"ISO:Extended",
"ISO:Extended:Z",
"ISO:Basic",
"ISO:Basic:Z",
"RFC822z",
"RFC822",
"RFC1123z",
"RFC1123",
"RFC3339z",
"RFC3339",
"ANSIC",
"UNIX",
"ASN1:UTCtime",
"ASN1:GeneralizedTime",
"ASN1:GeneralizedTime:Z",
"ASN1:GeneralizedTime:TZ",
"kitchen"
])
],
&coalesce_token/1
)
end
@spec default_format_parser() :: (Combine.ParserState.t() -> Combine.ParserState.t())
defp default_format_parser() do
many1(
choice([
# {<padding><directive>}
label(
between(char(?{), directives(), char(?})),
"a valid directive."
),
label(
map(none_of(char(), ["{", "}"]), &map_literal/1),
"any character but { or }."
),
label(
map(pair_left(char(?{), char(?{)), &map_literal/1),
"an escaped { character"
),
label(
map(pair_left(char(?}), char(?})), &map_literal/1),
"an escaped } character"
)
])
)
|> eof
end
@spec coalesce_token(list(binary)) :: Directive.t()
defp coalesce_token([flags, directive]) do
flags = map_flag(flags)
width = [min: -1, max: nil]
modifiers = []
map_directive(directive, flags: flags, width: width, modifiers: modifiers)
end
@spec map_directive(String.t(), list()) :: Directive.t()
defp map_directive(directive, opts) do
case directive do
# Years/Centuries
"YYYY" -> set_width(1, 4, :year4, directive, opts)
"YY" -> set_width(1, 2, :year2, directive, opts)
"C" -> set_width(1, 2, :century, directive, opts)
"WYYYY" -> force_width(4, :iso_year4, directive, opts)
"WYY" -> force_width(2, :iso_year2, directive, opts)
# Months
"M" -> set_width(1, 2, :month, directive, opts)
"Mfull" -> Directive.get(:mfull, directive, opts)
"Mshort" -> Directive.get(:mshort, directive, opts)
# Days
"D" -> set_width(1, 2, :day, directive, opts)
"Dord" -> set_width(1, 3, :oday, directive, opts)
# Weeks
"Wiso" -> force_width(2, :iso_weeknum, directive, opts)
"Wmon" -> set_width(1, 2, :week_mon, directive, opts)
"Wsun" -> set_width(1, 2, :week_sun, directive, opts)
"WDmon" -> Directive.get(:wday_mon, directive, opts)
"WDsun" -> Directive.get(:wday_sun, directive, opts)
"WDshort" -> Directive.get(:wdshort, directive, opts)
"WDfull" -> Directive.get(:wdfull, directive, opts)
# Hours
"h24" -> force_width(2, :hour24, directive, opts)
"h12" -> set_width(1, 2, :hour12, directive, opts)
"m" -> force_width(2, :min, directive, opts)
"s" -> force_width(2, :sec, directive, opts)
"s-epoch" -> Directive.get(:sec_epoch, directive, opts)
"ss" -> Directive.get(:sec_fractional, directive, opts)
"am" -> %{Directive.get(:am, directive, opts) | :weight => 99}
"AM" -> %{Directive.get(:AM, directive, opts) | :weight => 99}
# Timezones
"Zname" -> Directive.get(:zname, directive, opts)
"Zabbr" -> Directive.get(:zabbr, directive, opts)
"Z" -> Directive.get(:zoffs, directive, opts)
"Z:" -> Directive.get(:zoffs_colon, directive, opts)
"Z::" -> Directive.get(:zoffs_sec, directive, opts)
# Preformatted Directives
"ISO:Extended" -> Directive.get(:iso_8601_extended, directive, opts)
"ISO:Extended:Z" -> Directive.get(:iso_8601_extended_z, directive, opts)
"ISO:Basic" -> Directive.get(:iso_8601_basic, directive, opts)
"ISO:Basic:Z" -> Directive.get(:iso_8601_basic_z, directive, opts)
"ISOdate" -> Directive.get(:iso_date, directive, opts)
"ISOtime" -> Directive.get(:iso_time, directive, opts)
"ISOweek" -> Directive.get(:iso_week, directive, opts)
"ISOweek-day" -> Directive.get(:iso_weekday, directive, opts)
"ISOord" -> Directive.get(:iso_ordinal, directive, opts)
"RFC822" -> Directive.get(:rfc_822, directive, opts)
"RFC822z" -> Directive.get(:rfc_822z, directive, opts)
"RFC1123" -> Directive.get(:rfc_1123, directive, opts)
"RFC1123z" -> Directive.get(:rfc_1123z, directive, opts)
"RFC3339" -> Directive.get(:rfc_3339, directive, opts)
"RFC3339z" -> Directive.get(:rfc_3339z, directive, opts)
"ANSIC" -> Directive.get(:ansic, directive, opts)
"UNIX" -> Directive.get(:unix, directive, opts)
"ASN1:UTCtime" -> Directive.get(:asn1_utc_time, directive, opts)
"ASN1:GeneralizedTime" -> Directive.get(:asn1_generalized_time, directive, opts)
"ASN1:GeneralizedTime:Z" -> Directive.get(:asn1_generalized_time_z, directive, opts)
"ASN1:GeneralizedTime:TZ" -> Directive.get(:asn1_generalized_time_tz, directive, opts)
"kitchen" -> Directive.get(:kitchen, directive, opts)
t -> raise "invalid formatting directive #{t}"
end
end
defp set_width(min, max, type, directive, opts) do
case get_in(opts, [:flags, :padding]) do
pad_type when pad_type in [nil, :none] ->
opts = Keyword.merge(opts, width: [min: min, max: max])
Directive.get(type, directive, opts)
pad_type when pad_type in [:spaces, :zeroes] ->
opts = Keyword.merge(opts, width: [min: max, max: max])
Directive.get(type, directive, opts)
end
end
defp force_width(size, type, directive, opts) do
flags = Keyword.merge([padding: :zeroes], get_in(opts, [:flags]))
mods = get_in(opts, [:modifiers])
Directive.get(type, directive, flags: flags, modifiers: mods, width: [min: size, max: size])
end
defp map_literal([]), do: nil
defp map_literal(literals)
when is_list(literals),
do: Enum.map(literals, &map_literal/1)
defp map_literal(literal), do: %Directive{type: :literal, value: literal, parser: char(literal)}
@spec map_flag(binary) :: [{:padding, :spaces | :zeroes}] | []
defp map_flag("_"), do: [padding: :spaces]
defp map_flag("0"), do: [padding: :zeroes]
defp map_flag(_), do: []
end