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