tzinfo 1.2.9 → 2.0.4

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