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