Packages
timex
0.19.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.Time
alias Timex.DateTime
alias Timex.TimezoneInfo
alias Timex.Timezone
alias Timex.Parse.ParseError
alias Timex.Parse.DateTime.Tokenizers.Directive
alias Timex.Parse.DateTime.Tokenizers.Default, as: DefaultTokenizer
@doc """
Parses a date/time string using the default parser.
## Examples
iex> use Timex
...> {:ok, dt} = #{__MODULE__}.parse("2014-07-29T00:20:41.196Z", "{ISOz}")
...> dt.year
2014
...> dt.month
7
...> dt.day
29
...> dt.timezone.full_name
"UTC"
"""
@spec parse(binary, binary) :: {:ok, %DateTime{}} | {:error, term}
def parse(date_string, format_string)
when is_binary(date_string) and is_binary(format_string),
do: parse(date_string, format_string, DefaultTokenizer)
@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-0200", "{ISO}", Timex.Parse.DateTime.Tokenizers.Default)
...> dt.year
2014
...> dt.month
7
...> dt.day
29
...> dt.timezone.full_name
"Etc/GMT+2"
"""
@spec parse(binary, binary, atom) :: {:ok, %DateTime{}} | {:error, term}
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
@doc """
Same as `parse/2` and `parse/3`, but raises on error.
"""
@spec parse!(String.t, String.t, atom | nil) :: %DateTime{} | no_return
def parse!(date_string, format_string, tokenizer \\ DefaultTokenizer)
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
|> Stream.with_index
|> Enum.sort_by(fn
# If :force_utc exists, make sure it is applied last
{{[force_utc: true], _}, _} -> 9999
# If weight is zeroed, use the index as it's weight
{{_token, 0}, i} -> i
# Otherwise use the weight supplied by the directive
{{_token, weight}, _} -> weight
end)
|> Stream.flat_map(fn
{{token, _}, _} when is_list(token) -> List.flatten(token);
{{token, _}, _} -> [token]
end)
|> Enum.filter(&Kernel.is_tuple/1)
|> apply_directives(tokenizer)
{:error, _} = err -> err
end
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} = 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, Date.now, tokenizer) do
{:error, _} = err -> err
{:ok, date} when clock == :kitchen -> %{date | :second => 0, :ms => 0}
{:ok, date} -> %{date | :ms => 0}
end
# Years
:century ->
century = Date.century(%{date | :year => year})
year_shifted = year + ((value - century) * 100)
%{date | :year => year_shifted}
y when y in [:year2, :iso_year2] ->
current_century = Date.century(Date.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.timezone do
%Timex.TimezoneInfo{:full_name => tzname} ->
zone_date = {{value, date.month, date.day}, {date.hour, date.minute, date.second}}
%{date | :year => value, :timezone => Timezone.get(tzname, zone_date)}
nil ->
%{date | :year => value}
end
# Months
:month -> %{date | :month => value}
month when month in [:mshort, :mfull] ->
%{date | :month => Date.month_to_num(value)}
# Days
:day -> %{date | :day => value}
:oday -> Date.from_iso_day(value, date)
:wday_mon ->
current_day = Date.weekday(date)
cond do
current_day == value -> date
current_day > value -> Date.shift(date, days: current_day - value)
current_day < value -> Date.shift(date, days: value - current_day)
end
:wday_sun ->
current_day = Date.weekday(date) - 1
cond do
current_day == value -> date
current_day > value -> Date.shift(date, days: current_day - value)
current_day < value -> Date.shift(date, days: value - current_day)
end
day when day in [:wdshort, :wdfull] ->
%{date | :day => Date.day_to_num(value)}
# Weeks
:iso_weeknum ->
{year, _, weekday} = Date.iso_triplet(date)
converted = Date.from_iso_triplet({year, value, weekday})
%{date | :year => converted.year, :month => converted.month, :day => converted.day}
week_num when week_num in [:week_mon, :week_sun] ->
reset = %{date | :month => 1, :day => 1}
reset |> Date.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 | :ms => n}
end
:sec_epoch -> Date.from(value, :secs, :epoch)
am_pm when am_pm in [:am, :AM] ->
{converted, hemisphere} = Time.to_12hour_clock(date.hour)
case value do
am when am in ["am", "AM"]->
%{date | :hour => converted}
pm when pm in ["pm", "PM"] and hemisphere == :am ->
%{date | :hour => converted + 12}
_ ->
%{date | :hour => converted}
end
# Timezones
:zoffs ->
zone_date = {{date.year, date.month, date.day}, {date.hour, date.minute, date.second}}
%{date | :timezone => Timezone.get(value, zone_date)}
:zname ->
zone_date = {{date.year, date.month, date.day}, {date.hour, date.minute, date.second}}
%{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 = {{date.year, date.month, date.day}, {date.hour, date.minute, date.second}}
%{date | :timezone => Timezone.get(<<?-, h1::utf8, h2::utf8>>, zone_date)}
<<?+, h1::utf8, h2::utf8, _::binary>> ->
zone_date = {{date.year, date.month, date.day}, {date.hour, date.minute, date.second}}
%{date | :timezone => Timezone.get(<<?+, h1::utf8, h2::utf8>>, zone_date)}
_ ->
{:error, "#{token} not implemented"}
end
:force_utc ->
case date.timezone do
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