tzinfo 1.2.10 → 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 +583 -391
  5. data/LICENSE +13 -13
  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 +24 -155
  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 +50 -105
  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_country_info.rb +0 -74
  67. data/lib/tzinfo/ruby_data_source.rb +0 -140
  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 -512
  76. data/lib/tzinfo/zoneinfo_timezone_info.rb +0 -520
  77. data/test/assets/payload.rb +0 -1
  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 -173
  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 -1223
  112. data/test/tc_zoneinfo_timezone_info.rb +0 -2153
  113. data/test/test_utils.rb +0 -208
  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
@@ -1,190 +1,446 @@
1
+ # encoding: UTF-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'concurrent'
1
5
  require 'thread'
2
6
 
3
7
  module TZInfo
4
- # InvalidDataSource is raised if the DataSource is used doesn't implement one
5
- # of the required methods.
8
+ # {InvalidDataSource} is raised if the selected {DataSource} doesn't implement
9
+ # one of the required methods.
6
10
  class InvalidDataSource < StandardError
7
11
  end
8
-
9
- # DataSourceNotFound is raised if no data source could be found (i.e.
10
- # if 'tzinfo/data' cannot be found on the load path and no valid zoneinfo
12
+
13
+ # {DataSourceNotFound} is raised if no data source could be found (i.e. if
14
+ # `'tzinfo/data'` cannot be found on the load path and no valid zoneinfo
11
15
  # directory can be found on the system).
12
16
  class DataSourceNotFound < StandardError
13
17
  end
14
18
 
15
- # The base class for data sources of timezone and country data.
19
+ # TZInfo can be used with different data sources for time zone and country
20
+ # data. Each source of data is implemented as a subclass of {DataSource}.
16
21
  #
17
- # Use DataSource.set to change the data source being used.
22
+ # To choose a data source and override the default selection, use the
23
+ # {DataSource.set} method.
24
+ #
25
+ # @abstract To create a custom data source, create a subclass of {DataSource}
26
+ # and implement the {load_timezone_info}, {data_timezone_identifiers},
27
+ # {linked_timezone_identifiers}, {load_country_info} and {country_codes}
28
+ # methods.
18
29
  class DataSource
19
30
  # The currently selected data source.
31
+ #
32
+ # @private
20
33
  @@instance = nil
21
-
22
- # Mutex used to ensure the default data source is only created once.
34
+
35
+ # A `Mutex` used to ensure the default data source is only created once.
36
+ #
37
+ # @private
23
38
  @@default_mutex = Mutex.new
24
-
25
- # Returns the currently selected DataSource instance.
26
- def self.get
27
- # If a DataSource hasn't been manually set when the first request is
28
- # made to obtain a DataSource, then a Default data source is created.
29
-
30
- # This is done at the first request rather than when TZInfo is loaded to
31
- # avoid unnecessary (or in some cases potentially harmful) attempts to
32
- # find a suitable DataSource.
33
-
34
- # A Mutex is used to ensure that only a single default instance is
35
- # created (having two different DataSources in use simultaneously could
36
- # cause unexpected results).
37
-
38
- unless @@instance
39
- @@default_mutex.synchronize do
40
- set(create_default_data_source) unless @@instance
39
+
40
+ class << self
41
+ # @return [DataSource] the currently selected source of data.
42
+ def get
43
+ # If a DataSource hasn't been manually set when the first request is
44
+ # made to obtain a DataSource, then a default data source is created.
45
+ #
46
+ # This is done at the first request rather than when TZInfo is loaded to
47
+ # avoid unnecessary attempts to find a suitable DataSource.
48
+ #
49
+ # A `Mutex` is used to ensure that only a single default instance is
50
+ # created (this avoiding the possibility of retaining two copies of the
51
+ # same data in memory).
52
+
53
+ unless @@instance
54
+ @@default_mutex.synchronize do
55
+ set(create_default_data_source) unless @@instance
56
+ end
57
+ end
58
+
59
+ @@instance
60
+ end
61
+
62
+ # Sets the currently selected data source for time zone and country data.
63
+ #
64
+ # This should usually be set to one of the two standard data source types:
65
+ #
66
+ # * `:ruby` - read data from the Ruby modules included in the TZInfo::Data
67
+ # library (tzinfo-data gem).
68
+ # * `:zoneinfo` - read data from the zoneinfo files included with most
69
+ # Unix-like operating systems (e.g. in /usr/share/zoneinfo).
70
+ #
71
+ # To set TZInfo to use one of the standard data source types, call
72
+ # `TZInfo::DataSource.set`` in one of the following ways:
73
+ #
74
+ # TZInfo::DataSource.set(:ruby)
75
+ # TZInfo::DataSource.set(:zoneinfo)
76
+ # TZInfo::DataSource.set(:zoneinfo, zoneinfo_dir)
77
+ # TZInfo::DataSource.set(:zoneinfo, zoneinfo_dir, iso3166_tab_file)
78
+ #
79
+ # `DataSource.set(:zoneinfo)` will automatically search for the zoneinfo
80
+ # directory by checking the paths specified in
81
+ # {DataSources::ZoneinfoDataSource.search_path}.
82
+ # {DataSources::ZoneinfoDirectoryNotFound} will be raised if no valid
83
+ # zoneinfo directory could be found.
84
+ #
85
+ # `DataSource.set(:zoneinfo, zoneinfo_dir)` uses the specified
86
+ # `zoneinfo_dir` directory as the data source. If the directory is not a
87
+ # valid zoneinfo directory, a {DataSources::InvalidZoneinfoDirectory}
88
+ # exception will be raised.
89
+ #
90
+ # `DataSource.set(:zoneinfo, zoneinfo_dir, iso3166_tab_file)` uses the
91
+ # specified `zoneinfo_dir` directory as the data source, but loads the
92
+ # `iso3166.tab` file from the path given by `iso3166_tab_file`. If the
93
+ # directory is not a valid zoneinfo directory, a
94
+ # {DataSources::InvalidZoneinfoDirectory} exception will be raised.
95
+ #
96
+ # Custom data sources can be created by subclassing TZInfo::DataSource and
97
+ # implementing the following methods:
98
+ #
99
+ # * {load_timezone_info}
100
+ # * {data_timezone_identifiers}
101
+ # * {linked_timezone_identifiers}
102
+ # * {load_country_info}
103
+ # * {country_codes}
104
+ #
105
+ # To have TZInfo use the custom data source, call {DataSource.set},
106
+ # passing an instance of the custom data source implementation as follows:
107
+ #
108
+ # TZInfo::DataSource.set(CustomDataSource.new)
109
+ #
110
+ # Calling {DataSource.set} will only affect instances of {Timezone} and
111
+ # {Country} obtained with {Timezone.get} and {Country.get} subsequent to
112
+ # the {DataSource.set} call. Existing {Timezone} and {Country} instances
113
+ # will be unaffected.
114
+ #
115
+ # If {DataSource.set} is not called, TZInfo will by default attempt to use
116
+ # TZInfo::Data as the data source. If TZInfo::Data is not available (i.e.
117
+ # if `require 'tzinfo/data'` fails), then TZInfo will search for a
118
+ # zoneinfo directory instead (using the search path specified by
119
+ # {DataSources::ZoneinfoDataSource.search_path}).
120
+ #
121
+ # @param data_source_or_type [Object] either `:ruby`, `:zoneinfo` or an
122
+ # instance of a {DataSource}.
123
+ # @param args [Array<Object>] when `data_source_or_type` is a symbol,
124
+ # optional arguments to use when initializing the data source.
125
+ # @raise [ArgumentError] if `data_source_or_type` is not `:ruby`,
126
+ # `:zoneinfo` or an instance of {DataSource}.
127
+ def set(data_source_or_type, *args)
128
+ if data_source_or_type.kind_of?(DataSource)
129
+ @@instance = data_source_or_type
130
+ elsif data_source_or_type == :ruby
131
+ @@instance = DataSources::RubyDataSource.new
132
+ elsif data_source_or_type == :zoneinfo
133
+ @@instance = DataSources::ZoneinfoDataSource.new(*args)
134
+ else
135
+ raise ArgumentError, 'data_source_or_type must be a DataSource instance or a data source type (:ruby or :zoneinfo)'
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ # Creates a {DataSource} instance for use as the default. Used if no
142
+ # preference has been specified manually.
143
+ #
144
+ # @return [DataSource] the newly created default {DataSource} instance.
145
+ def create_default_data_source
146
+ has_tzinfo_data = false
147
+
148
+ begin
149
+ require 'tzinfo/data'
150
+ has_tzinfo_data = true
151
+ rescue LoadError
152
+ end
153
+
154
+ return DataSources::RubyDataSource.new if has_tzinfo_data
155
+
156
+ begin
157
+ return DataSources::ZoneinfoDataSource.new
158
+ rescue DataSources::ZoneinfoDirectoryNotFound
159
+ raise DataSourceNotFound, "No source of timezone data could be found.\nPlease refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error."
41
160
  end
42
- end
43
-
44
- @@instance
161
+ end
45
162
  end
46
-
47
- # Sets the currently selected data source for Timezone and Country data.
48
- #
49
- # This should usually be set to one of the two standard data source types:
50
- #
51
- # * +:ruby+ - read data from the Ruby modules included in the TZInfo::Data
52
- # library (tzinfo-data gem).
53
- # * +:zoneinfo+ - read data from the zoneinfo files included with most
54
- # Unix-like operating sytems (e.g. in /usr/share/zoneinfo).
55
- #
56
- # To set TZInfo to use one of the standard data source types, call
57
- # \TZInfo::DataSource.set in one of the following ways:
58
- #
59
- # TZInfo::DataSource.set(:ruby)
60
- # TZInfo::DataSource.set(:zoneinfo)
61
- # TZInfo::DataSource.set(:zoneinfo, zoneinfo_dir)
62
- # TZInfo::DataSource.set(:zoneinfo, zoneinfo_dir, iso3166_tab_file)
63
- #
64
- # \DataSource.set(:zoneinfo) will automatically search for the zoneinfo
65
- # directory by checking the paths specified in
66
- # ZoneinfoDataSource.search_paths. ZoneinfoDirectoryNotFound will be raised
67
- # if no valid zoneinfo directory could be found.
68
- #
69
- # \DataSource.set(:zoneinfo, zoneinfo_dir) uses the specified zoneinfo
70
- # directory as the data source. If the directory is not a valid zoneinfo
71
- # directory, an InvalidZoneinfoDirectory exception will be raised.
72
- #
73
- # \DataSource.set(:zoneinfo, zoneinfo_dir, iso3166_tab_file) uses the
74
- # specified zoneinfo directory as the data source, but loads the iso3166.tab
75
- # file from an alternate path. If the directory is not a valid zoneinfo
76
- # directory, an InvalidZoneinfoDirectory exception will be raised.
77
- #
78
- # Custom data sources can be created by subclassing TZInfo::DataSource and
79
- # implementing the following methods:
80
- #
81
- # * \load_timezone_info
82
- # * \timezone_identifiers
83
- # * \data_timezone_identifiers
84
- # * \linked_timezone_identifiers
85
- # * \load_country_info
86
- # * \country_codes
87
- #
88
- # To have TZInfo use the custom data source, call \DataSource.set
89
- # as follows:
90
- #
91
- # TZInfo::DataSource.set(CustomDataSource.new)
163
+
164
+ # Initializes a new {DataSource} instance. Typically only called via
165
+ # subclasses of {DataSource}.
166
+ def initialize
167
+ @timezones = Concurrent::Map.new
168
+ end
169
+
170
+ # Returns a {DataSources::TimezoneInfo} instance for the given identifier.
171
+ # The result will derive from either {DataSources::DataTimezoneInfo} for
172
+ # time zones that define their own data or {DataSources::LinkedTimezoneInfo}
173
+ # for links or aliases to other time zones.
92
174
  #
93
- # To avoid inconsistent data, \DataSource.set should be called before
94
- # accessing any Timezone or Country data.
175
+ # {get_timezone_info} calls {load_timezone_info} to create the
176
+ # {DataSources::TimezoneInfo} instance. The returned instance is cached and
177
+ # returned in subsequent calls to {get_timezone_info} for the identifier.
95
178
  #
96
- # If \DataSource.set is not called, TZInfo will by default use TZInfo::Data
97
- # as the data source. If TZInfo::Data is not available (i.e. if require
98
- # 'tzinfo/data' fails), then TZInfo will search for a zoneinfo directory
99
- # instead (using the search path specified by
100
- # TZInfo::ZoneinfoDataSource::DEFAULT_SEARCH_PATH).
101
- def self.set(data_source_or_type, *args)
102
- if data_source_or_type.kind_of?(DataSource)
103
- @@instance = data_source_or_type
104
- elsif data_source_or_type == :ruby
105
- @@instance = RubyDataSource.new
106
- elsif data_source_or_type == :zoneinfo
107
- @@instance = ZoneinfoDataSource.new(*args)
108
- else
109
- raise ArgumentError, 'data_source_or_type must be a DataSource instance or a data source type (:ruby)'
179
+ # @param identifier [String] A time zone identifier.
180
+ # @return [DataSources::TimezoneInfo] a {DataSources::TimezoneInfo} instance
181
+ # for a given identifier.
182
+ # @raise [InvalidTimezoneIdentifier] if the time zone is not found or the
183
+ # identifier is invalid.
184
+ def get_timezone_info(identifier)
185
+ result = @timezones[identifier]
186
+
187
+ unless result
188
+ # Thread-safety: It is possible that multiple equivalent TimezoneInfo
189
+ # instances could be created here in concurrently executing threads. The
190
+ # consequences of this are that the data may be loaded more than once
191
+ # (depending on the data source). The performance benefit of ensuring
192
+ # that only a single instance is created is unlikely to be worth the
193
+ # overhead of only allowing one TimezoneInfo to be loaded at a time.
194
+
195
+ result = load_timezone_info(identifier)
196
+ @timezones[result.identifier] = result
110
197
  end
198
+
199
+ result
111
200
  end
112
-
113
- # Returns a TimezoneInfo instance for a given identifier. The TimezoneInfo
114
- # instance should derive from either DataTimzoneInfo for timezones that
115
- # define their own data or LinkedTimezoneInfo for links or aliases to
116
- # other timezones.
117
- #
118
- # Raises InvalidTimezoneIdentifier if the timezone is not found or the
119
- # identifier is invalid.
120
- def load_timezone_info(identifier)
121
- raise_invalid_data_source('load_timezone_info')
122
- end
123
-
124
- # Returns an array of all the available timezone identifiers.
201
+
202
+ # @return [Array<String>] a frozen `Array`` of all the available time zone
203
+ # identifiers. The identifiers are sorted according to `String#<=>`.
125
204
  def timezone_identifiers
126
- raise_invalid_data_source('timezone_identifiers')
205
+ # Thread-safety: It is possible that the value of @timezone_identifiers
206
+ # may be calculated multiple times in concurrently executing threads. It
207
+ # is not worth the overhead of locking to ensure that
208
+ # @timezone_identifiers is only calculated once.
209
+ @timezone_identifiers ||= build_timezone_identifiers
127
210
  end
128
-
129
- # Returns an array of all the available timezone identifiers for
130
- # data timezones (i.e. those that actually contain definitions).
211
+
212
+ # Returns a frozen `Array` of all the available time zone identifiers for
213
+ # data time zones (i.e. those that actually contain definitions). The
214
+ # identifiers are sorted according to `String#<=>`.
215
+ #
216
+ # @return [Array<String>] a frozen `Array` of all the available time zone
217
+ # identifiers for data time zones.
131
218
  def data_timezone_identifiers
132
219
  raise_invalid_data_source('data_timezone_identifiers')
133
220
  end
134
-
135
- # Returns an array of all the available timezone identifiers that
136
- # are links to other timezones.
221
+
222
+ # Returns a frozen `Array` of all the available time zone identifiers that
223
+ # are links to other time zones. The identifiers are sorted according to
224
+ # `String#<=>`.
225
+ #
226
+ # @return [Array<String>] a frozen `Array` of all the available time zone
227
+ # identifiers that are links to other time zones.
137
228
  def linked_timezone_identifiers
138
229
  raise_invalid_data_source('linked_timezone_identifiers')
139
230
  end
140
-
141
- # Returns a CountryInfo instance for the given ISO 3166-1 alpha-2
142
- # country code. Raises InvalidCountryCode if the country could not be found
143
- # or the code is invalid.
144
- def load_country_info(code)
145
- raise_invalid_data_source('load_country_info')
231
+
232
+ # @param code [String] an ISO 3166-1 alpha-2 country code.
233
+ # @return [DataSources::CountryInfo] a {DataSources::CountryInfo} instance
234
+ # for the given ISO 3166-1 alpha-2 country code.
235
+ # @raise [InvalidCountryCode] if the country could not be found or the code
236
+ # is invalid.
237
+ def get_country_info(code)
238
+ load_country_info(code)
146
239
  end
147
-
148
- # Returns an array of all the available ISO 3166-1 alpha-2
149
- # country codes.
240
+
241
+ # Returns a frozen `Array` of all the available ISO 3166-1 alpha-2 country
242
+ # codes. The identifiers are sorted according to `String#<=>`.
243
+ #
244
+ # @return [Array<String>] a frozen `Array` of all the available ISO 3166-1
245
+ # alpha-2 country codes.
150
246
  def country_codes
151
247
  raise_invalid_data_source('country_codes')
152
248
  end
153
-
154
- # Returns the name of this DataSource.
249
+
250
+ # Loads all timezone and country data into memory.
251
+ #
252
+ # This may be desirable in production environments to improve copy-on-write
253
+ # performance and to avoid flushing the constant cache every time a new
254
+ # timezone or country is loaded from {DataSources::RubyDataSource}.
255
+ def eager_load!
256
+ timezone_identifiers.each {|identifier| load_timezone_info(identifier) }
257
+ country_codes.each {|code| load_country_info(code) }
258
+ nil
259
+ end
260
+
261
+ # @return [String] a description of the {DataSource}.
155
262
  def to_s
156
263
  "Default DataSource"
157
264
  end
158
-
159
- # Returns internal object state as a programmer-readable string.
265
+
266
+ # @return [String] the internal object state as a programmer-readable
267
+ # `String`.
160
268
  def inspect
161
269
  "#<#{self.class}>"
162
270
  end
163
-
271
+
272
+ protected
273
+
274
+ # Returns a {DataSources::TimezoneInfo} instance for the given time zone
275
+ # identifier. The result should derive from either
276
+ # {DataSources::DataTimezoneInfo} for time zones that define their own data
277
+ # or {DataSources::LinkedTimezoneInfo} for links to or aliases for other
278
+ # time zones.
279
+ #
280
+ # @param identifier [String] A time zone identifier.
281
+ # @return [DataSources::TimezoneInfo] a {DataSources::TimezoneInfo} instance
282
+ # for the given time zone identifier.
283
+ # @raise [InvalidTimezoneIdentifier] if the time zone is not found or the
284
+ # identifier is invalid.
285
+ def load_timezone_info(identifier)
286
+ raise_invalid_data_source('load_timezone_info')
287
+ end
288
+
289
+ # @param code [String] an ISO 3166-1 alpha-2 country code.
290
+ # @return [DataSources::CountryInfo] a {DataSources::CountryInfo} instance
291
+ # for the given ISO 3166-1 alpha-2 country code.
292
+ # @raise [InvalidCountryCode] if the country could not be found or the code
293
+ # is invalid.
294
+ def load_country_info(code)
295
+ raise_invalid_data_source('load_country_info')
296
+ end
297
+
298
+ # @return [Encoding] the `Encoding` used by the `String` instances returned
299
+ # by {data_timezone_identifiers} and {linked_timezone_identifiers}.
300
+ def timezone_identifier_encoding
301
+ Encoding::UTF_8
302
+ end
303
+
304
+ # Checks that the given identifier is a valid time zone identifier (can be
305
+ # found in the {timezone_identifiers} `Array`). If the identifier is valid,
306
+ # the `String` instance representing that identifier from
307
+ # `timezone_identifiers` is returned. Otherwise an
308
+ # {InvalidTimezoneIdentifier} exception is raised.
309
+ #
310
+ # @param identifier [String] a time zone identifier to be validated.
311
+ # @return [String] the `String` instance equivalent to `identifier` from
312
+ # {timezone_identifiers}.
313
+ # @raise [InvalidTimezoneIdentifier] if `identifier` was not found in
314
+ # {timezone_identifiers}.
315
+ def validate_timezone_identifier(identifier)
316
+ raise InvalidTimezoneIdentifier, "Invalid identifier: #{identifier.nil? ? 'nil' : identifier}" unless identifier.kind_of?(String)
317
+
318
+ valid_identifier = try_with_encoding(identifier, timezone_identifier_encoding) {|id| find_timezone_identifier(id) }
319
+ return valid_identifier if valid_identifier
320
+
321
+ raise InvalidTimezoneIdentifier, "Invalid identifier: #{identifier.encode(Encoding::UTF_8)}"
322
+ end
323
+
324
+ # Looks up a given code in the given hash of code to
325
+ # {DataSources::CountryInfo} mappings. If the code is found the
326
+ # {DataSources::CountryInfo} is returned. Otherwise an {InvalidCountryCode}
327
+ # exception is raised.
328
+ #
329
+ # @param hash [String, DataSources::CountryInfo] a mapping from ISO 3166-1
330
+ # alpha-2 country codes to {DataSources::CountryInfo} instances.
331
+ # @param code [String] a country code to lookup.
332
+ # @param encoding [Encoding] the encoding used for the country codes in
333
+ # `hash`.
334
+ # @return [DataSources::CountryInfo] the {DataSources::CountryInfo} instance
335
+ # corresponding to `code`.
336
+ # @raise [InvalidCountryCode] if `code` was not found in `hash`.
337
+ def lookup_country_info(hash, code, encoding = Encoding::UTF_8)
338
+ raise InvalidCountryCode, "Invalid country code: #{code.nil? ? 'nil' : code}" unless code.kind_of?(String)
339
+
340
+ info = try_with_encoding(code, encoding) {|c| hash[c] }
341
+ return info if info
342
+
343
+ raise InvalidCountryCode, "Invalid country code: #{code.encode(Encoding::UTF_8)}"
344
+ end
345
+
164
346
  private
165
-
166
- # Creates a DataSource instance for use as the default. Used if
167
- # no preference has been specified manually.
168
- def self.create_default_data_source
169
- has_tzinfo_data = false
170
-
171
- begin
172
- require 'tzinfo/data'
173
- has_tzinfo_data = true
174
- rescue LoadError
347
+
348
+ # Raises {InvalidDataSource} to indicate that a method has not been
349
+ # overridden by a particular data source implementation.
350
+ #
351
+ # @raise [InvalidDataSource] always.
352
+ def raise_invalid_data_source(method_name)
353
+ raise InvalidDataSource, "#{method_name} not defined"
354
+ end
355
+
356
+ # Combines {data_timezone_identifiers} and {linked_timezone_identifiers}
357
+ # to create an `Array` containing all valid time zone identifiers. If
358
+ # {linked_timezone_identifiers} is empty, the {data_timezone_identifiers}
359
+ # instance is returned.
360
+ #
361
+ # The returned `Array` is frozen. The identifiers are sorted according to
362
+ # `String#<=>`.
363
+ #
364
+ # @return [Array<String>] an `Array` containing all valid time zone
365
+ # identifiers.
366
+ def build_timezone_identifiers
367
+ data = data_timezone_identifiers
368
+ linked = linked_timezone_identifiers
369
+ linked.empty? ? data : (data + linked).sort!.freeze
370
+ end
371
+
372
+ if [].respond_to?(:bsearch)
373
+ # If the given `identifier` is contained within the {timezone_identifiers}
374
+ # `Array`, the `String` instance representing that identifier from
375
+ # {timezone_identifiers} is returned. Otherwise, `nil` is returned.
376
+ #
377
+ # @param identifier [String] A time zone identifier to search for.
378
+ # @return [String] the `String` instance representing `identifier` from
379
+ # {timezone_identifiers} if found, or `nil` if not found.
380
+ #
381
+ # :nocov_no_array_bsearch:
382
+ def find_timezone_identifier(identifier)
383
+
384
+ result = timezone_identifiers.bsearch {|i| i >= identifier }
385
+ result == identifier ? result : nil
175
386
  end
176
-
177
- return RubyDataSource.new if has_tzinfo_data
178
-
179
- begin
180
- return ZoneinfoDataSource.new
181
- rescue ZoneinfoDirectoryNotFound
182
- raise DataSourceNotFound, "No source of timezone data could be found.\nPlease refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error."
387
+ # :nocov_no_array_bsearch:
388
+ else
389
+ # If the given `identifier` is contained within the {timezone_identifiers}
390
+ # `Array`, the `String` instance representing that identifier from
391
+ # {timezone_identifiers} is returned. Otherwise, `nil` is returned.
392
+ #
393
+ # @param identifier [String] A time zone identifier to search for.
394
+ # @return [String] the `String` instance representing `identifier` from
395
+ # {timezone_identifiers} if found, or `nil` if not found.
396
+ #
397
+ # :nocov_array_bsearch:
398
+ def find_timezone_identifier(identifier)
399
+ identifiers = timezone_identifiers
400
+ low = 0
401
+ high = identifiers.length
402
+
403
+ while low < high do
404
+ mid = (low + high).div(2)
405
+ mid_identifier = identifiers[mid]
406
+ cmp = mid_identifier <=> identifier
407
+
408
+ return mid_identifier if cmp == 0
409
+
410
+ if cmp > 0
411
+ high = mid
412
+ else
413
+ low = mid + 1
414
+ end
415
+ end
416
+
417
+ nil
183
418
  end
419
+ # :nocov_array_bsearch:
184
420
  end
185
421
 
186
- def raise_invalid_data_source(method_name)
187
- raise InvalidDataSource, "#{method_name} not defined"
422
+ # Tries an operation using `string` directly. If the operation fails, the
423
+ # string is copied and encoded with `encoding` and the operation is tried
424
+ # again.
425
+ #
426
+ # @param string [String] The `String` to perform the operation on.
427
+ # @param encoding [Encoding] The `Encoding` to use if the initial attempt
428
+ # fails.
429
+ # @yield [s] the caller will be yielded to once or twice to attempt the
430
+ # operation.
431
+ # @yieldparam s [String] either `string` or an encoded copy of `string`.
432
+ # @yieldreturn [Object] The result of the operation. Must be truthy if
433
+ # successful.
434
+ # @return [Object] the result of the operation or `nil` if the first attempt
435
+ # fails and `string` is already encoded with `encoding`.
436
+ def try_with_encoding(string, encoding)
437
+ result = yield string
438
+ return result if result
439
+
440
+ unless encoding == string.encoding
441
+ string = string.encode(encoding)
442
+ yield string
443
+ end
188
444
  end
189
445
  end
190
446
  end
@@ -0,0 +1,56 @@
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 constantly observed offset.
7
+ class ConstantOffsetDataTimezoneInfo < DataTimezoneInfo
8
+ # @return [TimezoneOffset] the offset that is constantly observed.
9
+ attr_reader :constant_offset
10
+
11
+ # Initializes a new {ConstantOffsetDataTimezoneInfo}.
12
+ #
13
+ # The passed in `identifier` instance will be frozen. A reference to the
14
+ # passed in {TimezoneOffset} will be retained.
15
+ #
16
+ # @param identifier [String] the identifier of the time zone.
17
+ # @param constant_offset [TimezoneOffset] the constantly observed offset.
18
+ # @raise [ArgumentError] if `identifier` or `constant_offset` is `nil`.
19
+ def initialize(identifier, constant_offset)
20
+ super(identifier)
21
+ raise ArgumentError, 'constant_offset must be specified' unless constant_offset
22
+ @constant_offset = constant_offset
23
+ end
24
+
25
+ # @param timestamp [Timestamp] ignored.
26
+ # @return [TimezonePeriod] an unbounded {TimezonePeriod} for the time
27
+ # zone's constantly observed offset.
28
+ def period_for(timestamp)
29
+ constant_period
30
+ end
31
+
32
+ # @param local_timestamp [Timestamp] ignored.
33
+ # @return [Array<TimezonePeriod>] an `Array` containing a single unbounded
34
+ # {TimezonePeriod} for the time zone's constantly observed offset.
35
+ def periods_for_local(local_timestamp)
36
+ [constant_period]
37
+ end
38
+
39
+ # @param to_timestamp [Timestamp] ignored.
40
+ # @param from_timestamp [Timestamp] ignored.
41
+ # @return [Array] an empty `Array`, since there are no transitions in time
42
+ # zones that observe a constant offset.
43
+ def transitions_up_to(to_timestamp, from_timestamp = nil)
44
+ []
45
+ end
46
+
47
+ private
48
+
49
+ # @return [TimezonePeriod] an unbounded {TimezonePeriod} with the constant
50
+ # offset of this timezone.
51
+ def constant_period
52
+ OffsetTimezonePeriod.new(@constant_offset)
53
+ end
54
+ end
55
+ end
56
+ end