Packages
timex
2.0.0
3.7.13
3.7.12
3.7.11
3.7.9
3.7.8
3.7.7
3.7.6
3.7.5
3.7.3
3.7.2
3.7.1
3.7.0
3.6.4
3.6.3
3.6.2
retired
3.6.1
3.6.0
3.5.0
3.4.2
3.4.1
3.3.0
3.2.2
3.2.1
3.2.0
3.1.25
retired
3.1.24
3.1.23
3.1.22
3.1.21
3.1.20
3.1.19
3.1.18
3.1.17
3.1.16
3.1.15
3.1.13
3.1.12
3.1.11
3.1.10
3.1.9
3.1.8
3.1.7
3.1.6
3.1.5
3.1.4
3.1.3
3.1.2
3.1.1
3.1.0
3.0.8
3.0.7
3.0.6
3.0.5
3.0.4
3.0.3
3.0.2
3.0.1
3.0.0
2.2.1
2.1.6
2.1.5
2.1.4
2.1.3
2.1.2
2.1.1
2.1.0
2.0.0
1.0.2
1.0.1
1.0.0
1.0.0-rc4
1.0.0-rc3
1.0.0-rc2
1.0.0-rc1
1.0.0-pre
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.2
0.18.1
0.18.0
0.17.0
0.16.2
0.16.1
0.16.0
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.5
0.13.4
0.13.3
0.13.2
0.13.1
0.13.0
0.12.9
0.12.8
0.12.7
0.12.6
0.12.5
0.12.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.0
0.10.2
0.10.1
0.10.0
0.9.0
0.8.0
0.7.1
0.6.0
0.5.0
0.4.8
0.4.7
0.4.6
Timex is a rich, comprehensive Date/Time library for Elixir projects, with full timezone support via the :tzdata package. If you need to manipulate dates, times, datetimes, timestamps, etc., then Timex is for you!
Current section
Files
Jump to
Current section
Files
lib/parse/datetime/parser.ex
defmodule Timex.Parse.DateTime.Parser do
@moduledoc """
This is the base plugin behavior for all Timex date/time string parsers.
"""
import Combine.Parsers.Base, only: [eof: 0, sequence: 1, map: 2, pipe: 2]
alias Timex.Date
alias Timex.DateTime
alias Timex.Time
alias Timex.Timezone
alias Timex.TimezoneInfo
alias Timex.Parse.ParseError
alias Timex.Parse.DateTime.Tokenizers.Directive
alias Timex.Parse.DateTime.Tokenizers.Default
alias Timex.Parse.DateTime.Tokenizers.Strftime
@doc """
Parses a date/time string using the default parser.
## Examples
iex> use Timex
...> {:ok, dt} = #{__MODULE__}.parse("2014-07-29T00:20:41.196Z", "{ISO:Extended:Z}")
...> dt.year
2014
iex> dt.month
7
iex> dt.day
29
iex> dt.timezone.full_name
"UTC"
"""
@spec parse(binary, binary) :: {:ok, DateTime.t} | {:error, term}
def parse(date_string, format_string)
when is_binary(date_string) and is_binary(format_string),
do: parse(date_string, format_string, Default)
def parse(_, _),
do: {:error, :badarg}
@doc """
Parses a date/time string using the provided tokenizer. Tokenizers must implement the
`Timex.Parse.DateTime.Tokenizer` behaviour.
## Examples
iex> use Timex
...> {:ok, dt} = #{__MODULE__}.parse("2014-07-29T00:30:41.196-02:00", "{ISO:Extended}", Timex.Parse.DateTime.Tokenizers.Default)
...> dt.year
2014
iex> dt.month
7
iex> dt.day
29
iex> dt.timezone.full_name
"Etc/GMT+2"
"""
@spec parse(binary, binary, atom) :: {:ok, DateTime.t} | {:error, term}
def parse(date_string, format_string, :strftime),
do: parse(date_string, format_string, Strftime)
def parse(date_string, format_string, tokenizer)
when is_binary(date_string) and is_binary(format_string)
do
case tokenizer.tokenize(format_string) do
{:error, err} -> {:error, {:format, err}}
{:ok, []} -> {:error, "There were no parsing directives in the provided format string."}
{:ok, directives} ->
case date_string do
"" -> {:error, "Input datetime string cannot be empty."}
_ -> do_parse(date_string, directives, tokenizer)
end
end
end
def parse(_, _, _), do: {:error, :badarg}
@doc """
Same as `parse/2` and `parse/3`, but raises on error.
"""
@spec parse!(String.t, String.t, atom | nil) :: DateTime.t | no_return
def parse!(date_string, format_string, tokenizer \\ Default)
when is_binary(date_string) and is_binary(format_string) and is_atom(tokenizer)
do
case parse(date_string, format_string, tokenizer) do
{:ok, result} -> result
{:error, reason} -> raise ParseError, message: reason
end
end
defp do_parse(str, directives, tokenizer) do
parsers = directives
|> Stream.map(fn %Directive{weight: weight, parser: parser} -> map(parser, &({&1, weight})) end)
|> Stream.filter(fn nil -> false; _ -> true end)
|> Enum.reverse
case Combine.parse(str, pipe([eof|parsers] |> Enum.reverse, &(&1))) do
[results] when is_list(results) ->
results
|> extract_parse_results
|> Stream.with_index
|> Enum.sort_by(fn
# If :force_utc exists, make sure it is applied last
{{{:force_utc, true}, _}, _} -> 9999
# Timezones must always be applied after other date/time tokens ->
{{{tz, _}, _}, _} when tz in [:zname, :zoffs, :zoffs_colon, :zoffs_sec] -> 9998
# If no weight is set, use the index as its weight
{{{_token, _value}, 0}, i} -> i
# Use the directive weight
{{{_token, _value}, weight}, _} -> weight
end)
|> Stream.flat_map(fn {{token, _}, _} -> [token] end)
|> Enum.filter(&Kernel.is_tuple/1)
|> apply_directives(tokenizer)
{:error, _} = err -> err
end
end
defp extract_parse_results(parse_results), do: extract_parse_results(parse_results, [])
defp extract_parse_results([], acc), do: Enum.reverse(acc)
defp extract_parse_results([{tokens, _}|rest], acc) when is_list(tokens) do
extracted = Enum.reverse(extract_parse_results(tokens))
extract_parse_results(rest, extracted ++ acc)
end
defp extract_parse_results([{{token, value}, weight}|rest], acc) when is_atom(token) do
extract_parse_results(rest, [{{token, value}, weight}|acc])
end
defp extract_parse_results([{token, value}|rest], acc) when is_atom(token) do
extract_parse_results(rest, [{{token, value}, 0}|acc])
end
defp extract_parse_results([[{token, value}]|rest], acc) when is_atom(token) do
extract_parse_results(rest, [{{token, value}, 0}|acc])
end
defp extract_parse_results([h|rest], acc) when is_list(h) do
extracted = Enum.reverse(extract_parse_results(h))
extract_parse_results(rest, extracted ++ acc)
end
defp extract_parse_results([_|rest], acc) do
extract_parse_results(rest, acc)
end
# Constructs a DateTime from the parsed tokens
defp apply_directives([], _), do: {:ok, %DateTime{}}
defp apply_directives(tokens, tokenizer), do: apply_directives(tokens, %DateTime{}, tokenizer)
defp apply_directives([], %DateTime{timezone: nil} = date, tokenizer) do
apply_directives([], %{date | :timezone => %TimezoneInfo{}}, tokenizer)
end
defp apply_directives([], %DateTime{} = date, _), do: {:ok, date}
defp apply_directives([{token, value}|tokens], %DateTime{} = date, tokenizer) do
case update_date(date, token, value, tokenizer) do
{:error, _} = error -> error
updated -> apply_directives(tokens, updated, tokenizer)
end
end
# Given a date, a token, and the value for that token, update the
# date according to the rules for that token and the provided value
defp update_date(%DateTime{year: year, hour: hh} = date, token, value, tokenizer) when is_atom(token) do
case token do
# Formats
clock when clock in [:kitchen, :strftime_iso_kitchen] ->
case apply_directives(value, DateTime.now, tokenizer) do
{:error, _} = err -> err
{:ok, date} when clock == :kitchen -> %{date | :second => 0, :millisecond => 0}
{:ok, date} -> %{date | :millisecond => 0}
end
# Years
:century ->
century = Timex.century(%{date | :year => year})
year_shifted = year + ((value - century) * 100)
%{date | :year => year_shifted}
y when y in [:year2, :iso_year2] ->
current_century = Timex.century(DateTime.now)
year_shifted = value + ((current_century - 1) * 100)
%{date | :year => year_shifted}
y when y in [:year4, :iso_year4] ->
# Special case for UNIX format dates, where the year is parsed after the timezone,
# so we must lookup the timezone again to ensure it's properly set
case date do
%DateTime{timezone: %Timex.TimezoneInfo{:full_name => tzname}} ->
zone_date = Timex.to_erlang_datetime(%{date | :year => value})
%{date | :year => value, :timezone => Timezone.get(tzname, zone_date)}
%DateTime{timezone: nil} ->
%{date | :year => value}
end
# Months
:month -> %{date | :month => value}
month when month in [:mshort, :mfull] ->
%{date | :month => Timex.month_to_num(value)}
# Days
:day -> %{date | :day => value}
:oday when is_integer(value) and value >= 0 ->
Timex.from_iso_day(value, date)
:wday_mon ->
current_day = Date.weekday(date)
cond do
current_day == value -> date
current_day > value -> Timex.shift(date, days: current_day - value)
current_day < value -> Timex.shift(date, days: value - current_day)
end
:wday_sun ->
current_day = Date.weekday(date) - 1
cond do
current_day == value -> date
current_day > value -> Timex.shift(date, days: current_day - value)
current_day < value -> Timex.shift(date, days: value - current_day)
end
day when day in [:wdshort, :wdfull] ->
%{date | :day => Timex.day_to_num(value)}
# Weeks
:iso_weeknum ->
{year, _, weekday} = Timex.iso_triplet(date)
%DateTime{year: y, month: m, day: d} = Timex.from_iso_triplet({year, value, weekday})
%{date | :year => y, :month => m, :day => d}
week_num when week_num in [:week_mon, :week_sun] ->
reset = %{date | :month => 1, :day => 1}
reset |> Timex.shift(weeks: value)
# Hours
hour when hour in [:hour24, :hour12] ->
%{date | :hour => value}
:min -> %{date | :minute => value}
:sec -> %{date | :second => value}
:sec_fractional ->
case value do
"" -> date
n when is_number(n) -> %{date | :millisecond => n}
end
:us -> %{date | :millisecond => div(value, 1000)}
:sec_epoch -> DateTime.from_seconds(value, :epoch)
am_pm when am_pm in [:am, :AM] ->
{converted, hemisphere} = Time.to_12hour_clock(hh)
case value do
am when am in ["am", "AM"]->
%{date | :hour => converted}
pm when pm in ["pm", "PM"] and hemisphere == :am ->
cond do
converted + 12 == 24 -> %{date | :hour => 0}
:else -> %{date | :hour => converted + 12}
end
_ ->
%{date | :hour => converted}
end
# Timezones
:zoffs ->
zone_date = Timex.to_erlang_datetime(date)
%{date | :timezone => Timezone.get(value, zone_date)}
:zname ->
zone_date = Timex.to_erlang_datetime(date)
%{date | :timezone => Timezone.get(value, zone_date)}
tz when tz in [:zoffs_colon, :zoffs_sec] ->
case value do
<<?-, h1::utf8, h2::utf8, _::binary>> ->
zone_date = Timex.to_erlang_datetime(date)
%{date | :timezone => Timezone.get(<<?-, h1::utf8, h2::utf8>>, zone_date)}
<<?+, h1::utf8, h2::utf8, _::binary>> ->
zone_date = Timex.to_erlang_datetime(date)
%{date | :timezone => Timezone.get(<<?+, h1::utf8, h2::utf8>>, zone_date)}
_ ->
{:error, "#{token} not implemented"}
end
:force_utc ->
case date do
%DateTime{timezone: nil} -> %{date | :timezone => %Timex.TimezoneInfo{}}
_ -> Timezone.convert(date, "UTC")
end
:literal -> date
_ ->
case tokenizer.apply(date, token, value) do
{:ok, date} -> date
{:error, _} = err -> err
_ ->
{:error, "Unrecognized token: #{token}"}
end
end
end
end