TimezoneParser 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.yardopts +1 -0
- data/Gemfile +10 -0
- data/README.md +133 -0
- data/Rakefile +160 -0
- data/TimezoneParser.gemspec +40 -0
- data/UNLICENSE +24 -0
- data/data/version.yml +5 -0
- data/lib/timezone_parser.rb +159 -0
- data/lib/timezone_parser/abbreviation.rb +120 -0
- data/lib/timezone_parser/data.rb +121 -0
- data/lib/timezone_parser/data/cldr.rb +175 -0
- data/lib/timezone_parser/data/storage.rb +172 -0
- data/lib/timezone_parser/data/tzinfo.rb +151 -0
- data/lib/timezone_parser/data/windows.rb +124 -0
- data/lib/timezone_parser/rails_zone.rb +178 -0
- data/lib/timezone_parser/timezone.rb +159 -0
- data/lib/timezone_parser/version.rb +5 -0
- data/lib/timezone_parser/windows_zone.rb +169 -0
- data/lib/timezone_parser/zone_info.rb +70 -0
- data/spec/abbreviation_spec.rb +122 -0
- data/spec/rails_zone_spec.rb +84 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/timezone_parser_spec.rb +76 -0
- data/spec/timezone_spec.rb +108 -0
- data/spec/windows_zone_spec.rb +81 -0
- metadata +228 -0
@@ -0,0 +1,151 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'tzinfo'
|
3
|
+
require 'tzinfo/data/tzdataparser'
|
4
|
+
require 'uri'
|
5
|
+
require 'open-uri'
|
6
|
+
require 'rubygems/package'
|
7
|
+
require 'zlib'
|
8
|
+
require 'yaml'
|
9
|
+
require 'pathname'
|
10
|
+
require 'timezone_parser/data'
|
11
|
+
|
12
|
+
module TimezoneParser
|
13
|
+
# TZInfo module
|
14
|
+
module TZInfo
|
15
|
+
|
16
|
+
protected
|
17
|
+
@@Version = nil
|
18
|
+
@@TimezoneCountries = nil
|
19
|
+
|
20
|
+
public
|
21
|
+
|
22
|
+
# TZData source location
|
23
|
+
TZDataSource = 'ftp://ftp.iana.org/tz/tzdata-latest.tar.gz'
|
24
|
+
# Extracted TZData path
|
25
|
+
TZDataPath = TimezoneParser::Data::VendorDir + 'zoneinfo'
|
26
|
+
# TZInfo data path
|
27
|
+
TZInfoData = TimezoneParser::Data::VendorDir + 'tzinfo'
|
28
|
+
# Max Timestamp
|
29
|
+
LastTimestamp = 2147483647
|
30
|
+
def self.download(source = TZDataSource, location = TZDataPath, target = TZInfoData)
|
31
|
+
URI.parse(source).open do |tempfile|
|
32
|
+
FileUtils.mkdir_p(location)
|
33
|
+
tar = Gem::Package::TarReader.new(Zlib::GzipReader.open(tempfile.path))
|
34
|
+
tar.each do |entry|
|
35
|
+
path = location + entry.full_name
|
36
|
+
FileUtils.mkdir_p(path.dirname)
|
37
|
+
if entry.file?
|
38
|
+
File.open(path, 'wb') do |file|
|
39
|
+
file.write(entry.read)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
tar.close
|
44
|
+
end
|
45
|
+
parser = ::TZInfo::Data::TZDataParser.new(location, target)
|
46
|
+
parser.execute
|
47
|
+
getVersion(location)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.getVersion(source = TZDataPath)
|
51
|
+
return @@Version if @@Version
|
52
|
+
File.open(source + 'Makefile', 'r', { :encoding => 'UTF-8:UTF-8' }) do |file|
|
53
|
+
file.each_line do |line|
|
54
|
+
line = line.gsub(/#.*$/, '')
|
55
|
+
v = line.match(/^\s*VERSION\s*=\s*(\w+)\s*$/)
|
56
|
+
@@Version = v[1] if v
|
57
|
+
end
|
58
|
+
end
|
59
|
+
@@Version
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.init
|
63
|
+
::TZInfo::DataSource.set(:ruby, TZInfoData)
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.getTimezoneCountries
|
67
|
+
unless @@TimezoneCountries
|
68
|
+
@@TimezoneCountries = {}
|
69
|
+
::TZInfo::Country.all.each do |countryData|
|
70
|
+
countryData.zone_identifiers.each do |timezone|
|
71
|
+
@@TimezoneCountries[timezone] ||= []
|
72
|
+
@@TimezoneCountries[timezone] << countryData.code
|
73
|
+
@@TimezoneCountries[timezone].uniq!
|
74
|
+
@@TimezoneCountries[timezone].sort!
|
75
|
+
end
|
76
|
+
end
|
77
|
+
@@TimezoneCountries = Hash[@@TimezoneCountries.to_a.sort_by { |pair| pair.first }]
|
78
|
+
end
|
79
|
+
@@TimezoneCountries
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.getAbbreviations
|
83
|
+
transitionData = {}
|
84
|
+
::TZInfo::Timezone.all_data_zone_identifiers.each do |name|
|
85
|
+
zone = ::TZInfo::Timezone.get(name)
|
86
|
+
zone_transitions = zone.transitions_up_to(Time.at(LastTimestamp))
|
87
|
+
zone_transitions.each_index do |i|
|
88
|
+
offset = zone_transitions[i].offset
|
89
|
+
next if offset.abbreviation == :LMT or offset.abbreviation == :zzz
|
90
|
+
abbr = offset.abbreviation.to_s
|
91
|
+
transitionData[abbr] = [] unless transitionData[abbr]
|
92
|
+
period = ::TZInfo::TimezonePeriod.new(zone_transitions[i], zone_transitions[i+1])
|
93
|
+
transitionData[abbr] << { :name => name, :period => period }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
timezoneData = {}
|
97
|
+
transitionData.keys.sort.each do |name|
|
98
|
+
transitions = []
|
99
|
+
transitionData[name].each do |transition|
|
100
|
+
period_start = transition[:period].utc_start
|
101
|
+
period_start = period_start.to_s
|
102
|
+
period_end = transition[:period].utc_end
|
103
|
+
period_end = period_end.to_s if period_end
|
104
|
+
timezone = transition[:name]
|
105
|
+
countries = getTimezoneCountries[timezone]
|
106
|
+
countries = countries.dup if countries
|
107
|
+
data = { 'Offset' => transition[:period].utc_total_offset, 'Timezones' => [timezone], 'Countries' => countries, 'From' => period_start }
|
108
|
+
data['To'] = period_end if period_end
|
109
|
+
transitions << data
|
110
|
+
end
|
111
|
+
transitions.sort_by! { |data| [data['To'] ? data['To'] : 'zzzz', data['From']] }
|
112
|
+
|
113
|
+
abbreviationData = []
|
114
|
+
abbreviationData << transitions.shift
|
115
|
+
transitions.each do |data|
|
116
|
+
current = abbreviationData.last
|
117
|
+
if data['Offset'] == current['Offset'] and (current['Timezones'].sort == data['Timezones'].sort || (data['From'] == current['From'] and data['To'] == current['To']))
|
118
|
+
current['Timezones'] += data['Timezones']
|
119
|
+
current['Timezones'].uniq!
|
120
|
+
current['Timezones'].sort!
|
121
|
+
if data['Countries']
|
122
|
+
current['Countries'] = current['Countries'].to_a + data['Countries']
|
123
|
+
current['Countries'].uniq!
|
124
|
+
current['Countries'].sort!
|
125
|
+
end
|
126
|
+
current['To'] = data['To']
|
127
|
+
current.delete('To') unless current['To']
|
128
|
+
else
|
129
|
+
abbreviationData << data
|
130
|
+
end
|
131
|
+
end
|
132
|
+
previous = nil
|
133
|
+
abbreviationData.delete_if do |item|
|
134
|
+
if previous.nil?
|
135
|
+
previous = item
|
136
|
+
false
|
137
|
+
elsif previous['Offset'] == item['Offset'] and previous['Timezones'] == item['Timezones']
|
138
|
+
previous['To'] = item['To']
|
139
|
+
previous.delete('To') unless previous['To']
|
140
|
+
true
|
141
|
+
else
|
142
|
+
previous = item
|
143
|
+
false
|
144
|
+
end
|
145
|
+
end
|
146
|
+
timezoneData[name] = abbreviationData
|
147
|
+
end
|
148
|
+
timezoneData
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'win32/registry'
|
3
|
+
require 'fiddle'
|
4
|
+
|
5
|
+
module TimezoneParser
|
6
|
+
# Windows module
|
7
|
+
module Windows
|
8
|
+
|
9
|
+
protected
|
10
|
+
|
11
|
+
@@Version = nil
|
12
|
+
@@Errors = ''
|
13
|
+
|
14
|
+
public
|
15
|
+
# Windows Registry path to Time Zone data
|
16
|
+
TimeZonePath = 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones'
|
17
|
+
def self.errors
|
18
|
+
@@Errors
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.getVersion(path = TimeZonePath)
|
22
|
+
return @@Version if @@Version
|
23
|
+
begin
|
24
|
+
Win32::Registry::HKEY_LOCAL_MACHINE.open(path, Win32::Registry::KEY_READ) do |reg|
|
25
|
+
@@Version = reg['TzVersion', Win32::Registry::REG_DWORD].to_s(16)
|
26
|
+
end
|
27
|
+
rescue Win32::Registry::Error => e
|
28
|
+
@@Errors << e.message
|
29
|
+
end
|
30
|
+
@@Version
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.getTimezones(path = TimeZonePath)
|
34
|
+
timezones = {}
|
35
|
+
begin
|
36
|
+
Win32::Registry::HKEY_LOCAL_MACHINE.open(path, Win32::Registry::KEY_READ).each_key do |key, wtime|
|
37
|
+
Win32::Registry::HKEY_LOCAL_MACHINE.open(path + '\\' + key, Win32::Registry::KEY_READ) do |reg|
|
38
|
+
timezones[key] ||= {}
|
39
|
+
tzi = reg.read('TZI', Win32::Registry::REG_BINARY).last
|
40
|
+
# TZI Structure (http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481.aspx)
|
41
|
+
# typedef struct _REG_TZI_FORMAT
|
42
|
+
# {
|
43
|
+
# LONG Bias;
|
44
|
+
# LONG StandardBias;
|
45
|
+
# LONG DaylightBias;
|
46
|
+
# SYSTEMTIME StandardDate;
|
47
|
+
# SYSTEMTIME DaylightDate;
|
48
|
+
# } REG_TZI_FORMAT;
|
49
|
+
unpacked = tzi.unpack('lllSSSSSSSSSSSSSSSS')
|
50
|
+
timezones[key]['standard'] = (0 - unpacked[0] - unpacked[1]) * 60
|
51
|
+
timezones[key]['daylight'] = (0 - unpacked[0] - unpacked[2]) * 60
|
52
|
+
end
|
53
|
+
end
|
54
|
+
rescue Win32::Registry::Error => e
|
55
|
+
@@Errors << e.message
|
56
|
+
end
|
57
|
+
timezones = Hash[timezones.to_a.sort_by { |d| d.first } ]
|
58
|
+
timezones
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.getMUIOffsets(path = TimeZonePath)
|
62
|
+
offsets = {}
|
63
|
+
begin
|
64
|
+
Win32::Registry::HKEY_LOCAL_MACHINE.open(path, Win32::Registry::KEY_READ).each_key do |key, wtime|
|
65
|
+
Win32::Registry::HKEY_LOCAL_MACHINE.open(path + '\\' + key, Win32::Registry::KEY_READ) do |reg|
|
66
|
+
muiDlt = reg.read_s('MUI_Dlt')
|
67
|
+
muiStd = reg.read_s('MUI_Std')
|
68
|
+
|
69
|
+
offsets[self.parseMUI(muiDlt)] = ['daylight', key]
|
70
|
+
offsets[self.parseMUI(muiStd)] = ['standard', key]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
rescue Win32::Registry::Error => e
|
74
|
+
@@Errors << e.message
|
75
|
+
end
|
76
|
+
puts @@Errors
|
77
|
+
offsets
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.parseMUI(str)
|
81
|
+
str.split(',').last.to_i.abs
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.parseMetazones(metazoneList, offsets)
|
85
|
+
metazones = {}
|
86
|
+
metazoneList.each do |lcid, data|
|
87
|
+
localeMem = Fiddle::Pointer.malloc(LOCALE_NAME_MAX_LENGTH)
|
88
|
+
chars = LCIDToLocaleName.call(lcid, localeMem, LOCALE_NAME_MAX_LENGTH, 0)
|
89
|
+
return nil if chars.zero?
|
90
|
+
locale = localeMem.to_s((chars-1)*2).force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8)
|
91
|
+
metazones[locale] = {}
|
92
|
+
offsets.each do |id, info|
|
93
|
+
name = data[id]
|
94
|
+
metazones[locale][name] ||= {}
|
95
|
+
metazones[locale][name]['Types'] ||= []
|
96
|
+
metazones[locale][name]['Metazones'] ||= []
|
97
|
+
metazones[locale][name]['Types'] << info.first
|
98
|
+
metazones[locale][name]['Metazones'] << info.last
|
99
|
+
metazones[locale][name]['Types'].uniq!
|
100
|
+
metazones[locale][name]['Metazones'].uniq!
|
101
|
+
end
|
102
|
+
metazones[locale] = Hash[metazones[locale].to_a.sort_by { |d| d.first } ]
|
103
|
+
end
|
104
|
+
metazones = Hash[metazones.to_a.sort_by { |d| d.first } ]
|
105
|
+
metazones
|
106
|
+
end
|
107
|
+
|
108
|
+
# Windows Kernel32 library
|
109
|
+
kernel32 = Fiddle.dlopen('kernel32')
|
110
|
+
|
111
|
+
# function
|
112
|
+
# int LCIDToLocaleName (
|
113
|
+
# _In_ LCID Locale,
|
114
|
+
# _Out_opt_ LPWSTR lpName,
|
115
|
+
# _In_ int cchName,
|
116
|
+
# _In_ DWORD dwFlags
|
117
|
+
# );
|
118
|
+
# @see http://msdn.microsoft.com/en-us/library/windows/desktop/dd318698.aspx
|
119
|
+
LCIDToLocaleName = Fiddle::Function.new( kernel32['LCIDToLocaleName'],
|
120
|
+
[Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_LONG], Fiddle::TYPE_INT )
|
121
|
+
# Max locale length
|
122
|
+
LOCALE_NAME_MAX_LENGTH = 85
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TimezoneParser
|
4
|
+
# Rails zone data
|
5
|
+
class RailsData < Data
|
6
|
+
protected
|
7
|
+
@RailsZone = nil
|
8
|
+
|
9
|
+
public
|
10
|
+
attr_reader :RailsZone
|
11
|
+
def processEntry(data, rails)
|
12
|
+
if rails
|
13
|
+
@RailsZone = rails
|
14
|
+
@Metazones << rails
|
15
|
+
@Timezones << data
|
16
|
+
else
|
17
|
+
rails = Storage.RailsZones[data]
|
18
|
+
if rails
|
19
|
+
@RailsZone = data
|
20
|
+
@Metazones << data
|
21
|
+
@Timezones << rails
|
22
|
+
end
|
23
|
+
end
|
24
|
+
self
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Rails zone
|
29
|
+
class RailsZone < ZoneInfo
|
30
|
+
protected
|
31
|
+
@@Locales = []
|
32
|
+
|
33
|
+
public
|
34
|
+
# Locales which will be used for RailsZone methods if not specified there
|
35
|
+
#
|
36
|
+
# Each locale is language identifier based on IETF BCP 47 and ISO 639 code
|
37
|
+
# @return [Array<String>] list containing locale identifiers
|
38
|
+
# @see http://en.wikipedia.org/wiki/IETF_language_tag
|
39
|
+
def self.Locales
|
40
|
+
@@Locales
|
41
|
+
end
|
42
|
+
|
43
|
+
attr_accessor :All
|
44
|
+
|
45
|
+
# Rails zone instance
|
46
|
+
# @param name [String] Rails zone name
|
47
|
+
def initialize(name)
|
48
|
+
@Name = name
|
49
|
+
@Data = RailsData.new
|
50
|
+
@Valid = nil
|
51
|
+
setTime
|
52
|
+
set(@@Locales.dup, true)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Set locales and all
|
56
|
+
# @param locales [Array<String>] search only in these locales
|
57
|
+
# @param all [Boolean] specify whether should search for all zones or return as soon as found any
|
58
|
+
# @return [RailsZone] self
|
59
|
+
# @see Locales
|
60
|
+
def set(locales = nil, all = true)
|
61
|
+
@Locales = locales unless locales.nil?
|
62
|
+
@All = all ? true : false
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
# Check if Rails zone is valid
|
67
|
+
# @return [Boolean] whether Rails zone is valid
|
68
|
+
def isValid?
|
69
|
+
if @Valid.nil?
|
70
|
+
@Valid = false
|
71
|
+
@Valid = Data::Storage.RailsZones.has_key?(@Name) if (not @Locales) or (@Locales and @Locales.include?('en'))
|
72
|
+
return @Valid if @Valid
|
73
|
+
locales = @Locales
|
74
|
+
locales = Data::Storage.RailsTranslated.keys if locales.empty?
|
75
|
+
locales.each do |locale|
|
76
|
+
next unless Data::Storage.RailsTranslated.has_key?(locale)
|
77
|
+
@Valid = Data::Storage.RailsTranslated[locale].has_key?(@Name)
|
78
|
+
return @Valid if @Valid
|
79
|
+
end
|
80
|
+
end
|
81
|
+
@Valid = false
|
82
|
+
end
|
83
|
+
|
84
|
+
# Rails zone data
|
85
|
+
# @return [RailsData] data
|
86
|
+
def getData
|
87
|
+
unless @Loaded
|
88
|
+
@Loaded = true
|
89
|
+
@Valid = false
|
90
|
+
@Valid = Data::Storage.RailsZones.has_key?(@Name) if (not @Locales) or (@Locales and @Locales.include?('en'))
|
91
|
+
if @Valid
|
92
|
+
@Data.processEntry(Data::Storage.RailsZones[@Name], @Name)
|
93
|
+
return @Data unless @All
|
94
|
+
end
|
95
|
+
locales = @Locales
|
96
|
+
locales = Data::Storage.RailsTranslated.keys if locales.empty?
|
97
|
+
locales.each do |locale|
|
98
|
+
next unless Data::Storage.RailsTranslated.has_key?(locale)
|
99
|
+
entry = Data::Storage.RailsTranslated[locale][@Name]
|
100
|
+
if entry
|
101
|
+
@Data.processEntry(entry, false)
|
102
|
+
@Valid = true
|
103
|
+
return @Data unless @All
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
@Data
|
108
|
+
end
|
109
|
+
|
110
|
+
# Get UTC offsets in seconds
|
111
|
+
# @return [Array<Fixnum>] list of timezone offsets in seconds
|
112
|
+
def getOffsets
|
113
|
+
if not @Offsets and not getTimezones.empty?
|
114
|
+
@Offsets = @Data.findOffsets(@ToTime, @FromTime).to_a
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
118
|
+
@Offsets
|
119
|
+
end
|
120
|
+
|
121
|
+
# Rails zone identifier
|
122
|
+
# @return [String] Rails zone identifier
|
123
|
+
def getZone
|
124
|
+
getData.RailsZone
|
125
|
+
end
|
126
|
+
|
127
|
+
# Check if given Rails zone name is a valid timezone
|
128
|
+
# @param name [String] Rails zone name
|
129
|
+
# @param locales [Array<String>] search zone name only for these locales
|
130
|
+
# @return [Boolean] whether Timezone is valid
|
131
|
+
# @see Locales
|
132
|
+
def self.isValid?(name, locales = nil)
|
133
|
+
self.new(name).set(locales).isValid?
|
134
|
+
end
|
135
|
+
|
136
|
+
# Get UTC offsets in seconds for given Rails zone name
|
137
|
+
# @param name [String] Rails zone name
|
138
|
+
# @param toTime [DateTime] look for offsets which came into effect before this date, exclusive
|
139
|
+
# @param fromTime [DateTime] look for offsets which came into effect at this date, inclusive
|
140
|
+
# @param locales [Array<String>] search zone name only for these locales
|
141
|
+
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
142
|
+
# @return [Array<Fixnum>] list of timezone offsets in seconds
|
143
|
+
# @see Locales
|
144
|
+
def self.getOffsets(name, toTime = nil, fromTime = nil, locales = nil, all = true)
|
145
|
+
self.new(name).setTime(toTime, fromTime).set(locales, all).getOffsets
|
146
|
+
end
|
147
|
+
|
148
|
+
# Get Timezone identifiers for given Rails zone name
|
149
|
+
# @param name [String] Rails zone name
|
150
|
+
# @param locales [Array<String>] search zone name only for these locales
|
151
|
+
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
152
|
+
# @return [Array<String>] list of timezone identifiers
|
153
|
+
# @see Locales
|
154
|
+
def self.getTimezones(name, locales = nil, all = true)
|
155
|
+
self.new(name).set(locales, all).getTimezones
|
156
|
+
end
|
157
|
+
|
158
|
+
# Get Metazone identifiers for given Rails zone name
|
159
|
+
# @param name [String] Rails zone name
|
160
|
+
# @param locales [Array<String>] search zone name only for these locales
|
161
|
+
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
162
|
+
# @return [Array<String>] list of metazone identifiers
|
163
|
+
# @see Locales
|
164
|
+
# @see Regions
|
165
|
+
def self.getMetazones(name, locales = nil, all = true)
|
166
|
+
self.new(name).set(locales, all).getMetazones
|
167
|
+
end
|
168
|
+
|
169
|
+
# Rails zone identifier
|
170
|
+
# @param name [String] Rails zone name
|
171
|
+
# @param locales [Array<String>] search zone name only for these locales
|
172
|
+
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
173
|
+
# @return [String] Timezone identifier
|
174
|
+
def self.getZone(name, locales = nil, all = true)
|
175
|
+
self.new(name).set(locales, all).getZone
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|