-
Notifications
You must be signed in to change notification settings - Fork 382
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor local timezone lookup/parser, add additional tests. Closes #2
- Loading branch information
Showing
6 changed files
with
807 additions
and
732 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
defmodule Timex.Parsers.ZoneInfo do | ||
@moduledoc """ | ||
This module is responsible for parsing binary zoneinfo files, | ||
such as those found in /usr/local/zoneinfo. | ||
""" | ||
|
||
# See http://linux.about.com/library/cmd/blcmdl5_tzfile.htm or | ||
# https://github.com/eggert/tz/blob/master/tzfile.h for details on the tzfile format | ||
defmodule Zone do | ||
@moduledoc """ | ||
Represents the data retreived from a binary tzfile. | ||
For details on the tzfile format, see: | ||
http://linux.about.com/library/cmd/blcmdl5_tzfile.htm | ||
https://github.com/eggert/tz/blob/master/tzfile.h | ||
""" | ||
@derive Access | ||
defstruct transitions: [], # Transition times | ||
abbreviations: [], # Zone abbreviations, | ||
leaps: [], # Leap second adjustments | ||
std_or_wall?: false, # whether local transitions are standard or wall | ||
utc_or_local?: false # whether local transitions are UTC or local | ||
end | ||
|
||
defmodule Header do | ||
@moduledoc false | ||
|
||
# Six big-endian 4-8 byte integers | ||
@derive Access | ||
defstruct utc_count: 0, # count of UTC/local indicators | ||
wall_count: 0, # count of standard/wall indicators | ||
leap_count: 0, # number of leap seconds | ||
transition_count: 0, # number of transition times | ||
type_count: 0, # number of local time types (never zero) | ||
abbrev_length: 0 # total number of characters of the zone abbreviations string | ||
end | ||
|
||
defmodule TransitionInfo do | ||
@moduledoc false | ||
@derive Access | ||
defstruct gmt_offset: 0, # total ISO 8601 offset (std + dst) | ||
starts_at: 0, # The time at which this transition starts | ||
is_dst?: false, # Is this transition in daylight savings time | ||
abbrev_index: 0, # The lookup index of the abbreviation | ||
abbreviation: "N/A", # The zone abbreviation | ||
is_std?: true, # Whether transitions are standard or wall | ||
is_utc?: false # Whether transitions are UTC or local | ||
end | ||
|
||
defmodule LeapSecond do | ||
@moduledoc false | ||
@derive Access | ||
defstruct start: 0, # The time at which this leap second occurs | ||
remaining: 0 # The count of leap seconds after this leap second | ||
end | ||
|
||
|
||
############## | ||
# Macros defining common bitstring modifier combinations in zoneinfo files | ||
defmacrop bytes(size) do | ||
quote do: [binary, size(unquote(size)), unit(8)] | ||
end | ||
defmacrop integer_32bit_be do | ||
quote do: [big, size(4), unit(8), integer] | ||
end | ||
defmacrop signed_char_be do | ||
quote do: [big, size(1), unit(8), signed, integer] | ||
end | ||
defmacrop unsigned_char_be do | ||
quote do: [big, size(1), unit(8), unsigned, integer] | ||
end | ||
|
||
@doc """ | ||
Given a path to a zoneinfo file, or the binary data from a zoneinfo file, | ||
parse the timezone information inside, and return it as a Zone struct. | ||
""" | ||
@spec parse(binary) :: {:ok, Zone.t} | {:error, binary} | ||
def parse(<<?T, ?Z, ?i, ?f, _reserved :: bytes(16), rest :: binary>>) do | ||
do_parse_header(rest) | ||
end | ||
def parse(path) when is_binary(path) do | ||
if path |> File.exists? do | ||
path |> File.read! |> parse | ||
else | ||
{:error, "No zoneinfo file at #{path}"} | ||
end | ||
end | ||
def parse(_) do | ||
{:error, "Invalid zoneinfo file header"} | ||
end | ||
|
||
# Parse the header information from the zoneinfo file | ||
defp do_parse_header(<<header :: bytes(24), rest :: binary>>) do | ||
{utc_count, next} = parse_int(header) | ||
{wall_count, next} = parse_int(next) | ||
{leap_count, next} = parse_int(next) | ||
{tx_count, next} = parse_int(next) | ||
{type_count, next} = parse_int(next) | ||
{abbrev_length, _} = parse_int(next) | ||
header = %Header{ | ||
utc_count: utc_count, | ||
wall_count: wall_count, | ||
leap_count: leap_count, | ||
transition_count: tx_count, | ||
type_count: type_count, | ||
abbrev_length: abbrev_length | ||
} | ||
do_parse_transition_times(rest, header) | ||
end | ||
# Parse the number of transition times in this zone | ||
defp do_parse_transition_times(data, %Header{transition_count: tx_count} = header) do | ||
{times, rest} = parse_array(data, tx_count, &parse_int/1) | ||
do_parse_transition_info(rest, header, %Zone{transitions: times}) | ||
end | ||
# Parse transition time info for this zone | ||
defp do_parse_transition_info(data, %Header{transition_count: tx_count, type_count: type_count} = header, tzfile) do | ||
{indices, rest} = parse_array data, tx_count, &parse_uchar/1 | ||
{txinfos, rest} = parse_array rest, type_count, fn data -> | ||
{gmt_offset, next} = parse_int(data) | ||
{is_dst?, next} = parse_char(next) | ||
{abbrev_index, next} = parse_uchar(next) | ||
info = %TransitionInfo{ | ||
gmt_offset: gmt_offset, | ||
is_dst?: is_dst? == 1, | ||
abbrev_index: abbrev_index | ||
} | ||
{info, next} | ||
end | ||
txs = indices | ||
|> Enum.map(&(Enum.at(txinfos, &1))) | ||
|> Enum.zip(tzfile.transitions) | ||
|> Enum.map(fn {info, time} -> | ||
put_in(info, [:starts_at], time) | ||
end) | ||
do_parse_abbreviations(rest, header, %{tzfile | :transitions => txs}) | ||
end | ||
# Parses zone abbreviations for this zone | ||
defp do_parse_abbreviations(data, %Header{abbrev_length: len} = header, tzfile) do | ||
{abbrevs, rest} = parse_array(data, len, &parse_char/1) | ||
txinfos = Enum.map(tzfile.transitions, fn tx -> | ||
abbrev = abbrevs | ||
|> Enum.drop(tx.abbrev_index) | ||
|> Enum.take_while(fn c -> c > 0 end) | ||
%{tx | :abbreviation => "#{abbrev}"} | ||
end) | ||
do_parse_leap_seconds(rest, header, %{tzfile | :transitions => txinfos}) | ||
end | ||
# Parses leap second information for this zone | ||
defp do_parse_leap_seconds(data, %Header{leap_count: count} = header, tzfile) do | ||
{leaps, rest} = parse_array data, count, fn data -> | ||
{start, next} = parse_int(data) | ||
{remaining, next} = parse_int(next) | ||
leap = %LeapSecond{ | ||
start: start, | ||
remaining: remaining | ||
} | ||
{leap, next} | ||
end | ||
do_parse_flags(rest, header, %{tzfile | :leaps => leaps}) | ||
end | ||
# Parses the trailing flags in the zoneinfo binary | ||
defp do_parse_flags(data, %Header{utc_count: utc_count, wall_count: wall_count}, tzfile) do | ||
{is_std, rest} = parse_array(data, wall_count, &parse_char/1) | ||
{is_gmt, _} = parse_array(rest, utc_count, &parse_char/1) | ||
{:ok, %{tzfile | :std_or_wall? => is_std, :utc_or_local? => is_gmt}} | ||
end | ||
|
||
################ | ||
# Parses an array of a primitive type, ex: | ||
# parse_array(<<"test">>, 2, &parse_uchar/1) => [?t, ?e] | ||
### | ||
defp parse_array(data, 0, _parser), do: {[], data} | ||
defp parse_array(data, count, parser) when is_binary(data) and is_function(parser) do | ||
{results, rest} = Range.new(1, count) | ||
|> Enum.reduce({[], data}, &do_parse_array(&1, &2, parser)) | ||
{results |> Enum.reverse, rest} | ||
end | ||
defp do_parse_array(_, {acc, data}, parser) when is_function(parser) do | ||
{item, next} = parser.(data) | ||
{[item | acc], next} | ||
end | ||
|
||
################# | ||
# Data Type Parsers | ||
defp parse_int(<<val :: integer_32bit_be, rest :: binary>>), do: {val, rest} | ||
defp parse_char(<<val :: signed_char_be, rest :: binary>>), do: {val, rest} | ||
defp parse_uchar(<<val :: unsigned_char_be, rest :: binary>>), do: {val, rest} | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
defmodule Timex.Timezone.Database do | ||
@moduledoc """ | ||
This module provides access to the database of timezones. | ||
""" | ||
|
||
{_, olson_mappings} = Path.join("priv", "standard_to_olson.exs") |> Code.eval_file | ||
{_, windows_mappings} = Path.join("priv", "olson_to_win.exs") |> Code.eval_file | ||
|
||
|
||
@doc """ | ||
Lookup the Olson time zone given it's standard name | ||
## Example | ||
iex> Timex.Timezone.Database.to_olson("Azores Standard Time") | ||
"Atlantic/Azores" | ||
""" | ||
Enum.each(olson_mappings, fn {key, value} -> | ||
quoted = quote do | ||
def to_olson(unquote(key)), do: unquote(value) | ||
end | ||
Module.eval_quoted __MODULE__, quoted, [], __ENV__ | ||
end) | ||
def to_olson(_tz), do: nil | ||
|
||
@doc """ | ||
Lookup the Windows time zone name given an Olson time zone name. | ||
## Example | ||
iex> Timex.Timezone.Database.olson_to_win("Pacific/Noumea") | ||
Central Pacific Standard Time | ||
""" | ||
Enum.each(windows_mappings, fn {key, value} -> | ||
quoted = quote do | ||
def olson_to_win(unquote(key)), do: unquote(value) | ||
end | ||
Module.eval_quoted __MODULE__, quoted, [], __ENV__ | ||
end) | ||
def olson_to_win(_tz), do: nil | ||
end |
Oops, something went wrong.