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.
@@ -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