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.
@@ -0,0 +1,120 @@
1
+ # encoding: utf-8
2
+
3
+ module TimezoneParser
4
+ # Timezone abbreviation
5
+ class Abbreviation < ZoneInfo
6
+ protected
7
+ @@Regions = []
8
+
9
+ public
10
+ # Regions which will be used for Abbreviation methods if not specified there
11
+ #
12
+ # Each region is either ISO 3166-1 alpha-2 code
13
+ # @return [Array<String>] list containing region identifiers
14
+ # @see http://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
15
+ def self.Regions
16
+ @@Regions
17
+ end
18
+
19
+ attr_accessor :Regions
20
+ attr_accessor :Type
21
+
22
+ # Abbreviation instance
23
+ # @param abbreviation [String] Timezone abbreviation
24
+ def initialize(abbreviation)
25
+ @Abbreviation = abbreviation
26
+ @Data = Data.new
27
+ setTime
28
+ set(@@Regions.dup, nil)
29
+ end
30
+
31
+ # Set regions and type
32
+ # @param regions [Array<String>] filter for these regions
33
+ # @param type [Symbol] filter by type, :standard time or :daylight
34
+ # @return [Abbreviation] self
35
+ # @see Regions
36
+ def set(regions = nil, type = nil)
37
+ @Regions = regions unless regions.nil?
38
+ @Type = type.to_sym if type
39
+ self
40
+ end
41
+
42
+ # Check if abbreviation is valid
43
+ # @return [Boolean] whether abbreviation is valid
44
+ def isValid?
45
+ Data::Storage.Abbreviations.has_key?(@Abbreviation)
46
+ end
47
+
48
+ # Abbreviation data
49
+ # @return [Data] data
50
+ def getData
51
+ unless @Loaded
52
+ @Loaded = true
53
+ if isValid?
54
+ data = Data::Storage.Abbreviations[@Abbreviation]
55
+ if data.count == 1
56
+ @Data.processEntry(data.first, @ToTime, @FromTime, @Regions)
57
+ return @Data
58
+ end
59
+ entries = Data.filterData(data, @ToTime, @FromTime, @Type, @Regions)
60
+ entries.each do |entry|
61
+ @Data.processEntry(entry, @ToTime, @FromTime, @Regions)
62
+ end
63
+ return @Data
64
+ end
65
+ end
66
+ @Data
67
+ end
68
+
69
+ # Get UTC offsets in seconds
70
+ # @return [Array<Fixnum>] list of timezone offsets in seconds
71
+ def getOffsets
72
+ unless @Offsets
73
+ @Offsets = getData.Offsets.to_a
74
+ if @Offsets.empty? and not getTimezones.empty?
75
+ types = nil
76
+ types = [@Type] if @Type
77
+ @Offsets = @Data.findOffsets(@ToTime, @FromTime, @Regions, types).to_a
78
+ end
79
+ end
80
+ @Offsets
81
+ end
82
+
83
+ # Check if given Timezone abbreviation is a valid timezone
84
+ # @param abbreviation [String] Timezone abbreviation
85
+ # @return [Boolean] whether Timezone is valid
86
+ def self.isValid?(abbreviation)
87
+ Data::Storage.Abbreviations.has_key?(abbreviation)
88
+ end
89
+
90
+ # Get UTC offsets in seconds for given Timezone abbreviation
91
+ # @param abbreviation [String] Timezone abbreviation
92
+ # @param toTime [DateTime] look for offsets which came into effect before this date, exclusive
93
+ # @param fromTime [DateTime] look for offsets which came into effect at this date, inclusive
94
+ # @param regions [Array<String>] look for offsets only for these regions
95
+ # @param type [Symbol] specify whether offset should be :standard time or :daylight
96
+ # @return [Array<Fixnum>] list of timezone offsets in seconds
97
+ # @see Regions
98
+ def self.getOffsets(abbreviation, toTime = nil, fromTime = nil, regions = nil, type = nil)
99
+ self.new(abbreviation).setTime(toTime, fromTime).set(regions, type).getOffsets
100
+ end
101
+
102
+ # Get Timezone identifiers for given Timezone abbreviation
103
+ # @param abbreviation [String] Timezone abbreviation
104
+ # @param toTime [DateTime] look for timezones which came into effect before this date, exclusive
105
+ # @param fromTime [DateTime] look for timezones which came into effect at this date, inclusive
106
+ # @param regions [Array<String>] look for timezones only for these regions
107
+ # @param type [Symbol] specify whether timezones should be :standard time or :daylight
108
+ # @return [Array<String>] list of timezone identifiers
109
+ # @see Regions
110
+ def self.getTimezones(abbreviation, toTime = nil, fromTime = nil, regions = nil, type = nil)
111
+ self.new(abbreviation).setTime(toTime, fromTime).set(regions, type).getTimezones
112
+ end
113
+
114
+ # Get Metazone identifiers for given Timezone abbreviation
115
+ # @param abbreviation [String] Timezone abbreviation
116
+ def self.getMetazones(abbreviation)
117
+ self.new(abbreviation).getMetazones
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,121 @@
1
+ # encoding: utf-8
2
+ require 'pathname'
3
+ require 'set'
4
+ require 'tzinfo'
5
+ require 'timezone_parser/data/storage'
6
+
7
+ module TimezoneParser
8
+ # Timezone data
9
+ class Data
10
+ # Library Root directory
11
+ RootDir = Pathname.new(__FILE__).realpath.dirname.parent.parent
12
+ # Path to Data directory
13
+ DataDir = RootDir + 'data'
14
+ # Path to Vendor directory
15
+ VendorDir = RootDir + 'vendor'
16
+
17
+ attr_reader :Offsets
18
+ attr_reader :Timezones
19
+ attr_reader :Types
20
+ attr_reader :Metazones
21
+ def initialize
22
+ @Offsets = SortedSet.new
23
+ @Timezones = SortedSet.new
24
+ @Types = SortedSet.new
25
+ @Metazones = SortedSet.new
26
+ end
27
+
28
+ def processEntry(entry, toTime, fromTime, regions = [])
29
+ @Timezones += entry['Timezones'] if entry['Timezones']
30
+ @Offsets << entry['Offset'] if entry['Offset']
31
+ @Types += entry['Types'].map(&:to_sym) if entry['Types']
32
+ if entry.has_key?('Metazones')
33
+ entry['Metazones'].each do |zone|
34
+ @Metazones << zone
35
+ @Timezones += Storage.getTimezones(zone, toTime, fromTime, regions)
36
+ end
37
+ end
38
+ self
39
+ end
40
+
41
+ def findOffsets(toTime, fromTime, regions = nil, types = nil)
42
+ types = @Types.to_a unless types
43
+ types = [:daylight, :standard] if types.empty?
44
+ @Timezones.each do |timezone|
45
+ if regions and not regions.empty?
46
+ timezoneRegions = Data::Storage.TimezoneCountries[timezone]
47
+ next if timezoneRegions and (timezoneRegions & regions).empty?
48
+ end
49
+ begin
50
+ tz = TZInfo::Timezone.get(timezone)
51
+ rescue TZInfo::InvalidTimezoneIdentifier
52
+ tz = nil
53
+ end
54
+ next unless tz
55
+ offsets = []
56
+ ts = false
57
+ tz.transitions_up_to(toTime, fromTime).each do |transition|
58
+ ts = true
59
+ self.class.addOffset(offsets, transition.offset, types)
60
+ end
61
+ self.class.addOffset(offsets, tz.period_for_utc(toTime - 0.001).offset, types) unless ts
62
+ @Offsets += offsets
63
+ end
64
+ @Offsets
65
+ end
66
+
67
+ # Load data entries which match specified time
68
+ # @param data [Array<Hash>] array of entries
69
+ # @param toTime [DateTime] entries before this date, exclusive
70
+ # @param fromTime [DateTime] entries after/at this date, inclusive
71
+ # @return [Array<Hash>] resulting array containing filtered entries
72
+ def self.loadEntries(data, toTime, fromTime, offsets = false)
73
+ result = []
74
+ data.each do |entry|
75
+ result << entry if (entry['From'] && entry['To'] and toTime > entry['From'] and fromTime < entry['To']) or
76
+ (entry['From'] && !entry['To'] and toTime > entry['From']) or
77
+ (!entry['From'] && entry['To'] and fromTime < entry['To']) or
78
+ (!entry['From'] && !entry['To'])
79
+ end
80
+ result.each do |entry|
81
+ return result if not offsets or (offsets and entry['Offset'])
82
+ end
83
+ data.each_index do |i|
84
+ entry = data[i]
85
+ nextentry = offsets ? getNextEntry(data, i) : data[i+1]
86
+ result << entry if ( entry['From'] && entry['To'] and toTime > entry['From'] and fromTime < entry['To'] ) or
87
+ ( entry['To'] and ( (nextentry.nil? and fromTime >= entry['To']) or
88
+ (nextentry and nextentry['From'] and fromTime >= entry['To'] and toTime <= nextentry['From']) or
89
+ (!entry['From'] and fromTime < entry['To']) ) ) or
90
+ ( entry['From'] and ( (i.zero? and toTime <= entry['From']) or (!entry['To'] and toTime > entry['From']) ) )
91
+ end
92
+ result
93
+ end
94
+
95
+ def self.filterData(data, toTime, fromTime, type, regions)
96
+ entries = []
97
+ data.each do |entry|
98
+ next if type and entry['Types'] and not entry['Types'].include?(type)
99
+ next if not regions.empty? and entry['Countries'] and (entry['Countries'] & regions).empty?
100
+ entries << entry
101
+ end
102
+ loadEntries(entries, toTime, fromTime, true)
103
+ end
104
+
105
+ protected
106
+
107
+ def self.getNextEntry(data, i)
108
+ j = 1
109
+ begin
110
+ entry = data[i+j]
111
+ j += 1
112
+ end until entry.nil? or (entry and entry['Offset'])
113
+ entry
114
+ end
115
+
116
+ def self.addOffset(offsets, offset, types)
117
+ offsets << offset.utc_total_offset if (offset.dst? and types.include?(:daylight)) or (not offset.dst? and types.include?(:standard))
118
+ end
119
+
120
+ end
121
+ end
@@ -0,0 +1,175 @@
1
+ # encoding: utf-8
2
+ require 'cldr'
3
+ require 'cldr/download'
4
+ require 'cldr/export/data'
5
+ require 'cldr/export/data/timezones'
6
+ require 'pathname'
7
+
8
+ module TimezoneParser
9
+ # CLDR module
10
+ module CLDR
11
+
12
+ # Data directory
13
+ DataDir = Pathname.new(Cldr::Export::Data.dir)
14
+
15
+ protected
16
+ @@Version = nil
17
+
18
+ public
19
+ def self.download(source = 'http://unicode.org/Public/cldr/latest/core.zip', target = nil)
20
+ Cldr.download(source, target)
21
+ end
22
+
23
+ def self.updateHash(hash, name, data)
24
+ hash[name] ||= []
25
+ hash[name] << data.to_s
26
+ hash[name].uniq!
27
+ hash[name].sort!
28
+ hash
29
+ end
30
+
31
+ def self.getVersion(source = DataDir)
32
+ return @@Version if @@Version
33
+ content = File.read(source + 'dtd' + 'ldml.dtd')
34
+ content.gsub!(/<!--.*?-->/, '')
35
+ data = content.match(/\s+cldrVersion\s+[\#\w\s]+\s+"(\d+)"\s*\>/)
36
+ @@Version = data[1].to_i if data
37
+ @@Version
38
+ end
39
+
40
+ def self.getTimezones
41
+ timezones = { }
42
+ Cldr::Export::Data.locales.sort.each do |locale|
43
+ tz = Cldr::Export::Data::Timezones.new(locale)
44
+ next if tz.timezones.empty? and tz.metazones.empty?
45
+
46
+ tz.timezones.each do |timezone, data|
47
+ next if timezone == :'Etc/Unknown' or data[:city].nil?
48
+ city = data[:city].to_s.encode(Encoding::UTF_8).chomp.strip
49
+ timezones[locale] ||= {}
50
+ timezones[locale][city] ||= {}
51
+ self.updateHash(timezones[locale][city], 'Timezones', timezone)
52
+ data[:long].to_a.each do |type, name|
53
+ name = name.to_s.encode(Encoding::UTF_8).chomp.strip
54
+ type = type.to_s.encode(Encoding::UTF_8)
55
+ timezones[locale][name] ||= {}
56
+ if type == 'generic'
57
+ self.updateHash(timezones[locale][name], 'Types', 'standard')
58
+ self.updateHash(timezones[locale][name], 'Types', 'daylight')
59
+ else
60
+ self.updateHash(timezones[locale][name], 'Types', type)
61
+ end
62
+ self.updateHash(timezones[locale][name], 'Timezones', timezone.to_s.encode(Encoding::UTF_8))
63
+ end
64
+ end
65
+ tz.metazones.each do |metazone, data|
66
+ data[:long].to_a.each do |type, name|
67
+ name = name.to_s.encode(Encoding::UTF_8).chomp.strip
68
+ next if name.empty?
69
+ type = type.to_s.encode(Encoding::UTF_8)
70
+ timezones[locale] ||= {}
71
+ timezones[locale][name] ||= {}
72
+ if type == 'generic'
73
+ self.updateHash(timezones[locale][name], 'Types', 'standard')
74
+ self.updateHash(timezones[locale][name], 'Types', 'daylight')
75
+ else
76
+ self.updateHash(timezones[locale][name], 'Types', type)
77
+ end
78
+ self.updateHash(timezones[locale][name], 'Metazones', metazone.to_s.encode(Encoding::UTF_8))
79
+ end
80
+ end
81
+ timezones[locale] = Hash[timezones[locale].to_a.sort_by { |d| d.first } ] if timezones[locale]
82
+ end
83
+ timezones
84
+ end
85
+
86
+ def self.updateAbbreviations(abbreviations)
87
+ Cldr::Export::Data.locales.sort.each do |locale|
88
+ tz = Cldr::Export::Data::Timezones.new(locale)
89
+ next if tz.timezones.empty? and tz.metazones.empty?
90
+ tz.timezones.each do |timezone, data|
91
+ data[:short].to_a.each do |type, name|
92
+ next if name == '∅∅∅'
93
+ name = name.chomp.strip
94
+ type = type.to_s.encode(Encoding::UTF_8)
95
+ abbreviations[name] ||= []
96
+ data = {}
97
+ add = true
98
+ abbreviations[name].each_index do |i|
99
+ next unless abbreviations[name][i]['Offset'].nil?
100
+ data = abbreviations[name][i]
101
+ add = false
102
+ break
103
+ end
104
+ if type == 'generic'
105
+ self.updateHash(data, 'Types', 'standard')
106
+ self.updateHash(data, 'Types', 'daylight')
107
+ else
108
+ self.updateHash(data, 'Types', type)
109
+ end
110
+ self.updateHash(data, 'Timezones', timezone)
111
+ abbreviations[name] << data if add
112
+ end
113
+ end
114
+ tz.metazones.each do |metazone, data|
115
+ data[:short].to_a.each do |type, name|
116
+ next if name == '∅∅∅'
117
+ name = name.chomp.strip
118
+ type = type.to_s.encode(Encoding::UTF_8)
119
+ abbreviations[name] ||= []
120
+ data = {}
121
+ add = true
122
+ abbreviations[name].each_index do |i|
123
+ next unless abbreviations[name][i]['Offset'].nil?
124
+ data = abbreviations[name][i]
125
+ add = false
126
+ break
127
+ end
128
+ if type == 'generic'
129
+ self.updateHash(data, 'Types', 'standard')
130
+ self.updateHash(data, 'Types', 'daylight')
131
+ else
132
+ self.updateHash(data, 'Types', type)
133
+ end
134
+ self.updateHash(data, 'Metazones', metazone)
135
+ abbreviations[name] << data if add
136
+ end
137
+ end
138
+ end
139
+ abbreviations = Hash[abbreviations.to_a.sort_by { |d| d.first } ]
140
+ abbreviations
141
+ end
142
+
143
+ def self.getMetazones
144
+ zones = {}
145
+ Cldr::Export::Data::Metazones.new[:timezones].each do |timezone, zonedata|
146
+ zonedata.each do |data|
147
+ entry = {}
148
+ add = true
149
+ zones[data['metazone']] ||= []
150
+ zones[data['metazone']].each_index do |i|
151
+ next if zones[data['metazone']][i]['From'].to_s != data['from'].to_s or zones[data['metazone']][i]['To'].to_s != data['to'].to_s
152
+ entry = zones[data['metazone']][i]
153
+ add = false
154
+ break
155
+ end
156
+ self.updateHash(entry, 'Timezones', timezone)
157
+ if add
158
+ entry['From'] = data['from'].to_s if data['from']
159
+ entry['To'] = data['to'].to_s if data['to']
160
+ zones[data['metazone']] << entry
161
+ end
162
+ zones[data['metazone']].sort_by! { |d| [d['To'] ? d['To'] : 'zzzz', d['From'] ? d['From'] : ''] }
163
+ end
164
+ end
165
+ zones = Hash[zones.to_a.sort_by { |d| d.first } ]
166
+ zones
167
+ end
168
+
169
+ def self.getWindowsZones
170
+ zones = Cldr::Export::Data::WindowsZones.new
171
+ zones = Hash[zones.to_a.sort_by { |d| d.first } ]
172
+ zones
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,172 @@
1
+ # encoding: utf-8
2
+ require 'yaml'
3
+ require 'date'
4
+ require 'insensitive_hash'
5
+
6
+ module TimezoneParser
7
+ class Data
8
+ # Timezone data Storage class
9
+ class Storage
10
+ protected
11
+ @@Abbreviations = nil
12
+ @@Timezones = nil
13
+ @@TimezoneCountries = nil
14
+ @@Metazones = nil
15
+ @@WindowsZones = nil
16
+ @@WindowsTimezones = nil
17
+ @@WindowsOffsets = nil
18
+ @@RailsZones = nil
19
+ @@RailsTranslated = nil
20
+
21
+ public
22
+ def self.Abbreviations
23
+ unless @@Abbreviations
24
+ @@Abbreviations = Marshal.load(File.open(Data::DataDir + 'abbreviations.dat')).insensitive
25
+ @@Abbreviations.each do |abbr, data|
26
+ proccessData(data)
27
+ end
28
+ end
29
+ @@Abbreviations
30
+ end
31
+
32
+ def self.Timezones
33
+ unless @@Timezones
34
+ @@Timezones = Marshal.load(File.open(Data::DataDir + 'timezones.dat')).insensitive
35
+ end
36
+ @@Timezones
37
+ end
38
+
39
+ def self.TimezoneCountries
40
+ unless @@TimezoneCountries
41
+ @@TimezoneCountries = Marshal.load(File.open(Data::DataDir + 'countries.dat')).insensitive
42
+ end
43
+ @@TimezoneCountries
44
+ end
45
+
46
+ def self.Metazones
47
+ unless @@Metazones
48
+ @@Metazones = Marshal.load(File.open(Data::DataDir + 'metazones.dat')).insensitive
49
+ @@Metazones.each do |zone, data|
50
+ proccessData(data)
51
+ end
52
+ end
53
+ @@Metazones
54
+ end
55
+
56
+ def self.WindowsZones
57
+ unless @@WindowsZones
58
+ @@WindowsZones = Marshal.load(File.open(Data::DataDir + 'windows_zonenames.dat')).insensitive
59
+ end
60
+ @@WindowsZones
61
+ end
62
+
63
+ def self.WindowsTimezones
64
+ unless @@WindowsTimezones
65
+ @@WindowsTimezones = Marshal.load(File.open(Data::DataDir + 'windows_timezones.dat')).insensitive
66
+ end
67
+ @@WindowsTimezones
68
+ end
69
+
70
+ def self.WindowsOffsets
71
+ unless @@WindowsOffsets
72
+ @@WindowsOffsets = Marshal.load(File.open(Data::DataDir + 'windows_offsets.dat')).insensitive
73
+ end
74
+ @@WindowsOffsets
75
+ end
76
+
77
+ def self.RailsZones
78
+ unless @@RailsZones
79
+ @@RailsZones = Marshal.load(File.open(Data::DataDir + 'rails.dat')).insensitive
80
+ end
81
+ @@RailsZones
82
+ end
83
+
84
+ def self.RailsTranslated
85
+ unless @@RailsTranslated
86
+ @@RailsTranslated = Marshal.load(File.open(Data::DataDir + 'rails_i18n.dat')).insensitive
87
+ end
88
+ @@RailsTranslated
89
+ end
90
+
91
+ def self.preload(modules)
92
+ preloaded = false
93
+ modules.each do |m|
94
+ case m
95
+ when :Abbreviations
96
+ self.Abbreviations
97
+ self.Metazones
98
+ preloaded = true
99
+ when :Timezones
100
+ self.Timezones
101
+ self.Metazones
102
+ preloaded = true
103
+ when :WindowsZones
104
+ self.WindowsZones
105
+ self.WindowsTimezones
106
+ self.WindowsOffsets
107
+ preloaded = true
108
+ when :RailsZones
109
+ self.RailsZones
110
+ self.RailsTranslated
111
+ preloaded = true
112
+ end
113
+ end
114
+ preloaded
115
+ end
116
+
117
+ def self.getTimezones(metazone, toTime, fromTime, regions = [])
118
+ timezones = SortedSet.new
119
+ if self.Metazones.has_key?(metazone)
120
+ entries = Data::loadEntries(self.Metazones[metazone], toTime, fromTime)
121
+ entries.each do |entry|
122
+ add = true
123
+ timezones += entry['Timezones'].select do |timezone|
124
+ if regions.empty?
125
+ true
126
+ else
127
+ timezoneRegions = self.TimezoneCountries[timezone]
128
+ timezoneRegions && !(regions & timezoneRegions).empty?
129
+ end
130
+ end
131
+ end
132
+ end
133
+ timezones
134
+ end
135
+
136
+ def self.getTimezones2(zone, regions = [])
137
+ timezones = SortedSet.new
138
+ if self.WindowsTimezones.has_key?(zone)
139
+ entries = self.WindowsTimezones[zone]
140
+ regions = entries.keys if regions.empty?
141
+ regions.each do |region|
142
+ next unless entries.has_key?(region)
143
+ timezones += entries[region]
144
+ end
145
+ end
146
+ timezones
147
+ end
148
+
149
+ def self.getOffsets(zone, types = [])
150
+ offsets = SortedSet.new
151
+ if self.WindowsOffsets.has_key?(zone)
152
+ data = self.WindowsOffsets[zone]
153
+ types = [:standard, :daylight] if types.empty?
154
+ types.each do |type|
155
+ next unless data.has_key?(type)
156
+ offsets << data[type]
157
+ end
158
+ end
159
+ offsets
160
+ end
161
+
162
+ protected
163
+
164
+ def self.proccessData(data)
165
+ data.each do |entry|
166
+ entry['From'] = DateTime.parse(entry['From']) if entry['From']
167
+ entry['To'] = DateTime.parse(entry['To']) if entry['To']
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end