class TZInfo::Data::TZDataParser

Parses Time Zone Data from the IANA Time Zone Database and transforms it into a set of Ruby modules that can be used with TZInfo.

Normally, this class wouldn’t be used. It is only run to update the timezone data and index modules.

Constants

DEFAULT_FUTURE_YEARS

Default number of future years data to generate (not including the current year).

DEFAULT_MIN_YEAR

Default earliest year that will be considered.

Attributes

exclude_zones[RW]

Zones to exclude from generation when not using only_zones (set to an array containing zone identifiers).

generate_countries[RW]

Whether to generate country definitions (set to false to stop countries being generated).

generate_zones[RW]

Whether to generate zone definitions (set to false to stop zones being generated).

max_year[RW]

Latest year that will be considered. Defaults to the current year plus FUTURE_YEARS.

min_year[RW]

Earliest year that will be considered. Defaults to DEFAULT_MIN_YEAR.

only_zones[RW]

Limit the set of zones to generate (set to an array containing zone identifiers).

Public Class Methods

new(input_dir, output_dir) click to toggle source

Initializes a new TZDataParser. input_dir must contain the extracted tzdata tarball. output_dir is the location to output the modules (in definitions and indexes directories).

Calls superclass method
# File lib/tzinfo/data/tzdataparser.rb, line 137
def initialize(input_dir, output_dir)
  super()
  @input_dir = input_dir
  @output_dir = output_dir
  @min_year = DEFAULT_MIN_YEAR
  @max_year = Time.now.year + DEFAULT_FUTURE_YEARS
  @rule_sets = {}
  @zones = {}
  @countries = {}
  @no_rules = TZDataNoRules.new
  @generate_zones = true
  @generate_countries = true
  @only_zones = []
  @exclude_zones = []
end

Public Instance Methods

execute() click to toggle source

Reads the tzdata source and generates the classes. Progress information is written to standard out.

# File lib/tzinfo/data/tzdataparser.rb, line 155
def execute
  # Note that the backzone file is ignored. backzone contains alternative
  # definitions of certain zones, primarily for pre-1970 data. It is not
  # recommended for ordinary use and the tzdata Makefile does not
  # install its entries by default.

  files = Dir.entries(@input_dir).select do |file|
    file =~ /\A[^\.]+\z/ &&
      !%w(backzone calendars leapseconds CONTRIBUTING LICENSE Makefile NEWS README SECURITY SOURCE Theory version).include?(file) &&
      File.file?(File.join(@input_dir, file))
  end

  files.each {|file| load_rules(file) }
  files.each {|file| load_zones(file) }
  files.each {|file| load_links(file) }

  load_countries

  if @generate_zones
    modules = []

    if @only_zones.nil? || @only_zones.empty?
      @zones.each_value {|zone|
        zone.write_module(@output_dir) unless @exclude_zones.include?(zone.name)
      }
    else
      @only_zones.each {|id|
        zone = @zones[id]
        zone.write_module(@output_dir)
      }
    end

    write_timezones_index
  end

  if @generate_countries
    write_countries_index
  end
end

Private Instance Methods

get_rules(ref) click to toggle source

Gets a rules object for the given reference. Might be a named rule set, a fixed offset or an empty ruleset.

# File lib/tzinfo/data/tzdataparser.rb, line 224
def get_rules(ref)
  if ref == '-'
    @no_rules
  elsif ref =~ /^-?[0-9]+:[0-9]+$/
    TZDataFixedOffsetRules.new(parse_offset(ref))
  else
    rule_set = @rule_sets[ref]
    raise "Ruleset not found: #{ref}" if rule_set.nil?
    rule_set
  end
end
load_countries() click to toggle source

Loads countries from iso3166.tab and zone1970.tab and stores the result in the countries instance variable.

# File lib/tzinfo/data/tzdataparser.rb, line 297
def load_countries
  puts 'load_countries'

  # iso3166.tab is ASCII encoded, but is planned to change to UTF-8 (a
  # superset of ASCII) in the future.
  open_file(File.join(@input_dir, 'iso3166.tab'), 'r', :external_encoding => 'UTF-8', :internal_encoding => 'UTF-8') do |file|
    file.each_line do |line|

      if line =~ /^([A-Z]{2})\t(.*)$/
        code = $1
        name = $2
        @countries[code] = TZDataCountry.new(code, name)
      end
    end
  end

  primary_zones = {}
  secondary_zones = {}

  # zone1970.tab is UTF-8 encoded.
  open_file(File.join(@input_dir, 'zone1970.tab'), 'r', :external_encoding => 'UTF-8', :internal_encoding => 'UTF-8') do |file|
    file.each_line do |line|

      line.chomp!

      if line =~ /^([A-Z]{2}(?:,[A-Z]{2})*)\t([^\t]+)\t([^\t]+)(\t(.*))?$/
        codes = $1
        location_str = $2
        zone_name = $3
        description = $5

        location = TZDataLocation.new(location_str)

        zone = @zones[zone_name]
        raise "Zone not found: #{zone_name}" if zone.nil?

        description = nil if description == ''

        country_timezone = TZDataCountryTimezone.new(zone, description, location)

        codes = codes.split(',')

        (primary_zones[codes.first] ||= []) << country_timezone

        codes[1..-1].each do |code|
          (secondary_zones[code] ||= []) << country_timezone
        end
      end
    end
  end

  [primary_zones, secondary_zones].each do |zones|
    zones.each_pair do |code, country_timezones|
      country = @countries[code]
      raise "Country not found: #{code}" if country.nil?

      country_timezones.each do |country_timezone|
        country.add_zone(country_timezone)
      end
    end
  end
end
load_rules(file) click to toggle source

Loads all the Rule definitions from the tz data and stores them in the rule_sets instance variable.

# File lib/tzinfo/data/tzdataparser.rb, line 198
def load_rules(file)
  puts 'load_rules: ' + file

  # Files are in ASCII, but may change to UTF-8 (a superset of ASCII)
  # in the future.
  open_file(File.join(@input_dir, file), 'r', :external_encoding => 'UTF-8', :internal_encoding => 'UTF-8') do |file|
    file.each_line do |line|
      line = line.gsub(/#.*$/, '')
      line = line.gsub(/\s+$/, '')

      if line =~ /^Rule\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)/

        name = $1

        if @rule_sets[name].nil?
          @rule_sets[name] = TZDataRuleSet.new(name)
        end

        @rule_sets[name].add_rule(TZDataRule.new($2, $3, $4, $5, $6, $7, $8, $9))
      end
    end
  end
end
load_zones(file) click to toggle source

Loads in the Zone definitions from the tz data and stores them in @zones.

# File lib/tzinfo/data/tzdataparser.rb, line 237
def load_zones(file)
  puts 'load_zones: ' + file

  in_zone = nil

  # Files are in ASCII, but may change to UTF-8 (a superset of ASCII)
  # in the future.
  open_file(File.join(@input_dir, file), 'r', :external_encoding => 'UTF-8', :internal_encoding => 'UTF-8') do |file|
    file.each_line do |line|
      line = line.gsub(/#.*$/, '')
      line = line.gsub(/\s+$/, '')

      if in_zone
        if line =~ /^\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)(\s+([0-9]+(\s+.*)?))?$/

          in_zone.add_observance(TZDataObservance.new($1, get_rules($2), $3, $5))

          in_zone = nil if $4.nil?
        end
      else
        if line =~ /^Zone\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)\s+([^\s]+)(\s+([0-9]+(\s+.*)?))?$/
          name = $1

          if @zones[name].nil?
            @zones[name] = TZDataZone.new(name, @min_year..@max_year)
          end

          @zones[name].add_observance(TZDataObservance.new($2, get_rules($3), $4, $6))

          in_zone = @zones[name] if !$5.nil?
        end
      end
    end
  end
end
write_countries_index() click to toggle source

Writes a country index file.

# File lib/tzinfo/data/tzdataparser.rb, line 361
def write_countries_index
  dir = File.join(@output_dir, 'indexes')
  FileUtils.mkdir_p(dir)

  open_file(File.join(dir, 'countries.rb'), 'w', :external_encoding => 'UTF-8', :universal_newline => true) do |file|
    file.puts('# encoding: UTF-8')
    file.puts('')
    file.puts('# This file contains data derived from the IANA Time Zone Database')
    file.puts('# (https://www.iana.org/time-zones).')
    file.puts('')

    file.puts('module TZInfo')
    file.puts('  module Data')
    file.puts('    module Indexes')
    file.puts('      module Countries')
    file.puts('        include CountryIndexDefinition')
    file.puts('')

    countries = @countries.values.sort {|c1,c2| c1.code <=> c2.code}
    countries.each {|country| country.write_index_record(file)}

    file.puts('      end') # end module Countries
    file.puts('    end') # end module Indexes
    file.puts('  end') # end module Data
    file.puts('end') # end module TZInfo
  end
end
write_timezones_index() click to toggle source

Writes a timezone index file.

# File lib/tzinfo/data/tzdataparser.rb, line 390
def write_timezones_index
  dir = File.join(@output_dir, 'indexes')
  FileUtils.mkdir_p(dir)

  open_file(File.join(dir, 'timezones.rb'), 'w', :external_encoding => 'UTF-8', :universal_newline => true) do |file|
    file.puts('# encoding: UTF-8')
    file.puts('')
    file.puts('# This file contains data derived from the IANA Time Zone Database')
    file.puts('# (https://www.iana.org/time-zones).')
    file.puts('')

    file.puts('module TZInfo')
    file.puts('  module Data')
    file.puts('    module Indexes')
    file.puts('      module Timezones')
    file.puts('        include TimezoneIndexDefinition')
    file.puts('')

    zones = @zones.values.sort {|t1,t2| t1.name <=> t2.name}
    zones.each {|zone| zone.write_index_record(file)}

    file.puts('      end') # end module Timezones
    file.puts('    end') # end module Indexes
    file.puts('  end') # end module Data
    file.puts('end') # end module TZInfo
  end

end