1defmodule Calendar.NaiveDateTime.Parse do
2  import Calendar.ParseUtil
3
4  @doc """
5  Parse ASN.1 GeneralizedTime.
6
7  Returns tuple with {:ok, [NaiveDateTime], UTC offset (optional)}
8
9  ## Examples
10
11      iex> "19851106210627.3" |> asn1_generalized
12      {:ok, %NaiveDateTime{year: 1985, month: 11, day: 6, hour: 21, minute: 6, second: 27, microsecond: {300_000, 1}}, nil}
13      iex> "19851106210627.3Z" |> asn1_generalized
14      {:ok, %NaiveDateTime{year: 1985, month: 11, day: 6, hour: 21, minute: 6, second: 27, microsecond: {300_000, 1}}, 0}
15      iex> "19851106210627.3-5000" |> asn1_generalized
16      {:ok, %NaiveDateTime{year: 1985, month: 11, day: 6, hour: 21, minute: 6, second: 27, microsecond: {300_000, 1}}, -180000}
17  """
18  def asn1_generalized(string) do
19    captured = string |> capture_generalized_time_string
20    if captured do
21      parse_captured_iso8601(captured, captured["z"], captured["offset_hours"], captured["offset_mins"])
22    else
23      {:bad_format, nil, nil}
24    end
25  end
26  defp capture_generalized_time_string(string) do
27    ~r/(?<year>[\d]{4})(?<month>[\d]{2})(?<day>[\d]{2})(?<hour>[\d]{2})(?<min>[\d]{2})(?<sec>[\d]{2})(\.(?<fraction>[\d]+))?(?<z>[zZ])?((?<offset_sign>[\+\-])(?<offset_hours>[\d]{1,2})(?<offset_mins>[\d]{2}))?/
28    |> Regex.named_captures(string)
29  end
30
31  @doc """
32  Parses a "C time" string.
33
34  ## Examples
35      iex> Calendar.NaiveDateTime.Parse.asctime("Wed Apr  9 07:53:03 2003")
36      {:ok, %NaiveDateTime{year: 2003, month: 4, day: 9, hour: 7, minute: 53, second: 3, microsecond: {0, 0}}}
37      iex> asctime("Thu, Apr 10 07:53:03 2003")
38      {:ok, %NaiveDateTime{year: 2003, month: 4, day: 10, hour: 7, minute: 53, second: 3, microsecond: {0, 0}}}
39  """
40  def asctime(string) do
41    cap = capture_asctime_string(string)
42    month_num = month_number_for_month_name(cap["month"])
43    Calendar.NaiveDateTime.from_erl({{cap["year"]|>to_int, month_num, cap["day"]|>to_int}, {cap["hour"]|>to_int, cap["min"]|>to_int, cap["sec"]|>to_int}})
44  end
45
46  @doc """
47  Like `asctime/1`, but returns the result without tagging it with :ok.
48
49  ## Examples
50      iex> asctime!("Wed Apr  9 07:53:03 2003")
51      %NaiveDateTime{year: 2003, month: 4, day: 9, hour: 7, minute: 53, second: 3, microsecond: {0, 0}}
52      iex> asctime!("Thu, Apr 10 07:53:03 2003")
53      %NaiveDateTime{year: 2003, month: 4, day: 10, hour: 7, minute: 53, second: 3, microsecond: {0, 0}}
54  """
55  def asctime!(string) do
56    {:ok, result} = asctime(string)
57    result
58  end
59
60  defp capture_asctime_string(string) do
61    ~r/(?<month>[^\d]{3})[\s]+(?<day>[\d]{1,2})[\s]+(?<hour>[\d]{2})[^\d]?(?<min>[\d]{2})[^\d]?(?<sec>[\d]{2})[^\d]?(?<year>[\d]{4})/
62    |> Regex.named_captures(string)
63  end
64
65  @doc """
66  Parses an ISO8601 datetime. Returns {:ok, NaiveDateTime struct, UTC offset in secods}
67  In case there is no UTC offset, the third element of the tuple will be nil.
68
69  ## Examples
70
71      # With offset
72      iex> iso8601("1996-12-19T16:39:57-0200")
73      {:ok, %NaiveDateTime{year: 1996, month: 12, day: 19, hour: 16, minute: 39, second: 57, microsecond: {0, 0}}, -7200}
74
75      # Without offset
76      iex> iso8601("1996-12-19T16:39:57")
77      {:ok, %NaiveDateTime{year: 1996, month: 12, day: 19, hour: 16, minute: 39, second: 57, microsecond: {0, 0}}, nil}
78
79      # With fractional seconds
80      iex> iso8601("1996-12-19T16:39:57.123")
81      {:ok, %NaiveDateTime{year: 1996, month: 12, day: 19, hour: 16, minute: 39, second: 57, microsecond: {123000, 3}}, nil}
82
83      # With fractional seconds
84      iex> iso8601("1996-12-19T16:39:57,123")
85      {:ok, %NaiveDateTime{year: 1996, month: 12, day: 19, hour: 16, minute: 39, second: 57, microsecond: {123000, 3}}, nil}
86
87      # With Z denoting 0 offset
88      iex> iso8601("1996-12-19T16:39:57Z")
89      {:ok, %NaiveDateTime{year: 1996, month: 12, day: 19, hour: 16, minute: 39, second: 57, microsecond: {0, 0}}, 0}
90
91      # Invalid date
92      iex> iso8601("1996-13-19T16:39:57Z")
93      {:error, :invalid_datetime, nil}
94  """
95  def iso8601(string) do
96    captured = capture_iso8601_string(string)
97    if captured do
98      parse_captured_iso8601(captured, captured["z"], captured["offset_hours"], captured["offset_mins"])
99    else
100      {:bad_format, nil, nil}
101    end
102  end
103
104  defp parse_captured_iso8601(captured, z, _, _) when z != "" do
105    parse_captured_iso8601(captured, "", "00", "00")
106  end
107  defp parse_captured_iso8601(captured, _z, "", "") do
108    {tag, ndt} = Calendar.NaiveDateTime.from_erl(erl_date_time_from_regex_map(captured), parse_fraction(captured["fraction"]))
109    {tag, ndt, nil}
110  end
111  defp parse_captured_iso8601(captured, _z, offset_hours, offset_mins) do
112    {tag, ndt} = Calendar.NaiveDateTime.from_erl(erl_date_time_from_regex_map(captured), parse_fraction(captured["fraction"]))
113    if tag == :ok do
114      {:ok, offset_in_seconds} = offset_from_captured(captured, offset_hours, offset_mins)
115      {tag, ndt, offset_in_seconds}
116    else
117      {tag, ndt, nil}
118    end
119  end
120
121  defp offset_from_captured(captured, offset_hours, offset_mins) do
122    offset_in_secs = hours_mins_to_secs!(offset_hours, offset_mins)
123    offset_in_secs = case captured["offset_sign"] do
124      "-" -> offset_in_secs*-1
125      _   -> offset_in_secs
126    end
127    {:ok, offset_in_secs}
128  end
129
130  defp capture_iso8601_string(string) do
131    ~r/(?<year>[\d]{4})[^\d]?(?<month>[\d]{2})[^\d]?(?<day>[\d]{2})[^\d](?<hour>[\d]{2})[^\d]?(?<min>[\d]{2})[^\d]?(?<sec>[\d]{2})([\.\,](?<fraction>[\d]+))?(?<z>[zZ])?((?<offset_sign>[\+\-])(?<offset_hours>[\d]{1,2}):?(?<offset_mins>[\d]{2}))?/
132    |> Regex.named_captures(string)
133  end
134
135  defp erl_date_time_from_regex_map(mapped) do
136    erl_date_time_from_strings({{mapped["year"],mapped["month"],mapped["day"]},{mapped["hour"],mapped["min"],mapped["sec"]}})
137  end
138
139  defp erl_date_time_from_strings({{year, month, date},{hour, min, sec}}) do
140    { {year|>to_int, month|>to_int, date|>to_int},
141      {hour|>to_int, min|>to_int, sec|>to_int} }
142  end
143
144  defp parse_fraction(""), do: {0, 0}
145  # parse and return microseconds
146  defp parse_fraction(string) do
147    usec = String.slice(string, 0..5)
148    |> String.pad_trailing(6, "0")
149    |> Integer.parse
150    |> elem(0)
151    {usec, String.length(string)}
152  end
153end
154