Skip to content

Commit

Permalink
Refactor local timezone lookup/parser, add additional tests. Closes #2
Browse files Browse the repository at this point in the history
  • Loading branch information
bitwalker committed Jul 28, 2014
1 parent 43ac47d commit 428240f
Show file tree
Hide file tree
Showing 6 changed files with 807 additions and 732 deletions.
188 changes: 188 additions & 0 deletions lib/parsers/zoneinfo.ex
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
43 changes: 43 additions & 0 deletions lib/timezone/database.ex
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
Loading

0 comments on commit 428240f

Please sign in to comment.