Current section

Files

Jump to
timex lib parse datetime parsers iso8601_extended.ex
Raw

lib/parse/datetime/parsers/iso8601_extended.ex

defmodule Timex.Parse.DateTime.Parsers.ISO8601Extended do
use Combine.Helpers
alias Combine.ParserState
defparser parse(%ParserState{status: :ok, column: col, input: input, results: results} = state) do
case parse_extended(input) do
{:ok, parts, len, remaining} ->
%{state | :column => col + len, :input => remaining, :results => [Enum.reverse(parts)|results]}
{:error, reason, count} ->
%{state | :status => :error, :column => col + count, :error => reason}
end
end
def parse_extended(<<>>), do: {:error, "Expected year, but got end of input."}
def parse_extended(input), do: parse_extended(input, :year, [], 0)
def parse_extended(<<y1::utf8,y2::utf8,y3::utf8,y4::utf8,"-",rest::binary>>, :year, acc, count) when
y1 >= ?0 and y1 <= ?9 and
y2 >= ?0 and y2 <= ?9 and
y3 >= ?0 and y3 <= ?9 and
y4 >= ?0 and y4 <= ?9 do
year = String.to_integer(<<y1::utf8,y2::utf8,y3::utf8,y4::utf8>>)
parse_extended(rest, :month, [{:year4, year}|acc], count+4)
end
def parse_extended(<<h::utf8,_::binary>>, :year, _acc, count), do: {:error, "Expected 4 digit year, but got `#{<<h::utf8>>}` instead.", count}
def parse_extended(<<m1::utf8,m2::utf8,"-",rest::binary>>, :month, acc, count) when
m1 >= ?0 and m1 < ?2 and
m2 >= ?0 and m2 <= ?9 do
month = String.to_integer(<<m1::utf8,m2::utf8>>)
cond do
month > 0 and month < 13 ->
parse_extended(rest, :day, [{:month, month}|acc], count+2)
:else ->
{:error, "Expected month between 1-12, but got `#{month}` instead.", count}
end
end
def parse_extended(<<h::utf8,_::binary>>, :month, _acc, count), do: {:error, "Expected 2 digit month, but got `#{<<h::utf8>>}` instead.", count}
def parse_extended(<<d1::utf8,d2::utf8,sep::utf8,rest::binary>>, :day, acc, count) when
d1 >= ?0 and d1 <= ?3 and
d2 >= ?0 and d2 <= ?9 do
cond do
sep in [?T,?\s] ->
day = String.to_integer(<<d1::utf8,d2::utf8>>)
cond do
day > 0 and day < 32 ->
parse_extended(rest, :hour, [{:day, day}|acc], count+3)
:else ->
{:error, "Expected day between 1-31, but got `#{day}` instead.", count}
end
:else ->
{:error, "Expected valid date/time separator (T or space), but got `#{<<sep::utf8>>}` instead.", count+2}
end
end
def parse_extended(<<h::utf8,_::binary>>, :day, _acc, count), do: {:error, "Expected 2 digit day, but got `#{<<h::utf8>>}` instead.", count}
def parse_extended(<<h1::utf8,h2::utf8,rest::binary>>, :hour, acc, count) when
h1 >= ?0 and h1 < ?3 and
h2 >= ?0 and h2 <= ?9 do
hour = String.to_integer(<<h1::utf8,h2::utf8>>)
cond do
hour >= 0 and hour <= 24 ->
case rest do
<<":",rest::binary>> ->
parse_extended(rest, :minute, [{:hour24, hour}|acc], count+3)
_ ->
parse_offset(rest, [{:hour24, hour}|acc], count+2)
end
:else ->
{:error, "Expected hour between 0-24, but got `#{hour}` instead.", count}
end
end
def parse_extended(<<h::utf8,_::binary>>, :hour, _acc, count), do: {:error, "Expected 2 digit hour, but got `#{<<h::utf8>>}` instead.", count}
# Minutes are optional
def parse_extended(<<m1::utf8,m2::utf8,rest::binary>>, :minute, acc, count) when
m1 >= ?0 and m1 < ?6 and
m2 >= ?0 and m2 <= ?9 do
minute = String.to_integer(<<m1::utf8,m2::utf8>>)
cond do
minute >= 0 and minute <= 60 ->
case rest do
<<":",rest::binary>> ->
parse_extended(rest, :second, [{:min, minute}|acc], count+3)
_ ->
parse_offset(rest, [{:min, minute}|acc], count+2)
end
:else ->
{:error, "Expected minute between 0-60, but got `#{minute}` instead.", count}
end
end
def parse_extended(<<h::utf8,_::binary>>, :minute, _acc, count), do: {:error, "Expected 2 digit minute, but got `#{<<h::utf8>>}` instead.", count}
# Seconds are optional
def parse_extended(<<s1::utf8,s2::utf8,".",rest::binary>>, :second, acc, count) when # Has fractional seconds
s1 >= ?0 and s1 < ?6 and
s2 >= ?0 and s2 <= ?9 do
case parse_fractional_seconds(rest, count, <<>>) do
{:ok, fraction, count, rest} ->
seconds = String.to_integer(<<s1::utf8,s2::utf8>>)
precision = byte_size(fraction)
fraction = if precision > 6, do: binary_part(fraction, 0, 6), else: fraction
precision = if precision > 6, do: 6, else: precision
fractional = String.to_integer(fraction)
fractional = fractional * div(1_000_000, trunc(:math.pow(10, precision)))
cond do
seconds >= 0 and seconds <= 60 ->
parse_offset(rest, [{:sec_fractional, {fractional, precision}}, {:sec, seconds}|acc], count+2)
:else ->
{:error, "Expected second between 0-60, but got `#{seconds}` instead.", count}
end
{:error, _reason, _count} = err ->
err
end
end
def parse_extended(<<s1::utf8,s2::utf8,rest::binary>>, :second, acc, count) when # No fractional seconds
s1 >= ?0 and s1 < ?6 and
s2 >= ?0 and s2 <= ?9 do
second = String.to_integer(<<s1::utf8,s2::utf8>>)
cond do
second >= 0 and second <= 60 ->
parse_offset(rest, [{:sec, second}|acc], count+2)
:else ->
{:error, "Expected second between 0-60, but got `#{second}` instead.", count}
end
end
def parse_extended(<<h::utf8,_::binary>>, :second, _acc, count), do: {:error, "Expected valid value for seconds, but got `#{<<h::utf8>>}` instead.", count}
def parse_fractional_seconds(<<digit::utf8,rest::binary>>, count, acc) when
digit >= ?0 and digit <= ?9 do
parse_fractional_seconds(rest, count+1, <<acc::binary,digit::utf8>>)
end
def parse_fractional_seconds(rest, count, acc) do
{:ok, acc, count, rest}
end
def parse_offset(<<"Z",rest::binary>>, acc, count), do: {:ok, [{:zname, "Etc/UTC"}|acc], count+1, rest}
def parse_offset(<<dir::utf8,rest::binary>>, acc, count) when dir in [?+,?-] do
parse_offset(dir, rest, acc, count+1)
end
def parse_offset("", acc, count), do: {:ok, acc, count, ""}
def parse_offset(str, _acc, count), do: {:error, "Expected either Z or a valid timezone offset, but got `#{str}`", count}
# +/-HH:MM:SS (seconds are currently unhandled in offsets)
def parse_offset(dir, <<h1::utf8,h2::utf8,":",m1::utf8,m2::utf8,":",s1::utf8,s2::utf8,rest::binary>>, acc, count) when
h1 >= ?0 and h1 < ?2 and
h2 >= ?0 and h2 <= ?9 and
m1 >= ?0 and m1 < ?6 and
m2 >= ?0 and m2 <= ?9 and
s1 >= ?0 and s1 < ?6 and
s2 >= ?0 and s2 <= ?9 do
{:ok, [{:zname, <<dir::utf8,h1::utf8,h2::utf8,":",m1::utf8,m2::utf8>>}|acc], count+7, rest}
end
# +/-HH:MM
def parse_offset(dir, <<h1::utf8,h2::utf8,":",m1::utf8,m2::utf8,rest::binary>>, acc, count) when
h1 >= ?0 and h1 < ?2 and
h2 >= ?0 and h2 <= ?9 and
m1 >= ?0 and m1 < ?6 and
m2 >= ?0 and m2 <= ?9 do
{:ok, [{:zname, <<dir::utf8,h1::utf8,h2::utf8,":",m1::utf8,m2::utf8>>}|acc], count+5, rest}
end
# +/-HHMM
def parse_offset(dir, <<h1::utf8,h2::utf8,m1::utf8,m2::utf8,rest::binary>>, acc, count) when
h1 >= ?0 and h1 < ?2 and
h2 >= ?0 and h2 <= ?9 and
m1 >= ?0 and m1 < ?6 and
m2 >= ?0 and m2 <= ?9 do
{:ok, [{:zname, <<dir::utf8,h1::utf8,h2::utf8,":",m1::utf8,m2::utf8>>}|acc], count+5, rest}
end
# +/-HH
def parse_offset(dir, <<h1::utf8,h2::utf8,rest::binary>>, acc, count) when
h1 >= ?0 and h1 < ?2 and
h2 >= ?0 and h2 <= ?9 do
{:ok, [{:zname, <<dir::utf8,h1::utf8,h2::utf8,":00">>}|acc], count+2, rest}
end
def parse_offset(_, <<h::utf8,_rest::binary>>, _acc, count), do: {:error, "Expected valid offset, but got `#{<<h::utf8>>}` instead.", count}
end