1defmodule Tzdata.Parser do
2  @moduledoc false
3
4  require Tzdata.Util
5  import Tzdata.Util
6  def read_file(file_name, dir_prepend) do
7    File.stream!("#{dir_prepend}/#{file_name}")
8    |> process_file
9  end
10
11  def process_file(file_stream) do
12    file_stream
13    |> filter_comment_lines
14    |> filter_empty_lines
15    |> Stream.map(fn string -> strip_comment(string) end) # Strip comments at line end. Like this comment.
16    |> Enum.to_list
17    |> process_tz_list
18  end
19
20  def process_tz_list([]), do: []
21  def process_tz_list([ head | tail ]) do
22    split = String.split(head, ~r{\s})
23    case hd(split) do
24      "Rule" -> [process_rule(head)|process_tz_list(tail)]
25      "Link" -> [process_link(head)|process_tz_list(tail)]
26      "Zone" -> process_zone([head|tail])
27      ______ -> [head|process_tz_list(tail)] # pass through
28    end
29  end
30
31  def process_rule(line) do
32    rule_regex = ~r/Rule[\s]+(?<name>[^\s]+)[\s]+(?<from>[^\s]+)[\s]+(?<to>[^\s]+)[\s]+(?<type>[^\s]+)[\s]+(?<in>[^\s]+)[\s]+(?<on>[^\s]+)[\s]+(?<at>[^\s]+)[\s]+(?<save>[^\s]+)[\s]+(?<letter>[^\n]+)/
33    captured = Regex.named_captures(rule_regex, line)
34    captured = %{name: captured["name"],
35                 from: captured["from"] |> to_int,
36                 to: captured["to"] |> process_rule_to,
37                 type: captured["type"], # we don't use this column for anything
38                 in: captured["in"] |> month_number_for_month_name,
39                 on: captured["on"],
40                 at: captured["at"] |> transform_rule_at,
41                 save: captured["save"] |> string_amount_to_secs,
42                 letter: captured["letter"]}
43    Map.merge(captured, %{record_type: :rule})
44  end
45
46  # process "to" value of rule
47  defp process_rule_to("only"), do: :only
48  defp process_rule_to("max"), do: :max
49  defp process_rule_to(val), do: val |> to_int
50
51  def process_link(line) do
52    link_regex = ~r/Link[\s]+(?<from>[^\s]+)[\s]+(?<to>[^\s]+)/
53    captured = Regex.named_captures(link_regex, line)
54    %{record_type: :link, from: captured["from"], to: captured["to"]}
55  end
56
57  def process_zone(:head_no_until, captured, [head|tail]) do
58    name = captured["name"]
59    captured = captured_zone_map_clean_up(captured)
60    [%{record_type: :zone, name: name, zone_lines: [captured]}|process_tz_list([head|tail])]
61  end
62
63  def process_zone(:head_no_until, captured, []) do
64    name = captured["name"]
65    captured = captured_zone_map_clean_up(captured)
66    [%{record_type: :zone, name: name, zone_lines: [captured]}]
67  end
68
69  def process_zone(:head_with_until, captured, [head|tail]) do
70    name = captured["name"]
71    captured = captured_zone_map_clean_up(captured)
72    {line_type, new_capture} = zone_mapped(head)
73    process_zone(line_type, new_capture, name, [captured], tail)
74  end
75
76  def process_zone(:continuation_with_until, captured, zone_name, zone_lines, [head|tail]) do
77    captured = captured_zone_map_clean_up(captured)
78    zone_lines = zone_lines ++ [captured]
79    {line_type, new_capture} = zone_mapped(head)
80    process_zone(line_type, new_capture, zone_name, zone_lines, tail)
81  end
82
83  def process_zone(:continuation_no_until, captured, zone_name, zone_lines, [head|tail]) do
84    captured = captured_zone_map_clean_up(captured)
85    zone_lines = zone_lines ++ [captured]
86    [%{record_type: :zone, name: zone_name, zone_lines: zone_lines}|process_tz_list([head|tail])]
87  end
88
89  def process_zone(:continuation_no_until, captured, zone_name, zone_lines, []) do
90    captured = captured_zone_map_clean_up(captured)
91    zone_lines = zone_lines ++ [captured]
92    [%{record_type: :zone, name: zone_name, zone_lines: zone_lines}]
93  end
94
95  def process_zone([head|tail]) do
96    {line_type, captured} = zone_mapped(head)
97    process_zone(line_type, captured, tail)
98  end
99
100  def zone_mapped(line) do
101    # I use the term "head" in this context as the first line of a zone
102    # definition. So it will start with "Zone"
103    zone_line_regex = [
104      {:head_with_until, ~r/Zone[\s]+(?<name>[^\s]+)[\s]+(?<gmtoff>[^\s]+)[\s]+(?<rules>[^\s]+)[\s]+(?<format>[^\s]+)[\s]+(?<until>[^\n]+)/},
105      {:head_no_until, ~r/Zone[\s]+(?<name>[^\s]+)[\s]+(?<gmtoff>[^\s]+)[\s]+(?<rules>[^\s]+)[\s]+(?<format>[^\s]+)/},
106      {:continuation_with_until, ~r/[\s]+(?<gmtoff>[^\s]+)[\s]+(?<rules>[^\s]+)[\s]+(?<format>[^\s]+)[\s]+(?<until>[^\n]+)/},
107      {:continuation_no_until, ~r/[\s]+(?<gmtoff>[^\s]+)[\s]+(?<rules>[^\s]+)[\s]+(?<format>[^\s]+)/},
108    ]
109    zone_mapped(line, zone_line_regex)
110  end
111
112  defp zone_mapped(_line, []), do: {:error, :no_regex_matched}
113  defp zone_mapped(line,[regex_head|tail]) do
114    regex_name = elem(regex_head,0)
115    regex = elem(regex_head,1)
116    if Regex.match?(regex, line) do
117      captured = Regex.named_captures(regex, line)
118      {regex_name, captured}
119    else
120      zone_mapped(line, tail)
121    end
122  end
123
124  # if format in zone line is "-" change it to nil
125  defp transform_zone_line_rules("-"), do: nil
126  defp transform_zone_line_rules("0"), do: nil
127  defp transform_zone_line_rules(string) do
128    transform_zone_line_rules(string, Regex.match?(~r/\d/, string))
129  end
130  # If the regexp does not contain a number, we assume a named rule
131  defp transform_zone_line_rules(string, false), do: {:named_rules, string}
132  defp transform_zone_line_rules(string, true) do
133    {:amount, string |> string_amount_to_secs}
134  end
135
136  # Converts keys to atoms. Discards "name"
137  defp captured_zone_map_clean_up(captured) do
138    until = transform_until_datetime(captured["until"])
139    Map.merge %{gmtoff: string_amount_to_secs(captured["gmtoff"]),
140      rules: transform_zone_line_rules(captured["rules"]),
141      format: captured["format"]},
142        # remove until key if it is nil
143        if(until == nil, do: %{}, else: %{until: until})
144  end
145end
146