TimezoneParser 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  # encoding: utf-8
2
2
  module TimezoneParser
3
3
  # Version
4
- VERSION = '0.4.0'
4
+ VERSION = '1.0.0'
5
5
  end
@@ -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, true)
37
+ set(@@Locales.dup, @@Regions.dup)
61
38
  end
62
39
 
63
- # Set locales, regions and all
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, all = true)
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
- locales = @Locales
82
- locales = Data::Storage.WindowsZones.keys if locales.empty?
83
- locales.each do |locale|
84
- next unless Data::Storage.WindowsZones.has_key?(locale)
85
- @Valid = Data::Storage.WindowsZones[locale].has_key?(@Name)
86
- return @Valid if @Valid
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
- # Windows Timezone data
93
- # @return [WindowsData] data
94
- def getData
95
- unless @Loaded
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
- @Data
71
+ @Valid
111
72
  end
112
73
 
113
74
  # Windows Timezone identifier
114
75
  # @return [String] Timezone identifier
115
76
  def getZone
116
- getData.WindowsZone
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, all = true)
135
- self.new(name).set(locales, nil, all).getOffsets
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, all = true)
147
- self.new(name).set(locales, regions, all).getTimezones
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, all = true)
157
- self.new(name).set(locales, nil, all).getMetazones
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, all = true)
166
- self.new(name).set(locales, nil, all).getZone
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 = getData.Offsets.to_a
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 = getData.Timezones.to_a
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 = getData.Metazones.to_a
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
@@ -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
@@ -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('치와와', [], true)).to eq(['America/Chihuahua'])
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 return zone names' do
75
- expect(TimezoneParser::RailsZone::getMetazones('치와와')).to eq(['Chihuahua'])
78
+ it 'should raise error' do
79
+ expect { TimezoneParser::RailsZone::getMetazones('치와와') }.to raise_error(StandardError)
76
80
  end
77
81
  end
78
82