class TZInfo::TZDataParser

timezone data and index modules.
Normally, this class wouldn’t be used. It is only run to update the
a set of Ruby modules that can be used through Timezone and Country.
Parses tzdata from elsie.nci.nih.gov/pub/ and transforms it into

def self.parse_month(month)

between 1 and 12 representing January to December.
Parses a month specified in the tz data and converts it to a number
def self.parse_month(month)
  lower = month.downcase
  if lower =~ /^jan/
    @month = 1
  elsif lower =~ /^feb/
    @month = 2
  elsif lower =~ /^mar/
    @month = 3
  elsif lower =~ /^apr/
    @month = 4
  elsif lower =~ /^may/
    @month = 5
  elsif lower =~ /^jun/
    @month = 6
  elsif lower =~ /^jul/
    @month = 7
  elsif lower =~ /^aug/
    @month = 8
  elsif lower =~ /^sep/
    @month = 9
  elsif lower =~ /^oct/
    @month = 10
  elsif lower =~ /^nov/
    @month = 11
  elsif lower =~ /^dec/
    @month = 12
  else
    raise "Invalid month: #{month}"
  end
end

def self.parse_offset(offset)

the offset in seconds.
Parses an offset string [-]h:m:s (minutes and seconds are optional). Returns
def self.parse_offset(offset)
  raise "Invalid time: #{offset}" if offset !~ /^(-)?(?:([0-9]+)(?::([0-9]+)(?::([0-9]+))?)?)?$/
  
  negative = !$1.nil?      
  hour = $2.nil? ? 0 : $2.to_i
  minute = $3.nil? ? 0 : $3.to_i
  second = $4.nil? ? 0 : $4.to_i
  
  seconds = hour
  seconds = seconds * 60
  seconds = seconds + minute
  seconds = seconds * 60
  seconds = seconds + second
  seconds = -seconds if negative
  seconds
end

def self.quote_str(str)

the content.
Encloses the string in single quotes and escapes any single quotes in
def self.quote_str(str)
  "'#{str.gsub('\'', '\\\\\'')}'"
end

def execute

to run. Currently outputs debugging information to standard out.
Reads the tzdata source and generates the classes. Takes a long time
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 version CONTRIBUTING LICENSE Makefile NEWS README 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

def get_rules(ref)

a fixed offset or an empty ruleset.
Gets a rules object for the given reference. Might be a named rule set,
def get_rules(ref)
  if ref == '-'
    @no_rules
  elsif ref =~ /^-?[0-9]+:[0-9]+$/
    TZDataFixedOffsetRules.new(TZDataParser.parse_offset(ref))
  else
    rule_set = @rule_sets[ref]
    raise "Ruleset not found: #{ref}" if rule_set.nil?    
    rule_set
  end  
end

def initialize(input_dir, output_dir)

(in definitions and indexes directories).
tzdata tarball. output_dir is the location to output the modules
Initializes a new TZDataParser. input_dir must contain the extracted
def initialize(input_dir, output_dir)
  super()
  @input_dir = input_dir
  @output_dir = output_dir      
  @rule_sets = {}
  @zones = {}
  @countries = {}
  @no_rules = TZDataNoRules.new
  @generate_zones = true
  @generate_countries = true
  @only_zones = []      
  @exclude_zones = []      
end

def load_countries

@countries.
Loads countries from iso3166.tab and zone.tab and stores the result in
def load_countries
  puts 'load_countries'
  
  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
  open_file(File.join(@input_dir, 'zone.tab'), 'r', :external_encoding => 'UTF-8', :internal_encoding => 'UTF-8') do |file|
    file.each_line do |line|
      line.chomp!
      if line =~ /^([A-Z]{2})\t([^\t]+)\t([^\t]+)(\t(.*))?$/
        code = $1
        location_str = $2
        zone_name = $3
        description = $5
        country = @countries[code]
        raise "Country not found: #{code}" if country.nil?
        location = TZDataLocation.new(location_str)
        zone = @zones[zone_name]
        raise "Zone not found: #{zone_name}" if zone.nil?
        description = nil if description == ''
        country.add_zone(TZDataCountryTimezone.new(zone, description, location))
      end
    end
  end
end

def load_links(file)

Loads in the links and stores them in @zones.
def load_links(file)
  puts 'load_links: ' + file
  
  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 =~ /^Link\s+([^\s]+)\s+([^\s]+)/
        name = $2
        link_to = @zones[$1]
        raise "Link to zone not found (#{name}->#{link_to})" if link_to.nil?
        raise "Zone already defined: #{name}" if !@zones[name].nil?
        @zones[name] = TZDataLink.new(name, link_to)
      end
    end
  end
end

def load_rules(file)

@rule_sets.
Loads all the Rule definitions from the tz data and stores them in
def load_rules(file)
  puts 'load_rules: ' + file
  
  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

def load_zones(file)

Loads in the Zone definitions from the tz data and stores them in @zones.
def load_zones(file)
  puts 'load_zones: ' + file
  
  in_zone = nil
  
  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)
          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

def write_countries_index

Writes a country index file.
def write_countries_index
  dir = @output_dir + File::SEPARATOR + '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('module TZInfo')
    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 TZInfo
  end
end

def write_timezones_index

Writes a timezone index file.
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('module TZInfo')
    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 TZInfo
  end
  
end