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)
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)
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)
Encloses the string in single quotes and escapes any single quotes in
def self.quote_str(str) "'#{str.gsub('\'', '\\\\\'')}'" end
def execute
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)
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)
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
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)
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)
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)
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
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
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