tzinfo 1.2.11 → 2.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.yardopts +3 -0
  4. data/CHANGES.md +580 -394
  5. data/LICENSE +12 -12
  6. data/README.md +368 -114
  7. data/lib/tzinfo/annual_rules.rb +32 -12
  8. data/lib/tzinfo/country.rb +141 -129
  9. data/lib/tzinfo/country_timezone.rb +70 -112
  10. data/lib/tzinfo/data_source.rb +400 -144
  11. data/lib/tzinfo/data_sources/constant_offset_data_timezone_info.rb +56 -0
  12. data/lib/tzinfo/data_sources/country_info.rb +42 -0
  13. data/lib/tzinfo/data_sources/data_timezone_info.rb +91 -0
  14. data/lib/tzinfo/data_sources/linked_timezone_info.rb +33 -0
  15. data/lib/tzinfo/data_sources/posix_time_zone_parser.rb +177 -0
  16. data/lib/tzinfo/data_sources/ruby_data_source.rb +141 -0
  17. data/lib/tzinfo/data_sources/timezone_info.rb +47 -0
  18. data/lib/tzinfo/data_sources/transitions_data_timezone_info.rb +214 -0
  19. data/lib/tzinfo/data_sources/zoneinfo_data_source.rb +592 -0
  20. data/lib/tzinfo/data_sources/zoneinfo_reader.rb +482 -0
  21. data/lib/tzinfo/data_sources.rb +8 -0
  22. data/lib/tzinfo/data_timezone.rb +33 -47
  23. data/lib/tzinfo/datetime_with_offset.rb +153 -0
  24. data/lib/tzinfo/format1/country_definer.rb +17 -0
  25. data/lib/tzinfo/format1/country_index_definition.rb +64 -0
  26. data/lib/tzinfo/format1/timezone_definer.rb +64 -0
  27. data/lib/tzinfo/format1/timezone_definition.rb +39 -0
  28. data/lib/tzinfo/format1/timezone_index_definition.rb +77 -0
  29. data/lib/tzinfo/format1.rb +10 -0
  30. data/lib/tzinfo/format2/country_definer.rb +68 -0
  31. data/lib/tzinfo/format2/country_index_definer.rb +68 -0
  32. data/lib/tzinfo/format2/country_index_definition.rb +46 -0
  33. data/lib/tzinfo/format2/timezone_definer.rb +94 -0
  34. data/lib/tzinfo/format2/timezone_definition.rb +73 -0
  35. data/lib/tzinfo/format2/timezone_index_definer.rb +45 -0
  36. data/lib/tzinfo/format2/timezone_index_definition.rb +55 -0
  37. data/lib/tzinfo/format2.rb +10 -0
  38. data/lib/tzinfo/info_timezone.rb +26 -21
  39. data/lib/tzinfo/linked_timezone.rb +33 -52
  40. data/lib/tzinfo/offset_timezone_period.rb +42 -0
  41. data/lib/tzinfo/ruby_core_support.rb +25 -163
  42. data/lib/tzinfo/string_deduper.rb +118 -0
  43. data/lib/tzinfo/time_with_offset.rb +154 -0
  44. data/lib/tzinfo/timestamp.rb +552 -0
  45. data/lib/tzinfo/timestamp_with_offset.rb +85 -0
  46. data/lib/tzinfo/timezone.rb +989 -502
  47. data/lib/tzinfo/timezone_offset.rb +84 -74
  48. data/lib/tzinfo/timezone_period.rb +151 -217
  49. data/lib/tzinfo/timezone_proxy.rb +70 -79
  50. data/lib/tzinfo/timezone_transition.rb +77 -109
  51. data/lib/tzinfo/transition_rule.rb +207 -77
  52. data/lib/tzinfo/transitions_timezone_period.rb +63 -0
  53. data/lib/tzinfo/version.rb +7 -0
  54. data/lib/tzinfo/with_offset.rb +61 -0
  55. data/lib/tzinfo.rb +78 -40
  56. data.tar.gz.sig +0 -0
  57. metadata +48 -103
  58. metadata.gz.sig +1 -3
  59. data/Rakefile +0 -107
  60. data/lib/tzinfo/country_index_definition.rb +0 -31
  61. data/lib/tzinfo/country_info.rb +0 -42
  62. data/lib/tzinfo/data_timezone_info.rb +0 -55
  63. data/lib/tzinfo/linked_timezone_info.rb +0 -26
  64. data/lib/tzinfo/offset_rationals.rb +0 -77
  65. data/lib/tzinfo/posix_time_zone_parser.rb +0 -136
  66. data/lib/tzinfo/ruby_country_info.rb +0 -74
  67. data/lib/tzinfo/ruby_data_source.rb +0 -136
  68. data/lib/tzinfo/time_or_datetime.rb +0 -351
  69. data/lib/tzinfo/timezone_definition.rb +0 -36
  70. data/lib/tzinfo/timezone_index_definition.rb +0 -54
  71. data/lib/tzinfo/timezone_info.rb +0 -30
  72. data/lib/tzinfo/timezone_transition_definition.rb +0 -104
  73. data/lib/tzinfo/transition_data_timezone_info.rb +0 -274
  74. data/lib/tzinfo/zoneinfo_country_info.rb +0 -37
  75. data/lib/tzinfo/zoneinfo_data_source.rb +0 -504
  76. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -516
  77. data/test/assets/payload.rb +0 -1
  78. data/test/tc_annual_rules.rb +0 -95
  79. data/test/tc_country.rb +0 -240
  80. data/test/tc_country_index_definition.rb +0 -69
  81. data/test/tc_country_info.rb +0 -16
  82. data/test/tc_country_timezone.rb +0 -173
  83. data/test/tc_data_source.rb +0 -218
  84. data/test/tc_data_timezone.rb +0 -99
  85. data/test/tc_data_timezone_info.rb +0 -18
  86. data/test/tc_info_timezone.rb +0 -34
  87. data/test/tc_linked_timezone.rb +0 -155
  88. data/test/tc_linked_timezone_info.rb +0 -23
  89. data/test/tc_offset_rationals.rb +0 -23
  90. data/test/tc_posix_time_zone_parser.rb +0 -261
  91. data/test/tc_ruby_core_support.rb +0 -168
  92. data/test/tc_ruby_country_info.rb +0 -110
  93. data/test/tc_ruby_data_source.rb +0 -175
  94. data/test/tc_time_or_datetime.rb +0 -674
  95. data/test/tc_timezone.rb +0 -1361
  96. data/test/tc_timezone_definition.rb +0 -113
  97. data/test/tc_timezone_index_definition.rb +0 -73
  98. data/test/tc_timezone_info.rb +0 -11
  99. data/test/tc_timezone_london.rb +0 -143
  100. data/test/tc_timezone_melbourne.rb +0 -142
  101. data/test/tc_timezone_new_york.rb +0 -142
  102. data/test/tc_timezone_offset.rb +0 -126
  103. data/test/tc_timezone_period.rb +0 -555
  104. data/test/tc_timezone_proxy.rb +0 -136
  105. data/test/tc_timezone_transition.rb +0 -366
  106. data/test/tc_timezone_transition_definition.rb +0 -295
  107. data/test/tc_timezone_utc.rb +0 -27
  108. data/test/tc_transition_data_timezone_info.rb +0 -433
  109. data/test/tc_transition_rule.rb +0 -663
  110. data/test/tc_zoneinfo_country_info.rb +0 -78
  111. data/test/tc_zoneinfo_data_source.rb +0 -1226
  112. data/test/tc_zoneinfo_timezone_info.rb +0 -2149
  113. data/test/test_utils.rb +0 -214
  114. data/test/ts_all.rb +0 -7
  115. data/test/ts_all_ruby.rb +0 -5
  116. data/test/ts_all_zoneinfo.rb +0 -9
  117. data/test/tzinfo-data/tzinfo/data/definitions/America/Argentina/Buenos_Aires.rb +0 -89
  118. data/test/tzinfo-data/tzinfo/data/definitions/America/New_York.rb +0 -327
  119. data/test/tzinfo-data/tzinfo/data/definitions/Australia/Melbourne.rb +0 -230
  120. data/test/tzinfo-data/tzinfo/data/definitions/EST.rb +0 -19
  121. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__m__1.rb +0 -21
  122. data/test/tzinfo-data/tzinfo/data/definitions/Etc/GMT__p__1.rb +0 -21
  123. data/test/tzinfo-data/tzinfo/data/definitions/Etc/UTC.rb +0 -21
  124. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Amsterdam.rb +0 -273
  125. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Andorra.rb +0 -198
  126. data/test/tzinfo-data/tzinfo/data/definitions/Europe/London.rb +0 -333
  127. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Paris.rb +0 -277
  128. data/test/tzinfo-data/tzinfo/data/definitions/Europe/Prague.rb +0 -235
  129. data/test/tzinfo-data/tzinfo/data/definitions/UTC.rb +0 -16
  130. data/test/tzinfo-data/tzinfo/data/indexes/countries.rb +0 -940
  131. data/test/tzinfo-data/tzinfo/data/indexes/timezones.rb +0 -609
  132. data/test/tzinfo-data/tzinfo/data/version.rb +0 -20
  133. data/test/tzinfo-data/tzinfo/data.rb +0 -8
  134. data/test/zoneinfo/America/Argentina/Buenos_Aires +0 -0
  135. data/test/zoneinfo/America/New_York +0 -0
  136. data/test/zoneinfo/Australia/Melbourne +0 -0
  137. data/test/zoneinfo/EST +0 -0
  138. data/test/zoneinfo/Etc/UTC +0 -0
  139. data/test/zoneinfo/Europe/Amsterdam +0 -0
  140. data/test/zoneinfo/Europe/Andorra +0 -0
  141. data/test/zoneinfo/Europe/London +0 -0
  142. data/test/zoneinfo/Europe/Paris +0 -0
  143. data/test/zoneinfo/Europe/Prague +0 -0
  144. data/test/zoneinfo/Factory +0 -0
  145. data/test/zoneinfo/iso3166.tab +0 -274
  146. data/test/zoneinfo/leapseconds +0 -78
  147. data/test/zoneinfo/posix/Europe/London +0 -0
  148. data/test/zoneinfo/posixrules +0 -0
  149. data/test/zoneinfo/right/Europe/London +0 -0
  150. data/test/zoneinfo/zone.tab +0 -452
  151. data/test/zoneinfo/zone1970.tab +0 -384
  152. data/tzinfo.gemspec +0 -21
@@ -0,0 +1,592 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ module TZInfo
5
+ module DataSources
6
+ # An {InvalidZoneinfoDirectory} exception is raised if {ZoneinfoDataSource}
7
+ # is initialized with a specific zoneinfo path that is not a valid zoneinfo
8
+ # directory. A valid zoneinfo directory is one that contains time zone
9
+ # files, a country code index file named iso3166.tab and a time zone index
10
+ # file named zone1970.tab or zone.tab.
11
+ class InvalidZoneinfoDirectory < StandardError
12
+ end
13
+
14
+ # A {ZoneinfoDirectoryNotFound} exception is raised if no valid zoneinfo
15
+ # directory could be found when checking the paths listed in
16
+ # {ZoneinfoDataSource.search_path}. A valid zoneinfo directory is one that
17
+ # contains time zone files, a country code index file named iso3166.tab and
18
+ # a time zone index file named zone1970.tab or zone.tab.
19
+ class ZoneinfoDirectoryNotFound < StandardError
20
+ end
21
+
22
+ # A DataSource implementation that loads data from a 'zoneinfo' directory
23
+ # containing compiled "TZif" version 3 (or earlier) files in addition to
24
+ # iso3166.tab and zone1970.tab or zone.tab index files.
25
+ #
26
+ # To have TZInfo load the system zoneinfo files, call
27
+ # {TZInfo::DataSource.set} as follows:
28
+ #
29
+ # TZInfo::DataSource.set(:zoneinfo)
30
+ #
31
+ # To load zoneinfo files from a particular directory, pass the directory to
32
+ # {TZInfo::DataSource.set}:
33
+ #
34
+ # TZInfo::DataSource.set(:zoneinfo, directory)
35
+ #
36
+ # To load zoneinfo files from a particular directory, but load the
37
+ # iso3166.tab index file from a separate location, pass the directory and
38
+ # path to the iso3166.tab file to {TZInfo::DataSource.set}:
39
+ #
40
+ # TZInfo::DataSource.set(:zoneinfo, directory, iso3166_path)
41
+ #
42
+ # Please note that versions of the 'zic' tool (used to build zoneinfo files)
43
+ # that were released prior to February 2006 created zoneinfo files that used
44
+ # 32-bit integers for transition timestamps. Later versions of zic produce
45
+ # zoneinfo files that use 64-bit integers. If you have 32-bit zoneinfo files
46
+ # on your system, then any queries falling outside of the range 1901-12-13
47
+ # 20:45:52 to 2038-01-19 03:14:07 may be inaccurate.
48
+ #
49
+ # Most modern platforms include 64-bit zoneinfo files. However, Mac OS X (up
50
+ # to at least 10.8.4) still uses 32-bit zoneinfo files.
51
+ #
52
+ # To check whether your zoneinfo files contain 32-bit or 64-bit transition
53
+ # data, you can run the following code (substituting the identifier of the
54
+ # zone you want to test for `zone_identifier`):
55
+ #
56
+ # TZInfo::DataSource.set(:zoneinfo)
57
+ # dir = TZInfo::DataSource.get.zoneinfo_dir
58
+ # File.open(File.join(dir, zone_identifier), 'r') {|f| f.read(5) }
59
+ #
60
+ # If the last line returns `"TZif\\x00"`, then you have a 32-bit zoneinfo
61
+ # file. If it returns `"TZif2"` or `"TZif3"` then you have a 64-bit zoneinfo
62
+ # file.
63
+ #
64
+ # It is also worth noting that as of the 2017c release of the IANA Time Zone
65
+ # Database, 64-bit zoneinfo files only include future transitions up to
66
+ # 2038-01-19 03:14:07. Any queries falling after this time may be
67
+ # inaccurate.
68
+ class ZoneinfoDataSource < DataSource
69
+ # The default value of {ZoneinfoDataSource.search_path}.
70
+ DEFAULT_SEARCH_PATH = ['/usr/share/zoneinfo', '/usr/share/lib/zoneinfo', '/etc/zoneinfo'].freeze
71
+ private_constant :DEFAULT_SEARCH_PATH
72
+
73
+ # The default value of {ZoneinfoDataSource.alternate_iso3166_tab_search_path}.
74
+ DEFAULT_ALTERNATE_ISO3166_TAB_SEARCH_PATH = ['/usr/share/misc/iso3166.tab', '/usr/share/misc/iso3166'].freeze
75
+ private_constant :DEFAULT_ALTERNATE_ISO3166_TAB_SEARCH_PATH
76
+
77
+ # Files and directories in the top level zoneinfo directory that will be
78
+ # excluded from the list of available time zones:
79
+ #
80
+ # - +VERSION is included on Mac OS X.
81
+ # - leapseconds is a list of leap seconds.
82
+ # - localtime is the current local timezone (may be a link).
83
+ # - posix, posixrules and right are directories containing other
84
+ # versions of the zoneinfo files.
85
+ # - SECURITY is included in the Arch Linux tzdata package.
86
+ # - src is a directory containing the tzdata source included on Solaris.
87
+ # - timeconfig is a symlink included on Slackware.
88
+ EXCLUDED_FILENAMES = [
89
+ '+VERSION',
90
+ 'leapseconds',
91
+ 'localtime',
92
+ 'posix',
93
+ 'posixrules',
94
+ 'right',
95
+ 'SECURITY',
96
+ 'src',
97
+ 'timeconfig'
98
+ ].freeze
99
+ private_constant :EXCLUDED_FILENAMES
100
+
101
+ # Paths to be checked to find the system zoneinfo directory.
102
+ #
103
+ # @private
104
+ @@search_path = DEFAULT_SEARCH_PATH.dup
105
+
106
+ # Paths to possible alternate iso3166.tab files (used to locate the
107
+ # system-wide iso3166.tab files on FreeBSD and OpenBSD).
108
+ #
109
+ # @private
110
+ @@alternate_iso3166_tab_search_path = DEFAULT_ALTERNATE_ISO3166_TAB_SEARCH_PATH.dup
111
+
112
+ class << self
113
+ # An `Array` of directories that will be checked to find the system
114
+ # zoneinfo directory.
115
+ #
116
+ # Directories are checked in the order they appear in the `Array`.
117
+ #
118
+ # The default value is `['/usr/share/zoneinfo',
119
+ # '/usr/share/lib/zoneinfo', '/etc/zoneinfo']`.
120
+ #
121
+ # @return [Array<String>] an `Array` of directories to check in order to
122
+ # find the system zoneinfo directory.
123
+ def search_path
124
+ @@search_path
125
+ end
126
+
127
+ # Sets the directories to be checked when locating the system zoneinfo
128
+ # directory.
129
+ #
130
+ # Can be set to an `Array` of directories or a `String` containing
131
+ # directories separated with `File::PATH_SEPARATOR`.
132
+ #
133
+ # Directories are checked in the order they appear in the `Array` or
134
+ # `String`.
135
+ #
136
+ # Set to `nil` to revert to the default paths.
137
+ #
138
+ # @param search_path [Object] either `nil` or a list of directories to
139
+ # check as either an `Array` of `String` or a `File::PATH_SEPARATOR`
140
+ # separated `String`.
141
+ def search_path=(search_path)
142
+ @@search_path = process_search_path(search_path, DEFAULT_SEARCH_PATH)
143
+ end
144
+
145
+ # An `Array` of paths that will be checked to find an alternate
146
+ # iso3166.tab file if one was not included in the zoneinfo directory
147
+ # (for example, on FreeBSD and OpenBSD systems).
148
+ #
149
+ # Paths are checked in the order they appear in the `Array`.
150
+ #
151
+ # The default value is `['/usr/share/misc/iso3166.tab',
152
+ # '/usr/share/misc/iso3166']`.
153
+ #
154
+ # @return [Array<String>] an `Array` of paths to check in order to
155
+ # locate an iso3166.tab file.
156
+ def alternate_iso3166_tab_search_path
157
+ @@alternate_iso3166_tab_search_path
158
+ end
159
+
160
+ # Sets the paths to check to locate an alternate iso3166.tab file if one
161
+ # was not included in the zoneinfo directory.
162
+ #
163
+ # Can be set to an `Array` of paths or a `String` containing paths
164
+ # separated with `File::PATH_SEPARATOR`.
165
+ #
166
+ # Paths are checked in the order they appear in the array.
167
+ #
168
+ # Set to `nil` to revert to the default paths.
169
+ #
170
+ # @param alternate_iso3166_tab_search_path [Object] either `nil` or a
171
+ # list of paths to check as either an `Array` of `String` or a
172
+ # `File::PATH_SEPARATOR` separated `String`.
173
+ def alternate_iso3166_tab_search_path=(alternate_iso3166_tab_search_path)
174
+ @@alternate_iso3166_tab_search_path = process_search_path(alternate_iso3166_tab_search_path, DEFAULT_ALTERNATE_ISO3166_TAB_SEARCH_PATH)
175
+ end
176
+
177
+ private
178
+
179
+ # Processes a path for use as the {search_path} or
180
+ # {alternate_iso3166_tab_search_path}.
181
+ #
182
+ # @param path [Object] either `nil` or a list of paths to check as
183
+ # either an `Array` of `String` or a `File::PATH_SEPARATOR` separated
184
+ # `String`.
185
+ # @param default [Array<String>] the default value.
186
+ # @return [Array<String>] the processed path.
187
+ def process_search_path(path, default)
188
+ if path
189
+ if path.kind_of?(String)
190
+ path.split(File::PATH_SEPARATOR)
191
+ else
192
+ path.collect(&:to_s)
193
+ end
194
+ else
195
+ default.dup
196
+ end
197
+ end
198
+ end
199
+
200
+ # @return [String] the zoneinfo directory being used.
201
+ attr_reader :zoneinfo_dir
202
+
203
+ # (see DataSource#country_codes)
204
+ attr_reader :country_codes
205
+
206
+ # Initializes a new {ZoneinfoDataSource}.
207
+ #
208
+ # If `zoneinfo_dir` is specified, it will be checked and used as the
209
+ # source of zoneinfo files.
210
+ #
211
+ # The directory must contain a file named iso3166.tab and a file named
212
+ # either zone1970.tab or zone.tab. These may either be included in the
213
+ # root of the directory or in a 'tab' sub-directory and named country.tab
214
+ # and zone_sun.tab respectively (as is the case on Solaris).
215
+ #
216
+ # Additionally, the path to iso3166.tab can be overridden using the
217
+ # `alternate_iso3166_tab_path` parameter.
218
+ #
219
+ # If `zoneinfo_dir` is not specified or `nil`, the paths referenced in
220
+ # {search_path} are searched in order to find a valid zoneinfo directory
221
+ # (one that contains zone1970.tab or zone.tab and iso3166.tab files as
222
+ # above).
223
+ #
224
+ # The paths referenced in {alternate_iso3166_tab_search_path} are also
225
+ # searched to find an iso3166.tab file if one of the searched zoneinfo
226
+ # directories doesn't contain an iso3166.tab file.
227
+ #
228
+ # @param zoneinfo_dir [String] an optional path to a directory to use as
229
+ # the source of zoneinfo files.
230
+ # @param alternate_iso3166_tab_path [String] an optional path to the
231
+ # iso3166.tab file.
232
+ # @raise [InvalidZoneinfoDirectory] if the iso3166.tab and zone1970.tab or
233
+ # zone.tab files cannot be found using the `zoneinfo_dir` and
234
+ # `alternate_iso3166_tab_path` parameters.
235
+ # @raise [ZoneinfoDirectoryNotFound] if no valid directory can be found
236
+ # by searching.
237
+ def initialize(zoneinfo_dir = nil, alternate_iso3166_tab_path = nil)
238
+ super()
239
+
240
+ if zoneinfo_dir
241
+ iso3166_tab_path, zone_tab_path = validate_zoneinfo_dir(zoneinfo_dir, alternate_iso3166_tab_path)
242
+
243
+ unless iso3166_tab_path && zone_tab_path
244
+ raise InvalidZoneinfoDirectory, "#{zoneinfo_dir} is not a directory or doesn't contain a iso3166.tab file and a zone1970.tab or zone.tab file."
245
+ end
246
+
247
+ @zoneinfo_dir = zoneinfo_dir
248
+ else
249
+ @zoneinfo_dir, iso3166_tab_path, zone_tab_path = find_zoneinfo_dir
250
+
251
+ unless @zoneinfo_dir && iso3166_tab_path && zone_tab_path
252
+ raise ZoneinfoDirectoryNotFound, "None of the paths included in #{self.class.name}.search_path are valid zoneinfo directories."
253
+ end
254
+ end
255
+
256
+ @zoneinfo_dir = File.expand_path(@zoneinfo_dir).freeze
257
+ @timezone_identifiers = load_timezone_identifiers.freeze
258
+ @countries = load_countries(iso3166_tab_path, zone_tab_path).freeze
259
+ @country_codes = @countries.keys.sort!.freeze
260
+
261
+ string_deduper = ConcurrentStringDeduper.new
262
+ posix_tz_parser = PosixTimeZoneParser.new(string_deduper)
263
+ @zoneinfo_reader = ZoneinfoReader.new(posix_tz_parser, string_deduper)
264
+ end
265
+
266
+ # Returns a frozen `Array` of all the available time zone identifiers. The
267
+ # identifiers are sorted according to `String#<=>`.
268
+ #
269
+ # @return [Array<String>] a frozen `Array` of all the available time zone
270
+ # identifiers.
271
+ def data_timezone_identifiers
272
+ @timezone_identifiers
273
+ end
274
+
275
+ # Returns an empty `Array`. There is no information about linked/aliased
276
+ # time zones in the zoneinfo files. When using {ZoneinfoDataSource}, every
277
+ # time zone will be returned as a {DataTimezone}.
278
+ #
279
+ # @return [Array<String>] an empty `Array`.
280
+ def linked_timezone_identifiers
281
+ [].freeze
282
+ end
283
+
284
+ # (see DataSource#to_s)
285
+ def to_s
286
+ "Zoneinfo DataSource: #{@zoneinfo_dir}"
287
+ end
288
+
289
+ # (see DataSource#inspect)
290
+ def inspect
291
+ "#<#{self.class}: #{@zoneinfo_dir}>"
292
+ end
293
+
294
+ protected
295
+
296
+ # Returns a {TimezoneInfo} instance for the given time zone identifier.
297
+ # The result will either be a {ConstantOffsetDataTimezoneInfo} or a
298
+ # {TransitionsDataTimezoneInfo}.
299
+ #
300
+ # @param identifier [String] A time zone identifier.
301
+ # @return [TimezoneInfo] a {TimezoneInfo} instance for the given time zone
302
+ # identifier.
303
+ # @raise [InvalidTimezoneIdentifier] if the time zone is not found, the
304
+ # identifier is invalid, the zoneinfo file cannot be opened or the
305
+ # zoneinfo file is not valid.
306
+ def load_timezone_info(identifier)
307
+ valid_identifier = validate_timezone_identifier(identifier)
308
+ path = File.join(@zoneinfo_dir, valid_identifier)
309
+
310
+ zoneinfo = begin
311
+ @zoneinfo_reader.read(path)
312
+ rescue Errno::EACCES, InvalidZoneinfoFile => e
313
+ raise InvalidTimezoneIdentifier, "#{e.message.encode(Encoding::UTF_8)} (loading #{valid_identifier})"
314
+ rescue Errno::EISDIR, Errno::ENAMETOOLONG, Errno::ENOENT, Errno::ENOTDIR
315
+ raise InvalidTimezoneIdentifier, "Invalid identifier: #{valid_identifier}"
316
+ end
317
+
318
+ if zoneinfo.kind_of?(TimezoneOffset)
319
+ ConstantOffsetDataTimezoneInfo.new(valid_identifier, zoneinfo)
320
+ else
321
+ TransitionsDataTimezoneInfo.new(valid_identifier, zoneinfo)
322
+ end
323
+ end
324
+
325
+ # (see DataSource#load_country_info)
326
+ def load_country_info(code)
327
+ lookup_country_info(@countries, code)
328
+ end
329
+
330
+ private
331
+
332
+ # Validates a zoneinfo directory and returns the paths to the iso3166.tab
333
+ # and zone1970.tab or zone.tab files if valid. If the directory is not
334
+ # valid, returns `nil`.
335
+ #
336
+ # The path to the iso3166.tab file may be overridden by passing in a path.
337
+ # This is treated as either absolute or relative to the current working
338
+ # directory.
339
+ #
340
+ # @param path [String] the path to a possible zoneinfo directory.
341
+ # @param iso3166_tab_path [String] an optional path to an external
342
+ # iso3166.tab file.
343
+ # @return [Array<String>] an `Array` containing the iso3166.tab and
344
+ # zone.tab paths if the directory is valid, otherwise `nil`.
345
+ def validate_zoneinfo_dir(path, iso3166_tab_path = nil)
346
+ if File.directory?(path)
347
+ if iso3166_tab_path
348
+ return nil unless File.file?(iso3166_tab_path)
349
+ else
350
+ iso3166_tab_path = resolve_tab_path(path, ['iso3166.tab'], 'country.tab')
351
+ return nil unless iso3166_tab_path
352
+ end
353
+
354
+ zone_tab_path = resolve_tab_path(path, ['zone1970.tab', 'zone.tab'], 'zone_sun.tab')
355
+ return nil unless zone_tab_path
356
+
357
+ [iso3166_tab_path, zone_tab_path]
358
+ else
359
+ nil
360
+ end
361
+ end
362
+
363
+ # Attempts to resolve the path to a tab file given its standard names and
364
+ # tab sub-directory name (as used on Solaris).
365
+ #
366
+ # @param zoneinfo_path [String] the path to a zoneinfo directory.
367
+ # @param standard_names [Array<String>] the standard names for the tab
368
+ # file.
369
+ # @param tab_name [String] the alternate name for the tab file to check in
370
+ # the tab sub-directory.
371
+ # @return [String] the path to the tab file.
372
+ def resolve_tab_path(zoneinfo_path, standard_names, tab_name)
373
+ standard_names.each do |standard_name|
374
+ path = File.join(zoneinfo_path, standard_name)
375
+ return path if File.file?(path)
376
+ end
377
+
378
+ path = File.join(zoneinfo_path, 'tab', tab_name)
379
+ return path if File.file?(path)
380
+
381
+ nil
382
+ end
383
+
384
+ # Finds a zoneinfo directory using {search_path} and
385
+ # {alternate_iso3166_tab_search_path}.
386
+ #
387
+ # @return [Array<String>] an `Array` containing the iso3166.tab and
388
+ # zone.tab paths if a zoneinfo directory was found, otherwise `nil`.
389
+ def find_zoneinfo_dir
390
+ alternate_iso3166_tab_path = self.class.alternate_iso3166_tab_search_path.detect do |path|
391
+ File.file?(path)
392
+ end
393
+
394
+ self.class.search_path.each do |path|
395
+ # Try without the alternate_iso3166_tab_path first.
396
+ iso3166_tab_path, zone_tab_path = validate_zoneinfo_dir(path)
397
+ return path, iso3166_tab_path, zone_tab_path if iso3166_tab_path && zone_tab_path
398
+
399
+ if alternate_iso3166_tab_path
400
+ iso3166_tab_path, zone_tab_path = validate_zoneinfo_dir(path, alternate_iso3166_tab_path)
401
+ return path, iso3166_tab_path, zone_tab_path if iso3166_tab_path && zone_tab_path
402
+ end
403
+ end
404
+
405
+ # Not found.
406
+ nil
407
+ end
408
+
409
+ # Scans @zoneinfo_dir and returns an `Array` of available time zone
410
+ # identifiers. The result is sorted according to `String#<=>`.
411
+ #
412
+ # @return [Array<String>] an `Array` containing all the time zone
413
+ # identifiers found.
414
+ def load_timezone_identifiers
415
+ index = []
416
+
417
+ enum_timezones([], EXCLUDED_FILENAMES) do |identifier|
418
+ index << identifier.join('/').freeze
419
+ end
420
+
421
+ index.sort!
422
+ end
423
+
424
+ # Recursively enumerate a directory of time zones.
425
+ #
426
+ # @param dir [Array<String>] the directory to enumerate as an `Array` of
427
+ # path components.
428
+ # @param exclude [Array<String>] file names to exclude when scanning
429
+ # `dir`.
430
+ # @yield [path] the path of each time zone file found is passed to
431
+ # the block.
432
+ # @yieldparam path [Array<String>] the path of a time zone file as an
433
+ # `Array` of path components.
434
+ def enum_timezones(dir, exclude = [], &block)
435
+ Dir.foreach(File.join(@zoneinfo_dir, *dir)) do |entry|
436
+ begin
437
+ entry.encode!(Encoding::UTF_8)
438
+ rescue EncodingError
439
+ next
440
+ end
441
+
442
+ unless entry =~ /\./ || exclude.include?(entry)
443
+ RubyCoreSupport.untaint(entry)
444
+ path = dir + [entry]
445
+ full_path = File.join(@zoneinfo_dir, *path)
446
+
447
+ if File.directory?(full_path)
448
+ enum_timezones(path, [], &block)
449
+ elsif File.file?(full_path)
450
+ yield path
451
+ end
452
+ end
453
+ end
454
+ end
455
+
456
+ # Uses the iso3166.tab and zone1970.tab or zone.tab files to return a Hash
457
+ # mapping country codes to CountryInfo instances.
458
+ #
459
+ # @param iso3166_tab_path [String] the path to the iso3166.tab file.
460
+ # @param zone_tab_path [String] the path to the zone.tab file.
461
+ # @return [Hash<String, CountryInfo>] a mapping from ISO 3166-1 alpha-2
462
+ # country codes to {CountryInfo} instances.
463
+ def load_countries(iso3166_tab_path, zone_tab_path)
464
+
465
+ # Handle standard 3 to 4 column zone.tab files as well as the 4 to 5
466
+ # column format used by Solaris.
467
+ #
468
+ # On Solaris, an extra column before the comment gives an optional
469
+ # linked/alternate timezone identifier (or '-' if not set).
470
+ #
471
+ # Additionally, there is a section at the end of the file for timezones
472
+ # covering regions. These are given lower-case "country" codes. The timezone
473
+ # identifier column refers to a continent instead of an identifier. These
474
+ # lines will be ignored by TZInfo.
475
+ #
476
+ # Since the last column is optional in both formats, testing for the
477
+ # Solaris format is done in two passes. The first pass identifies if there
478
+ # are any lines using 5 columns.
479
+
480
+
481
+ # The first column is allowed to be a comma separated list of country
482
+ # codes, as used in zone1970.tab (introduced in tzdata 2014f).
483
+ #
484
+ # The first country code in the comma-separated list is the country that
485
+ # contains the city the zone identifier is based on. The first country
486
+ # code on each line is considered to be primary with the others
487
+ # secondary.
488
+ #
489
+ # The zones for each country are ordered primary first, then secondary.
490
+ # Within the primary and secondary groups, the zones are ordered by their
491
+ # order in the file.
492
+
493
+ file_is_5_column = false
494
+ zone_tab = []
495
+
496
+ file = File.read(zone_tab_path, external_encoding: Encoding::UTF_8, internal_encoding: Encoding::UTF_8)
497
+ file.each_line do |line|
498
+ line.chomp!
499
+
500
+ if line =~ /\A([A-Z]{2}(?:,[A-Z]{2})*)\t(?:([+\-])(\d{2})(\d{2})([+\-])(\d{3})(\d{2})|([+\-])(\d{2})(\d{2})(\d{2})([+\-])(\d{3})(\d{2})(\d{2}))\t([^\t]+)(?:\t([^\t]+))?(?:\t([^\t]+))?\z/
501
+ codes = $1
502
+
503
+ if $2
504
+ latitude = dms_to_rational($2, $3, $4)
505
+ longitude = dms_to_rational($5, $6, $7)
506
+ else
507
+ latitude = dms_to_rational($8, $9, $10, $11)
508
+ longitude = dms_to_rational($12, $13, $14, $15)
509
+ end
510
+
511
+ zone_identifier = $16
512
+ column4 = $17
513
+ column5 = $18
514
+
515
+ file_is_5_column = true if column5
516
+
517
+ zone_tab << [codes.split(','.freeze), zone_identifier, latitude, longitude, column4, column5]
518
+ end
519
+ end
520
+
521
+ string_deduper = StringDeduper.new
522
+ primary_zones = {}
523
+ secondary_zones = {}
524
+
525
+ zone_tab.each do |codes, zone_identifier, latitude, longitude, column4, column5|
526
+ description = file_is_5_column ? column5 : column4
527
+ description = string_deduper.dedupe(description) if description
528
+
529
+ # Lookup the identifier in the timezone index, so that the same
530
+ # String instance can be used (saving memory).
531
+ begin
532
+ zone_identifier = validate_timezone_identifier(zone_identifier)
533
+ rescue InvalidTimezoneIdentifier
534
+ # zone_identifier is not valid, dedupe and allow anyway.
535
+ zone_identifier = string_deduper.dedupe(zone_identifier)
536
+ end
537
+
538
+ country_timezone = CountryTimezone.new(zone_identifier, latitude, longitude, description)
539
+
540
+ # codes will always have at least one element
541
+
542
+ (primary_zones[codes.first.freeze] ||= []) << country_timezone
543
+
544
+ codes[1..-1].each do |code|
545
+ (secondary_zones[code.freeze] ||= []) << country_timezone
546
+ end
547
+ end
548
+
549
+ countries = {}
550
+
551
+ file = File.read(iso3166_tab_path, external_encoding: Encoding::UTF_8, internal_encoding: Encoding::UTF_8)
552
+ file.each_line do |line|
553
+ line.chomp!
554
+
555
+ # Handle both the two column alpha-2 and name format used in the tz
556
+ # database as well as the 4 column alpha-2, alpha-3, numeric-3 and
557
+ # name format used by FreeBSD and OpenBSD.
558
+
559
+ if line =~ /\A([A-Z]{2})(?:\t[A-Z]{3}\t[0-9]{3})?\t(.+)\z/
560
+ code = $1
561
+ name = $2
562
+ zones = (primary_zones[code] || []) + (secondary_zones[code] || [])
563
+
564
+ countries[code] = CountryInfo.new(code, name, zones)
565
+ end
566
+ end
567
+
568
+ countries
569
+ end
570
+
571
+ # Converts degrees, minutes and seconds to a Rational.
572
+ #
573
+ # @param sign [String] `'-'` or `'+'`.
574
+ # @param degrees [String] the number of degrees.
575
+ # @param minutes [String] the number of minutes.
576
+ # @param seconds [String] the number of seconds (optional).
577
+ # @return [Rational] the result of converting from degrees, minutes and
578
+ # seconds to a `Rational`.
579
+ def dms_to_rational(sign, degrees, minutes, seconds = nil)
580
+ degrees = degrees.to_i
581
+ minutes = minutes.to_i
582
+ sign = sign == '-'.freeze ? -1 : 1
583
+
584
+ if seconds
585
+ Rational(sign * (degrees * 3600 + minutes * 60 + seconds.to_i), 3600)
586
+ else
587
+ Rational(sign * (degrees * 60 + minutes), 60)
588
+ end
589
+ end
590
+ end
591
+ end
592
+ end