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( <>, :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(<>) parse_extended(rest, :month, [{:year4, year} | acc], count + 4) end def parse_extended(<>, :year, _acc, count), do: {:error, "Expected 4 digit year, but got `#{<>}` instead.", count} def parse_extended(_, :year, _acc, count), do: {:error, "Expected 4 digit year.", count} def parse_extended(<>, :month, acc, count) when m1 >= ?0 and m1 < ?2 and m2 >= ?0 and m2 <= ?9 do month = String.to_integer(<>) 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(<>, :month, _acc, count), do: {:error, "Expected 2 digit month, but got `#{<>}` instead.", count} def parse_extended(_, :month, _acc, count), do: {:error, "Expected 2 digit month.", count} def parse_extended(<>, :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(<>) 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 `#{<>}` instead.", count + 2} end end def parse_extended(<>, :day, _acc, count), do: {:error, "Expected 2 digit day, but got `#{<>}` instead.", count} def parse_extended(_, :day, _acc, count), do: {:error, "Expected 2 digit day.", count} def parse_extended(<>, :hour, acc, count) when h1 >= ?0 and h1 < ?3 and h2 >= ?0 and h2 <= ?9 do hour = String.to_integer(<>) 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(<>, :hour, _acc, count), do: {:error, "Expected 2 digit hour, but got `#{<>}` instead.", count} def parse_extended(_, :hour, _acc, count), do: {:error, "Expected 2 digit hour.", count} # Minutes are optional def parse_extended(<>, :minute, acc, count) when m1 >= ?0 and m1 < ?6 and m2 >= ?0 and m2 <= ?9 do minute = String.to_integer(<>) 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(<>, :minute, _acc, count), do: {:error, "Expected 2 digit minute, but got `#{<>}` instead.", count} def parse_extended(_, :minute, _acc, count), do: {:error, "Expected 2 digit minute.", count} # Seconds are optional # Has fractional seconds def parse_extended(<>, :second, acc, count) when 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(<>) 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 # No fractional seconds def parse_extended(<>, :second, acc, count) when s1 >= ?0 and s1 < ?6 and s2 >= ?0 and s2 <= ?9 do second = String.to_integer(<>) 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(<>, :second, _acc, count), do: {:error, "Expected valid value for seconds, but got `#{<>}` instead.", count} def parse_extended(_, :second, _acc, count), do: {:error, "Expected valid value for seconds.", count} def parse_fractional_seconds(<>, count, acc) when digit >= ?0 and digit <= ?9 do parse_fractional_seconds(rest, count + 1, <>) end def parse_fractional_seconds(_rest, count, "") do {:error, "Expected at least one digit after the decimal sign, but found none", count} 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(<>, 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, <>, 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, <>} | acc], count + 7, rest} end # +/-HH:MM def parse_offset(dir, <>, 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, <>} | acc], count + 5, rest} end # +/-HHMM def parse_offset(dir, <>, 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, <>} | acc], count + 5, rest} end # +/-HH def parse_offset(dir, <>, acc, count) when h1 >= ?0 and h1 < ?2 and h2 >= ?0 and h2 <= ?9 do {:ok, [{:zname, <>} | acc], count + 2, rest} end def parse_offset(_, <>, _acc, count), do: {:error, "Expected valid offset, but got `#{<>}` instead.", count} end