Current section

Files

Jump to
timex lib parse datetime tokenizers default.ex
Raw

lib/parse/datetime/tokenizers/default.ex

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) :: [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) :: {:ok, 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", "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)
"AM" -> Directive.get(:AM, directive, opts)
# 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" -> Directive.get(:iso_8601, directive, opts)
"ISOz" -> Directive.get(:iso_8601z, directive, opts)
"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)
"kitchen" -> Directive.get(:kitchen, directive, opts)
end
end
defp set_width(min, max, type, directive, opts) do
opts = Keyword.merge(opts, [width: [min: min, max: max]])
Directive.get(type, directive, opts)
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