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,151 @@
1
+ # encoding: utf-8
2
+ require 'tzinfo'
3
+ require 'tzinfo/data/tzdataparser'
4
+ require 'uri'
5
+ require 'open-uri'
6
+ require 'rubygems/package'
7
+ require 'zlib'
8
+ require 'yaml'
9
+ require 'pathname'
10
+ require 'timezone_parser/data'
11
+
12
+ module TimezoneParser
13
+ # TZInfo module
14
+ module TZInfo
15
+
16
+ protected
17
+ @@Version = nil
18
+ @@TimezoneCountries = nil
19
+
20
+ public
21
+
22
+ # TZData source location
23
+ TZDataSource = 'ftp://ftp.iana.org/tz/tzdata-latest.tar.gz'
24
+ # Extracted TZData path
25
+ TZDataPath = TimezoneParser::Data::VendorDir + 'zoneinfo'
26
+ # TZInfo data path
27
+ TZInfoData = TimezoneParser::Data::VendorDir + 'tzinfo'
28
+ # Max Timestamp
29
+ LastTimestamp = 2147483647
30
+ def self.download(source = TZDataSource, location = TZDataPath, target = TZInfoData)
31
+ URI.parse(source).open do |tempfile|
32
+ FileUtils.mkdir_p(location)
33
+ tar = Gem::Package::TarReader.new(Zlib::GzipReader.open(tempfile.path))
34
+ tar.each do |entry|
35
+ path = location + entry.full_name
36
+ FileUtils.mkdir_p(path.dirname)
37
+ if entry.file?
38
+ File.open(path, 'wb') do |file|
39
+ file.write(entry.read)
40
+ end
41
+ end
42
+ end
43
+ tar.close
44
+ end
45
+ parser = ::TZInfo::Data::TZDataParser.new(location, target)
46
+ parser.execute
47
+ getVersion(location)
48
+ end
49
+
50
+ def self.getVersion(source = TZDataPath)
51
+ return @@Version if @@Version
52
+ File.open(source + 'Makefile', 'r', { :encoding => 'UTF-8:UTF-8' }) do |file|
53
+ file.each_line do |line|
54
+ line = line.gsub(/#.*$/, '')
55
+ v = line.match(/^\s*VERSION\s*=\s*(\w+)\s*$/)
56
+ @@Version = v[1] if v
57
+ end
58
+ end
59
+ @@Version
60
+ end
61
+
62
+ def self.init
63
+ ::TZInfo::DataSource.set(:ruby, TZInfoData)
64
+ end
65
+
66
+ def self.getTimezoneCountries
67
+ unless @@TimezoneCountries
68
+ @@TimezoneCountries = {}
69
+ ::TZInfo::Country.all.each do |countryData|
70
+ countryData.zone_identifiers.each do |timezone|
71
+ @@TimezoneCountries[timezone] ||= []
72
+ @@TimezoneCountries[timezone] << countryData.code
73
+ @@TimezoneCountries[timezone].uniq!
74
+ @@TimezoneCountries[timezone].sort!
75
+ end
76
+ end
77
+ @@TimezoneCountries = Hash[@@TimezoneCountries.to_a.sort_by { |pair| pair.first }]
78
+ end
79
+ @@TimezoneCountries
80
+ end
81
+
82
+ def self.getAbbreviations
83
+ transitionData = {}
84
+ ::TZInfo::Timezone.all_data_zone_identifiers.each do |name|
85
+ zone = ::TZInfo::Timezone.get(name)
86
+ zone_transitions = zone.transitions_up_to(Time.at(LastTimestamp))
87
+ zone_transitions.each_index do |i|
88
+ offset = zone_transitions[i].offset
89
+ next if offset.abbreviation == :LMT or offset.abbreviation == :zzz
90
+ abbr = offset.abbreviation.to_s
91
+ transitionData[abbr] = [] unless transitionData[abbr]
92
+ period = ::TZInfo::TimezonePeriod.new(zone_transitions[i], zone_transitions[i+1])
93
+ transitionData[abbr] << { :name => name, :period => period }
94
+ end
95
+ end
96
+ timezoneData = {}
97
+ transitionData.keys.sort.each do |name|
98
+ transitions = []
99
+ transitionData[name].each do |transition|
100
+ period_start = transition[:period].utc_start
101
+ period_start = period_start.to_s
102
+ period_end = transition[:period].utc_end
103
+ period_end = period_end.to_s if period_end
104
+ timezone = transition[:name]
105
+ countries = getTimezoneCountries[timezone]
106
+ countries = countries.dup if countries
107
+ data = { 'Offset' => transition[:period].utc_total_offset, 'Timezones' => [timezone], 'Countries' => countries, 'From' => period_start }
108
+ data['To'] = period_end if period_end
109
+ transitions << data
110
+ end
111
+ transitions.sort_by! { |data| [data['To'] ? data['To'] : 'zzzz', data['From']] }
112
+
113
+ abbreviationData = []
114
+ abbreviationData << transitions.shift
115
+ transitions.each do |data|
116
+ current = abbreviationData.last
117
+ if data['Offset'] == current['Offset'] and (current['Timezones'].sort == data['Timezones'].sort || (data['From'] == current['From'] and data['To'] == current['To']))
118
+ current['Timezones'] += data['Timezones']
119
+ current['Timezones'].uniq!
120
+ current['Timezones'].sort!
121
+ if data['Countries']
122
+ current['Countries'] = current['Countries'].to_a + data['Countries']
123
+ current['Countries'].uniq!
124
+ current['Countries'].sort!
125
+ end
126
+ current['To'] = data['To']
127
+ current.delete('To') unless current['To']
128
+ else
129
+ abbreviationData << data
130
+ end
131
+ end
132
+ previous = nil
133
+ abbreviationData.delete_if do |item|
134
+ if previous.nil?
135
+ previous = item
136
+ false
137
+ elsif previous['Offset'] == item['Offset'] and previous['Timezones'] == item['Timezones']
138
+ previous['To'] = item['To']
139
+ previous.delete('To') unless previous['To']
140
+ true
141
+ else
142
+ previous = item
143
+ false
144
+ end
145
+ end
146
+ timezoneData[name] = abbreviationData
147
+ end
148
+ timezoneData
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,124 @@
1
+ # encoding: utf-8
2
+ require 'win32/registry'
3
+ require 'fiddle'
4
+
5
+ module TimezoneParser
6
+ # Windows module
7
+ module Windows
8
+
9
+ protected
10
+
11
+ @@Version = nil
12
+ @@Errors = ''
13
+
14
+ public
15
+ # Windows Registry path to Time Zone data
16
+ TimeZonePath = 'SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones'
17
+ def self.errors
18
+ @@Errors
19
+ end
20
+
21
+ def self.getVersion(path = TimeZonePath)
22
+ return @@Version if @@Version
23
+ begin
24
+ Win32::Registry::HKEY_LOCAL_MACHINE.open(path, Win32::Registry::KEY_READ) do |reg|
25
+ @@Version = reg['TzVersion', Win32::Registry::REG_DWORD].to_s(16)
26
+ end
27
+ rescue Win32::Registry::Error => e
28
+ @@Errors << e.message
29
+ end
30
+ @@Version
31
+ end
32
+
33
+ def self.getTimezones(path = TimeZonePath)
34
+ timezones = {}
35
+ begin
36
+ Win32::Registry::HKEY_LOCAL_MACHINE.open(path, Win32::Registry::KEY_READ).each_key do |key, wtime|
37
+ Win32::Registry::HKEY_LOCAL_MACHINE.open(path + '\\' + key, Win32::Registry::KEY_READ) do |reg|
38
+ timezones[key] ||= {}
39
+ tzi = reg.read('TZI', Win32::Registry::REG_BINARY).last
40
+ # TZI Structure (http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481.aspx)
41
+ # typedef struct _REG_TZI_FORMAT
42
+ # {
43
+ # LONG Bias;
44
+ # LONG StandardBias;
45
+ # LONG DaylightBias;
46
+ # SYSTEMTIME StandardDate;
47
+ # SYSTEMTIME DaylightDate;
48
+ # } REG_TZI_FORMAT;
49
+ unpacked = tzi.unpack('lllSSSSSSSSSSSSSSSS')
50
+ timezones[key]['standard'] = (0 - unpacked[0] - unpacked[1]) * 60
51
+ timezones[key]['daylight'] = (0 - unpacked[0] - unpacked[2]) * 60
52
+ end
53
+ end
54
+ rescue Win32::Registry::Error => e
55
+ @@Errors << e.message
56
+ end
57
+ timezones = Hash[timezones.to_a.sort_by { |d| d.first } ]
58
+ timezones
59
+ end
60
+
61
+ def self.getMUIOffsets(path = TimeZonePath)
62
+ offsets = {}
63
+ begin
64
+ Win32::Registry::HKEY_LOCAL_MACHINE.open(path, Win32::Registry::KEY_READ).each_key do |key, wtime|
65
+ Win32::Registry::HKEY_LOCAL_MACHINE.open(path + '\\' + key, Win32::Registry::KEY_READ) do |reg|
66
+ muiDlt = reg.read_s('MUI_Dlt')
67
+ muiStd = reg.read_s('MUI_Std')
68
+
69
+ offsets[self.parseMUI(muiDlt)] = ['daylight', key]
70
+ offsets[self.parseMUI(muiStd)] = ['standard', key]
71
+ end
72
+ end
73
+ rescue Win32::Registry::Error => e
74
+ @@Errors << e.message
75
+ end
76
+ puts @@Errors
77
+ offsets
78
+ end
79
+
80
+ def self.parseMUI(str)
81
+ str.split(',').last.to_i.abs
82
+ end
83
+
84
+ def self.parseMetazones(metazoneList, offsets)
85
+ metazones = {}
86
+ metazoneList.each do |lcid, data|
87
+ localeMem = Fiddle::Pointer.malloc(LOCALE_NAME_MAX_LENGTH)
88
+ chars = LCIDToLocaleName.call(lcid, localeMem, LOCALE_NAME_MAX_LENGTH, 0)
89
+ return nil if chars.zero?
90
+ locale = localeMem.to_s((chars-1)*2).force_encoding(Encoding::UTF_16LE).encode(Encoding::UTF_8)
91
+ metazones[locale] = {}
92
+ offsets.each do |id, info|
93
+ name = data[id]
94
+ metazones[locale][name] ||= {}
95
+ metazones[locale][name]['Types'] ||= []
96
+ metazones[locale][name]['Metazones'] ||= []
97
+ metazones[locale][name]['Types'] << info.first
98
+ metazones[locale][name]['Metazones'] << info.last
99
+ metazones[locale][name]['Types'].uniq!
100
+ metazones[locale][name]['Metazones'].uniq!
101
+ end
102
+ metazones[locale] = Hash[metazones[locale].to_a.sort_by { |d| d.first } ]
103
+ end
104
+ metazones = Hash[metazones.to_a.sort_by { |d| d.first } ]
105
+ metazones
106
+ end
107
+
108
+ # Windows Kernel32 library
109
+ kernel32 = Fiddle.dlopen('kernel32')
110
+
111
+ # function
112
+ # int LCIDToLocaleName (
113
+ # _In_ LCID Locale,
114
+ # _Out_opt_ LPWSTR lpName,
115
+ # _In_ int cchName,
116
+ # _In_ DWORD dwFlags
117
+ # );
118
+ # @see http://msdn.microsoft.com/en-us/library/windows/desktop/dd318698.aspx
119
+ LCIDToLocaleName = Fiddle::Function.new( kernel32['LCIDToLocaleName'],
120
+ [Fiddle::TYPE_LONG, Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_LONG], Fiddle::TYPE_INT )
121
+ # Max locale length
122
+ LOCALE_NAME_MAX_LENGTH = 85
123
+ end
124
+ end
@@ -0,0 +1,178 @@
1
+ # encoding: utf-8
2
+
3
+ module TimezoneParser
4
+ # Rails zone data
5
+ class RailsData < Data
6
+ protected
7
+ @RailsZone = nil
8
+
9
+ public
10
+ attr_reader :RailsZone
11
+ def processEntry(data, rails)
12
+ if rails
13
+ @RailsZone = rails
14
+ @Metazones << rails
15
+ @Timezones << data
16
+ else
17
+ rails = Storage.RailsZones[data]
18
+ if rails
19
+ @RailsZone = data
20
+ @Metazones << data
21
+ @Timezones << rails
22
+ end
23
+ end
24
+ self
25
+ end
26
+ end
27
+
28
+ # Rails zone
29
+ class RailsZone < ZoneInfo
30
+ protected
31
+ @@Locales = []
32
+
33
+ public
34
+ # Locales which will be used for RailsZone methods if not specified there
35
+ #
36
+ # Each locale is language identifier based on IETF BCP 47 and ISO 639 code
37
+ # @return [Array<String>] list containing locale identifiers
38
+ # @see http://en.wikipedia.org/wiki/IETF_language_tag
39
+ def self.Locales
40
+ @@Locales
41
+ end
42
+
43
+ attr_accessor :All
44
+
45
+ # Rails zone instance
46
+ # @param name [String] Rails zone name
47
+ def initialize(name)
48
+ @Name = name
49
+ @Data = RailsData.new
50
+ @Valid = nil
51
+ setTime
52
+ set(@@Locales.dup, true)
53
+ end
54
+
55
+ # Set locales and all
56
+ # @param locales [Array<String>] search only in these locales
57
+ # @param all [Boolean] specify whether should search for all zones or return as soon as found any
58
+ # @return [RailsZone] self
59
+ # @see Locales
60
+ def set(locales = nil, all = true)
61
+ @Locales = locales unless locales.nil?
62
+ @All = all ? true : false
63
+ self
64
+ end
65
+
66
+ # Check if Rails zone is valid
67
+ # @return [Boolean] whether Rails zone is valid
68
+ def isValid?
69
+ if @Valid.nil?
70
+ @Valid = false
71
+ @Valid = Data::Storage.RailsZones.has_key?(@Name) if (not @Locales) or (@Locales and @Locales.include?('en'))
72
+ return @Valid if @Valid
73
+ locales = @Locales
74
+ locales = Data::Storage.RailsTranslated.keys if locales.empty?
75
+ locales.each do |locale|
76
+ next unless Data::Storage.RailsTranslated.has_key?(locale)
77
+ @Valid = Data::Storage.RailsTranslated[locale].has_key?(@Name)
78
+ return @Valid if @Valid
79
+ end
80
+ end
81
+ @Valid = false
82
+ end
83
+
84
+ # Rails zone data
85
+ # @return [RailsData] data
86
+ def getData
87
+ unless @Loaded
88
+ @Loaded = true
89
+ @Valid = false
90
+ @Valid = Data::Storage.RailsZones.has_key?(@Name) if (not @Locales) or (@Locales and @Locales.include?('en'))
91
+ if @Valid
92
+ @Data.processEntry(Data::Storage.RailsZones[@Name], @Name)
93
+ return @Data unless @All
94
+ end
95
+ locales = @Locales
96
+ locales = Data::Storage.RailsTranslated.keys if locales.empty?
97
+ locales.each do |locale|
98
+ next unless Data::Storage.RailsTranslated.has_key?(locale)
99
+ entry = Data::Storage.RailsTranslated[locale][@Name]
100
+ if entry
101
+ @Data.processEntry(entry, false)
102
+ @Valid = true
103
+ return @Data unless @All
104
+ end
105
+ end
106
+ end
107
+ @Data
108
+ end
109
+
110
+ # Get UTC offsets in seconds
111
+ # @return [Array<Fixnum>] list of timezone offsets in seconds
112
+ def getOffsets
113
+ if not @Offsets and not getTimezones.empty?
114
+ @Offsets = @Data.findOffsets(@ToTime, @FromTime).to_a
115
+ else
116
+ super
117
+ end
118
+ @Offsets
119
+ end
120
+
121
+ # Rails zone identifier
122
+ # @return [String] Rails zone identifier
123
+ def getZone
124
+ getData.RailsZone
125
+ end
126
+
127
+ # Check if given Rails zone name is a valid timezone
128
+ # @param name [String] Rails zone name
129
+ # @param locales [Array<String>] search zone name only for these locales
130
+ # @return [Boolean] whether Timezone is valid
131
+ # @see Locales
132
+ def self.isValid?(name, locales = nil)
133
+ self.new(name).set(locales).isValid?
134
+ end
135
+
136
+ # Get UTC offsets in seconds for given Rails zone name
137
+ # @param name [String] Rails zone name
138
+ # @param toTime [DateTime] look for offsets which came into effect before this date, exclusive
139
+ # @param fromTime [DateTime] look for offsets which came into effect at this date, inclusive
140
+ # @param locales [Array<String>] search zone name only for these locales
141
+ # @param all [Boolean] specify whether should search for all timezones or return as soon as found any
142
+ # @return [Array<Fixnum>] list of timezone offsets in seconds
143
+ # @see Locales
144
+ def self.getOffsets(name, toTime = nil, fromTime = nil, locales = nil, all = true)
145
+ self.new(name).setTime(toTime, fromTime).set(locales, all).getOffsets
146
+ end
147
+
148
+ # Get Timezone identifiers for given Rails zone name
149
+ # @param name [String] Rails zone name
150
+ # @param locales [Array<String>] search zone name only for these locales
151
+ # @param all [Boolean] specify whether should search for all timezones or return as soon as found any
152
+ # @return [Array<String>] list of timezone identifiers
153
+ # @see Locales
154
+ def self.getTimezones(name, locales = nil, all = true)
155
+ self.new(name).set(locales, all).getTimezones
156
+ end
157
+
158
+ # Get Metazone identifiers for given Rails zone name
159
+ # @param name [String] Rails zone name
160
+ # @param locales [Array<String>] search zone name only for these locales
161
+ # @param all [Boolean] specify whether should search for all timezones or return as soon as found any
162
+ # @return [Array<String>] list of metazone identifiers
163
+ # @see Locales
164
+ # @see Regions
165
+ def self.getMetazones(name, locales = nil, all = true)
166
+ self.new(name).set(locales, all).getMetazones
167
+ end
168
+
169
+ # Rails zone identifier
170
+ # @param name [String] Rails zone name
171
+ # @param locales [Array<String>] search zone name only for these locales
172
+ # @param all [Boolean] specify whether should search for all timezones or return as soon as found any
173
+ # @return [String] Timezone identifier
174
+ def self.getZone(name, locales = nil, all = true)
175
+ self.new(name).set(locales, all).getZone
176
+ end
177
+ end
178
+ end