TimezoneParser 0.4.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|