TimezoneParser 0.4.0 → 1.0.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 +4 -4
- data/.gitignore +1 -1
- data/README.md +0 -1
- data/Rakefile +4 -6
- data/TimezoneParser.gemspec +2 -5
- data/data/schema.sql +164 -0
- data/lib/timezone_parser.rb +6 -17
- data/lib/timezone_parser/abbreviation.rb +112 -41
- data/lib/timezone_parser/data.rb +0 -102
- data/lib/timezone_parser/data/exporter.rb +242 -0
- data/lib/timezone_parser/data/storage.rb +12 -150
- data/lib/timezone_parser/data/tzinfo.rb +8 -0
- data/lib/timezone_parser/rails_zone.rb +101 -90
- data/lib/timezone_parser/timezone.rb +106 -56
- data/lib/timezone_parser/version.rb +1 -1
- data/lib/timezone_parser/windows_zone.rb +98 -68
- data/lib/timezone_parser/zone_info.rb +89 -18
- data/spec/abbreviation_spec.rb +25 -1
- data/spec/rails_zone_spec.rb +7 -3
- data/spec/timezone_parser_spec.rb +0 -6
- data/spec/timezone_spec.rb +15 -0
- data/spec/windows_zone_spec.rb +9 -2
- metadata +18 -10
@@ -1,27 +1,6 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
module TimezoneParser
|
4
|
-
# Windows Timezone data
|
5
|
-
class WindowsData < Data
|
6
|
-
protected
|
7
|
-
@WindowsZone = nil
|
8
|
-
|
9
|
-
public
|
10
|
-
attr_reader :WindowsZone
|
11
|
-
def processEntry(entry, region)
|
12
|
-
@Types += entry['Types'] if entry['Types']
|
13
|
-
if entry.has_key?('Metazones')
|
14
|
-
entry['Metazones'].each do |zone|
|
15
|
-
@WindowsZone = zone
|
16
|
-
@Metazones << zone
|
17
|
-
@Timezones += Storage.getTimezones2(zone, region)
|
18
|
-
@Offsets += Storage.getOffsets(zone, entry['Types'])
|
19
|
-
end
|
20
|
-
end
|
21
|
-
self
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
4
|
# Windows Timezone
|
26
5
|
class WindowsZone < ZoneInfo
|
27
6
|
protected
|
@@ -49,28 +28,24 @@ module TimezoneParser
|
|
49
28
|
|
50
29
|
attr_accessor :Locales
|
51
30
|
attr_accessor :Regions
|
52
|
-
attr_accessor :All
|
53
31
|
|
54
32
|
# Windows Timezone instance
|
55
33
|
# @param name [String] Windows Timezone name
|
56
34
|
def initialize(name)
|
57
35
|
@Name = name
|
58
|
-
@Data = WindowsData.new
|
59
36
|
@Valid = nil
|
60
|
-
set(@@Locales.dup, @@Regions.dup
|
37
|
+
set(@@Locales.dup, @@Regions.dup)
|
61
38
|
end
|
62
39
|
|
63
|
-
# Set locales
|
40
|
+
# Set locales and regions
|
64
41
|
# @param locales [Array<String>] search only in these locales
|
65
42
|
# @param regions [Array<String>] filter for these regions
|
66
|
-
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
67
43
|
# @return [WindowsZone] self
|
68
44
|
# @see Locales
|
69
45
|
# @see Regions
|
70
|
-
def set(locales = nil, regions = nil
|
46
|
+
def set(locales = nil, regions = nil)
|
71
47
|
@Locales = locales unless locales.nil?
|
72
48
|
@Regions = regions unless regions.nil?
|
73
|
-
@All = all ? true : false
|
74
49
|
self
|
75
50
|
end
|
76
51
|
|
@@ -78,42 +53,31 @@ module TimezoneParser
|
|
78
53
|
# @return [Boolean] whether timezone is valid
|
79
54
|
def isValid?
|
80
55
|
if @Valid.nil?
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
56
|
+
params = []
|
57
|
+
joins = ''
|
58
|
+
where = ''
|
59
|
+
|
60
|
+
if not @Locales.empty?
|
61
|
+
joins += ' LEFT JOIN `Locales` AS L ON TN.Locale = L.ID'
|
62
|
+
where = 'L.Name COLLATE NOCASE IN (' + Array.new(@Locales.count, '?').join(',') + ') AND '
|
63
|
+
params += @Locales
|
87
64
|
end
|
88
|
-
end
|
89
|
-
@Valid = false
|
90
|
-
end
|
91
65
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
@Loaded = true
|
97
|
-
@Valid = false
|
98
|
-
locales = @Locales
|
99
|
-
locales = Data::Storage.WindowsZones.keys if locales.empty?
|
100
|
-
locales.each do |locale|
|
101
|
-
next unless Data::Storage.WindowsZones.has_key?(locale)
|
102
|
-
entry = Data::Storage.WindowsZones[locale][@Name]
|
103
|
-
if entry
|
104
|
-
@Data.processEntry(entry, @Regions)
|
105
|
-
@Valid = true
|
106
|
-
return @Data unless @All
|
107
|
-
end
|
108
|
-
end
|
66
|
+
sql = "SELECT 1 FROM `WindowsZoneNames` TN #{joins} WHERE #{where}TN.`NameLowercase` = ? LIMIT 1"
|
67
|
+
params << @Name.downcase
|
68
|
+
|
69
|
+
@Valid = Data::Storage.getStatement(sql).execute(*params).count > 0
|
109
70
|
end
|
110
|
-
@
|
71
|
+
@Valid
|
111
72
|
end
|
112
73
|
|
113
74
|
# Windows Timezone identifier
|
114
75
|
# @return [String] Timezone identifier
|
115
76
|
def getZone
|
116
|
-
|
77
|
+
unless @Zone
|
78
|
+
@Zone = self.getFilteredData(:Zone).first
|
79
|
+
end
|
80
|
+
@Zone
|
117
81
|
end
|
118
82
|
|
119
83
|
# Check if given Windows Timezone name is a valid timezone
|
@@ -128,42 +92,108 @@ module TimezoneParser
|
|
128
92
|
# Get UTC offsets in seconds for given Windows Timezone name
|
129
93
|
# @param name [String] Windows Timezone name
|
130
94
|
# @param locales [Array<String>] search Timezone name only for these locales
|
131
|
-
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
132
95
|
# @return [Array<Fixnum>] list of timezone offsets in seconds
|
133
96
|
# @see Locales
|
134
|
-
def self.getOffsets(name, locales = nil
|
135
|
-
self.new(name).set(locales, nil
|
97
|
+
def self.getOffsets(name, locales = nil)
|
98
|
+
self.new(name).set(locales, nil).getOffsets
|
136
99
|
end
|
137
100
|
|
138
101
|
# Get Timezone identifiers for given Windows Timezone name
|
139
102
|
# @param name [String] Windows Timezone name
|
140
103
|
# @param locales [Array<String>] search Timezone name only for these locales
|
141
104
|
# @param regions [Array<String>] look for timezones only for these regions
|
142
|
-
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
143
105
|
# @return [Array<String>] list of timezone identifiers
|
144
106
|
# @see Locales
|
145
107
|
# @see Regions
|
146
|
-
def self.getTimezones(name, locales = nil, regions = nil
|
147
|
-
self.new(name).set(locales, regions
|
108
|
+
def self.getTimezones(name, locales = nil, regions = nil)
|
109
|
+
self.new(name).set(locales, regions).getTimezones
|
148
110
|
end
|
149
111
|
|
150
112
|
# Get Metazone identifiers for given Windows Timezone name
|
151
113
|
# @param name [String] Windows Timezone name
|
152
114
|
# @param locales [Array<String>] search Timezone name only for these locales
|
153
|
-
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
154
115
|
# @return [Array<String>] list of metazone identifiers
|
155
116
|
# @see Locales
|
156
|
-
def self.getMetazones(name, locales = nil
|
157
|
-
self.new(name).set(locales, nil
|
117
|
+
def self.getMetazones(name, locales = nil)
|
118
|
+
self.new(name).set(locales, nil).getMetazones
|
158
119
|
end
|
159
120
|
|
160
121
|
# Windows Timezone identifier
|
161
122
|
# @param name [String] Windows Timezone name
|
162
123
|
# @param locales [Array<String>] search Timezone name only for these locales
|
163
|
-
# @param all [Boolean] specify whether should search for all timezones or return as soon as found any
|
164
124
|
# @return [String] Timezone identifier
|
165
|
-
def self.getZone(name, locales = nil
|
166
|
-
self.new(name).set(locales, nil
|
125
|
+
def self.getZone(name, locales = nil)
|
126
|
+
self.new(name).set(locales, nil).getZone
|
167
127
|
end
|
128
|
+
|
129
|
+
protected
|
130
|
+
|
131
|
+
def getFilteredData(dataType)
|
132
|
+
params = []
|
133
|
+
column = nil
|
134
|
+
joins = ''
|
135
|
+
regionJoins = ''
|
136
|
+
useRegionFilter = !@Regions.nil? && !@Regions.empty?
|
137
|
+
case dataType
|
138
|
+
when :Zone
|
139
|
+
column = '`WindowsZones`.`Name`'
|
140
|
+
joins += ' LEFT JOIN `WindowsZoneName_Zones` AS NameZones ON NameZones.Name = ZN.ID'
|
141
|
+
joins += ' INNER JOIN `WindowsZones` ON WindowsZones.ID = NameZones.Zone'
|
142
|
+
when :Offsets
|
143
|
+
column = '`WindowsZones`.`Standard`, `WindowsZones`.`Daylight`, ZN.`Types`'
|
144
|
+
joins += ' LEFT JOIN `WindowsZoneName_Zones` AS NameZones ON NameZones.Name = ZN.ID'
|
145
|
+
joins += ' INNER JOIN `WindowsZones` ON WindowsZones.ID = NameZones.Zone'
|
146
|
+
when :Timezones
|
147
|
+
column = '`Timezones`.`Name`'
|
148
|
+
joins += ' LEFT JOIN `WindowsZoneName_Zones` AS NameZones ON NameZones.Name = ZN.ID'
|
149
|
+
joins += ' INNER JOIN `WindowsZone_Timezones` ZoneTimezones ON ZoneTimezones.Zone = NameZones.Zone'
|
150
|
+
joins += ' INNER JOIN `Timezones` ON ZoneTimezones.Timezone = Timezones.ID'
|
151
|
+
regionJoins += ' LEFT JOIN `Territories` ON ZoneTimezones.Territory = Territories.ID'
|
152
|
+
when :Metazones
|
153
|
+
raise StandardError, "Metazones is not implemented!"
|
154
|
+
when :Types
|
155
|
+
column = 'ZN.`Types`'
|
156
|
+
useRegionFilter = false
|
157
|
+
else
|
158
|
+
raise StandardError, "Unkown dataType '#{dataType}'"
|
159
|
+
end
|
160
|
+
|
161
|
+
if not @Locales.empty?
|
162
|
+
joins += ' LEFT JOIN `Locales` AS L ON ZN.Locale = L.ID'
|
163
|
+
where = 'L.Name COLLATE NOCASE IN (' + Array.new(@Locales.count, '?').join(',') + ') AND '
|
164
|
+
params += @Locales
|
165
|
+
end
|
166
|
+
|
167
|
+
sql = 'SELECT DISTINCT ' + column + ' FROM `WindowsZoneNames` AS ZN'
|
168
|
+
sql += joins
|
169
|
+
if useRegionFilter
|
170
|
+
sql += regionJoins
|
171
|
+
end
|
172
|
+
|
173
|
+
sql += " WHERE #{where}ZN.NameLowercase = ?"
|
174
|
+
params << @Name.downcase
|
175
|
+
|
176
|
+
if useRegionFilter
|
177
|
+
sql += ' AND Territories.Territory IN (' + Array.new(@Regions.count, '?').join(',') + ')'
|
178
|
+
params += @Regions
|
179
|
+
end
|
180
|
+
sql += ' ORDER BY ' + column
|
181
|
+
|
182
|
+
if dataType == :Offsets
|
183
|
+
allOffsets = Set.new
|
184
|
+
Data::Storage.getStatement(sql).execute(*params).each do |row|
|
185
|
+
allOffsets << row[0] if not (row.last & 0x01).zero?
|
186
|
+
allOffsets << row[1] if not (row.last & 0x02).zero?
|
187
|
+
end
|
188
|
+
result = allOffsets.sort
|
189
|
+
else
|
190
|
+
result = Data::Storage.getStatement(sql).execute(*params).collect { |row| row.first }
|
191
|
+
if dataType == :Types
|
192
|
+
result = self.class.convertTypes(result)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
result
|
196
|
+
end
|
197
|
+
|
168
198
|
end
|
169
199
|
end
|
@@ -4,13 +4,18 @@ module TimezoneParser
|
|
4
4
|
# Generic Timezone class
|
5
5
|
class ZoneInfo
|
6
6
|
protected
|
7
|
-
@Data = nil
|
8
|
-
@Loaded = false
|
9
7
|
@Offsets = nil
|
10
8
|
@Timezones = nil
|
11
9
|
@Metazones = nil
|
10
|
+
@TimezoneTypes = nil
|
11
|
+
@ToTime = nil
|
12
|
+
@FromTime = nil
|
12
13
|
|
13
14
|
public
|
15
|
+
|
16
|
+
TIMEZONE_TYPE_STANDARD = 0x01
|
17
|
+
TIMEZONE_TYPE_DAYLIGHT = 0X02
|
18
|
+
|
14
19
|
attr_accessor :ToTime
|
15
20
|
attr_accessor :FromTime
|
16
21
|
# Set time range
|
@@ -25,16 +30,12 @@ module TimezoneParser
|
|
25
30
|
self
|
26
31
|
end
|
27
32
|
|
28
|
-
# Get Timezone data
|
29
|
-
def getData
|
30
|
-
raise StandardError, '#getData must be implemented in subclass'
|
31
|
-
end
|
32
33
|
|
33
34
|
# Get UTC offsets in seconds
|
34
35
|
# @return [Array<Fixnum>] list of timezone offsets in seconds
|
35
36
|
def getOffsets
|
36
37
|
unless @Offsets
|
37
|
-
@Offsets =
|
38
|
+
@Offsets = self.getFilteredData(:Offsets)
|
38
39
|
end
|
39
40
|
@Offsets
|
40
41
|
end
|
@@ -43,28 +44,98 @@ module TimezoneParser
|
|
43
44
|
# @return [Array<String>] list of timezone identifiers
|
44
45
|
def getTimezones
|
45
46
|
unless @Timezones
|
46
|
-
@Timezones =
|
47
|
+
@Timezones = self.getFilteredData(:Timezones)
|
47
48
|
end
|
48
49
|
@Timezones
|
49
50
|
end
|
50
51
|
|
51
|
-
# Get types
|
52
|
-
# @return [Symbol] types
|
53
|
-
def getTypes
|
54
|
-
unless @Types
|
55
|
-
@Types = getData.Types.to_a
|
56
|
-
end
|
57
|
-
@Types
|
58
|
-
end
|
59
|
-
|
60
52
|
# Get Metazone identifiers
|
61
53
|
# @return [Array<String>] list of Metazone identifiers
|
62
54
|
def getMetazones
|
63
55
|
unless @Metazones
|
64
|
-
@Metazones =
|
56
|
+
@Metazones = self.getFilteredData(:Metazones)
|
65
57
|
end
|
66
58
|
@Metazones
|
67
59
|
end
|
68
60
|
|
61
|
+
# Get Types
|
62
|
+
# @return [Array<Symbol>] list of types
|
63
|
+
def getTypes
|
64
|
+
unless @TimezoneTypes
|
65
|
+
@TimezoneTypes = self.getFilteredData(:Types)
|
66
|
+
end
|
67
|
+
@TimezoneTypes
|
68
|
+
end
|
69
|
+
|
70
|
+
# Reset cached result
|
71
|
+
def reset
|
72
|
+
@Offsets = nil
|
73
|
+
@Timezones = nil
|
74
|
+
@Metazones = nil
|
75
|
+
@TimezoneTypes = nil
|
76
|
+
@ToTime = nil
|
77
|
+
@FromTime = nil
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
def getFilteredData(dataType)
|
83
|
+
raise StandardError, '#getFilteredData must be implemented in subclass'
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.findOffsets(timezones, toTime, fromTime, types = nil)
|
87
|
+
toTime = Time.now unless toTime
|
88
|
+
types = types.to_a unless types
|
89
|
+
types = [:daylight, :standard] if types.empty?
|
90
|
+
allOffsets = Set.new
|
91
|
+
timezones.each do |timezone|
|
92
|
+
begin
|
93
|
+
tz = TZInfo::Timezone.get(timezone)
|
94
|
+
rescue TZInfo::InvalidTimezoneIdentifier
|
95
|
+
tz = nil
|
96
|
+
end
|
97
|
+
next unless tz
|
98
|
+
offsets = []
|
99
|
+
self.addOffset(offsets, tz.period_for_utc(fromTime).offset, types)
|
100
|
+
tz.transitions_up_to(toTime, fromTime).each do |transition|
|
101
|
+
self.addOffset(offsets, transition.offset, types)
|
102
|
+
end
|
103
|
+
allOffsets += offsets
|
104
|
+
end
|
105
|
+
allOffsets.sort
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.addOffset(offsets, offset, types)
|
109
|
+
offsets << offset.utc_total_offset if (offset.dst? and types.include?(:daylight)) or (not offset.dst? and types.include?(:standard))
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.convertTypes(rawTypes)
|
113
|
+
types = Set.new
|
114
|
+
rawTypes.each do |t|
|
115
|
+
types << :standard unless (t.to_i & TIMEZONE_TYPE_STANDARD).zero?
|
116
|
+
types << :daylight unless (t.to_i & TIMEZONE_TYPE_DAYLIGHT).zero?
|
117
|
+
end
|
118
|
+
types.sort
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.findOffsetsFromTimezonesTypes(timezonesTypes, toTime, fromTime, types)
|
122
|
+
timezones = Set.new
|
123
|
+
timezoneTypes = Set.new
|
124
|
+
timezonesTypes.each do |timezoneType|
|
125
|
+
timezones << timezoneType[0]
|
126
|
+
timezoneTypes << timezoneType[1]
|
127
|
+
end
|
128
|
+
|
129
|
+
timezoneTypes = self.convertTypes(timezoneTypes)
|
130
|
+
|
131
|
+
if not types.nil? and not types.empty? and not timezoneTypes.empty?
|
132
|
+
types &= timezoneTypes
|
133
|
+
elsif not timezoneTypes.empty?
|
134
|
+
types = timezoneTypes
|
135
|
+
end
|
136
|
+
|
137
|
+
self.findOffsets(timezones, toTime, fromTime, types).sort
|
138
|
+
end
|
139
|
+
|
69
140
|
end
|
70
141
|
end
|
data/spec/abbreviation_spec.rb
CHANGED
@@ -76,7 +76,7 @@ describe TimezoneParser do
|
|
76
76
|
|
77
77
|
describe '#getTimezones' do
|
78
78
|
it 'should return all timezones for KMT abbreviation' do
|
79
|
-
expect(TimezoneParser::Abbreviation.new('KMT').getTimezones).to eq(['Europe/Kiev'])
|
79
|
+
expect(TimezoneParser::Abbreviation.new('KMT').setTime(DateTime.parse('1920-01-01T00:00:00+00:00')).getTimezones).to eq(['Europe/Kiev', 'Europe/Vilnius'])
|
80
80
|
end
|
81
81
|
|
82
82
|
context 'between specified time' do
|
@@ -86,6 +86,23 @@ describe TimezoneParser do
|
|
86
86
|
expect(TimezoneParser::Abbreviation.new('EET').setTime(DateTime.parse('1985-04-19T21:00:00+00:00'), DateTime.parse('1978-10-14T21:00:00+00:00')).getTimezones).to_not include('Europe/Istanbul')
|
87
87
|
end
|
88
88
|
end
|
89
|
+
|
90
|
+
context 'between times' do
|
91
|
+
it 'should include correct timezones for AWT' do
|
92
|
+
abbr = TimezoneParser::Abbreviation.new('AWT')
|
93
|
+
abbr.FromTime = nil
|
94
|
+
abbr.ToTime = DateTime.parse('1990-01-01T00:00:00+00:00')
|
95
|
+
expect(abbr.getTimezones).to eq(['Antarctica/Casey', 'Australia/Perth'])
|
96
|
+
|
97
|
+
abbr.reset
|
98
|
+
abbr.FromTime = DateTime.parse('2014-01-01T00:00:00+00:00')
|
99
|
+
abbr.ToTime = nil
|
100
|
+
expect(abbr.getTimezones).to eq(['Antarctica/Casey', 'Australia/Perth'])
|
101
|
+
|
102
|
+
abbr.reset
|
103
|
+
expect(abbr.setTime(DateTime.parse('2015-01-01T00:00:00+00:00'), DateTime.parse('1985-01-01T00:00:00+00:00')).getTimezones).to eq(['Antarctica/Casey', 'Australia/Perth'])
|
104
|
+
end
|
105
|
+
end
|
89
106
|
end
|
90
107
|
|
91
108
|
describe '#getMetazones' do
|
@@ -94,6 +111,13 @@ describe TimezoneParser do
|
|
94
111
|
end
|
95
112
|
end
|
96
113
|
|
114
|
+
describe '#getTypes' do
|
115
|
+
it 'should return types for abbreviations' do
|
116
|
+
expect(TimezoneParser::Abbreviation.new('EET').getTypes).to eq([:daylight, :standard])
|
117
|
+
expect(TimezoneParser::Abbreviation.new('EEST').getTypes).to eq([:daylight])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
97
121
|
describe '.isValid?' do
|
98
122
|
it 'should be valid abbreviation' do
|
99
123
|
expect(TimezoneParser::Abbreviation::isValid?('WAST')).to be true
|
data/spec/rails_zone_spec.rb
CHANGED
@@ -27,6 +27,10 @@ describe TimezoneParser do
|
|
27
27
|
it 'should return all offsets for "Grönland"' do
|
28
28
|
expect(TimezoneParser::RailsZone.new('Grönland').getOffsets).to eq([-10800, -7200])
|
29
29
|
end
|
30
|
+
|
31
|
+
it 'should return all offsets for "Ньюфаундленд" in CA region' do
|
32
|
+
expect(TimezoneParser::RailsZone.new('Ньюфаундленд').set( nil, ['CA']).getOffsets).to eq([-12600, -9000])
|
33
|
+
end
|
30
34
|
end
|
31
35
|
|
32
36
|
describe '#getTimezones' do
|
@@ -66,13 +70,13 @@ describe TimezoneParser do
|
|
66
70
|
|
67
71
|
describe '.getTimezones' do
|
68
72
|
it 'should find timezones' do
|
69
|
-
expect(TimezoneParser::RailsZone::getTimezones('치와와', []
|
73
|
+
expect(TimezoneParser::RailsZone::getTimezones('치와와', [])).to eq(['America/Chihuahua'])
|
70
74
|
end
|
71
75
|
end
|
72
76
|
|
73
77
|
describe '.getMetazones' do
|
74
|
-
it 'should
|
75
|
-
expect
|
78
|
+
it 'should raise error' do
|
79
|
+
expect { TimezoneParser::RailsZone::getMetazones('치와와') }.to raise_error(StandardError)
|
76
80
|
end
|
77
81
|
end
|
78
82
|
|