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
@@ -1,190 +1,435 @@
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)'
41
136
  end
42
- end
43
-
44
- @@instance
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."
160
+ end
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
+ # @return [String] a description of the {DataSource}.
155
251
  def to_s
156
252
  "Default DataSource"
157
253
  end
158
-
159
- # Returns internal object state as a programmer-readable string.
254
+
255
+ # @return [String] the internal object state as a programmer-readable
256
+ # `String`.
160
257
  def inspect
161
258
  "#<#{self.class}>"
162
259
  end
163
-
260
+
261
+ protected
262
+
263
+ # Returns a {DataSources::TimezoneInfo} instance for the given time zone
264
+ # identifier. The result should derive from either
265
+ # {DataSources::DataTimezoneInfo} for time zones that define their own data
266
+ # or {DataSources::LinkedTimezoneInfo} for links to or aliases for other
267
+ # time zones.
268
+ #
269
+ # @param identifier [String] A time zone identifier.
270
+ # @return [DataSources::TimezoneInfo] a {DataSources::TimezoneInfo} instance
271
+ # for the given time zone identifier.
272
+ # @raise [InvalidTimezoneIdentifier] if the time zone is not found or the
273
+ # identifier is invalid.
274
+ def load_timezone_info(identifier)
275
+ raise_invalid_data_source('load_timezone_info')
276
+ end
277
+
278
+ # @param code [String] an ISO 3166-1 alpha-2 country code.
279
+ # @return [DataSources::CountryInfo] a {DataSources::CountryInfo} instance
280
+ # for the given ISO 3166-1 alpha-2 country code.
281
+ # @raise [InvalidCountryCode] if the country could not be found or the code
282
+ # is invalid.
283
+ def load_country_info(code)
284
+ raise_invalid_data_source('load_country_info')
285
+ end
286
+
287
+ # @return [Encoding] the `Encoding` used by the `String` instances returned
288
+ # by {data_timezone_identifiers} and {linked_timezone_identifiers}.
289
+ def timezone_identifier_encoding
290
+ Encoding::UTF_8
291
+ end
292
+
293
+ # Checks that the given identifier is a valid time zone identifier (can be
294
+ # found in the {timezone_identifiers} `Array`). If the identifier is valid,
295
+ # the `String` instance representing that identifier from
296
+ # `timezone_identifiers` is returned. Otherwise an
297
+ # {InvalidTimezoneIdentifier} exception is raised.
298
+ #
299
+ # @param identifier [String] a time zone identifier to be validated.
300
+ # @return [String] the `String` instance equivalent to `identifier` from
301
+ # {timezone_identifiers}.
302
+ # @raise [InvalidTimezoneIdentifier] if `identifier` was not found in
303
+ # {timezone_identifiers}.
304
+ def validate_timezone_identifier(identifier)
305
+ raise InvalidTimezoneIdentifier, "Invalid identifier: #{identifier.nil? ? 'nil' : identifier}" unless identifier.kind_of?(String)
306
+
307
+ valid_identifier = try_with_encoding(identifier, timezone_identifier_encoding) {|id| find_timezone_identifier(id) }
308
+ return valid_identifier if valid_identifier
309
+
310
+ raise InvalidTimezoneIdentifier, "Invalid identifier: #{identifier.encode(Encoding::UTF_8)}"
311
+ end
312
+
313
+ # Looks up a given code in the given hash of code to
314
+ # {DataSources::CountryInfo} mappings. If the code is found the
315
+ # {DataSources::CountryInfo} is returned. Otherwise an {InvalidCountryCode}
316
+ # exception is raised.
317
+ #
318
+ # @param hash [String, DataSources::CountryInfo] a mapping from ISO 3166-1
319
+ # alpha-2 country codes to {DataSources::CountryInfo} instances.
320
+ # @param code [String] a country code to lookup.
321
+ # @param encoding [Encoding] the encoding used for the country codes in
322
+ # `hash`.
323
+ # @return [DataSources::CountryInfo] the {DataSources::CountryInfo} instance
324
+ # corresponding to `code`.
325
+ # @raise [InvalidCountryCode] if `code` was not found in `hash`.
326
+ def lookup_country_info(hash, code, encoding = Encoding::UTF_8)
327
+ raise InvalidCountryCode, "Invalid country code: #{code.nil? ? 'nil' : code}" unless code.kind_of?(String)
328
+
329
+ info = try_with_encoding(code, encoding) {|c| hash[c] }
330
+ return info if info
331
+
332
+ raise InvalidCountryCode, "Invalid country code: #{code.encode(Encoding::UTF_8)}"
333
+ end
334
+
164
335
  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
336
+
337
+ # Raises {InvalidDataSource} to indicate that a method has not been
338
+ # overridden by a particular data source implementation.
339
+ #
340
+ # @raise [InvalidDataSource] always.
341
+ def raise_invalid_data_source(method_name)
342
+ raise InvalidDataSource, "#{method_name} not defined"
343
+ end
344
+
345
+ # Combines {data_timezone_identifiers} and {linked_timezone_identifiers}
346
+ # to create an `Array` containing all valid time zone identifiers. If
347
+ # {linked_timezone_identifiers} is empty, the {data_timezone_identifiers}
348
+ # instance is returned.
349
+ #
350
+ # The returned `Array` is frozen. The identifiers are sorted according to
351
+ # `String#<=>`.
352
+ #
353
+ # @return [Array<String>] an `Array` containing all valid time zone
354
+ # identifiers.
355
+ def build_timezone_identifiers
356
+ data = data_timezone_identifiers
357
+ linked = linked_timezone_identifiers
358
+ linked.empty? ? data : (data + linked).sort!.freeze
359
+ end
360
+
361
+ if [].respond_to?(:bsearch)
362
+ # If the given `identifier` is contained within the {timezone_identifiers}
363
+ # `Array`, the `String` instance representing that identifier from
364
+ # {timezone_identifiers} is returned. Otherwise, `nil` is returned.
365
+ #
366
+ # @param identifier [String] A time zone identifier to search for.
367
+ # @return [String] the `String` instance representing `identifier` from
368
+ # {timezone_identifiers} if found, or `nil` if not found.
369
+ #
370
+ # :nocov_no_array_bsearch:
371
+ def find_timezone_identifier(identifier)
372
+
373
+ result = timezone_identifiers.bsearch {|i| i >= identifier }
374
+ result == identifier ? result : nil
175
375
  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."
376
+ # :nocov_no_array_bsearch:
377
+ else
378
+ # If the given `identifier` is contained within the {timezone_identifiers}
379
+ # `Array`, the `String` instance representing that identifier from
380
+ # {timezone_identifiers} is returned. Otherwise, `nil` is returned.
381
+ #
382
+ # @param identifier [String] A time zone identifier to search for.
383
+ # @return [String] the `String` instance representing `identifier` from
384
+ # {timezone_identifiers} if found, or `nil` if not found.
385
+ #
386
+ # :nocov_array_bsearch:
387
+ def find_timezone_identifier(identifier)
388
+ identifiers = timezone_identifiers
389
+ low = 0
390
+ high = identifiers.length
391
+
392
+ while low < high do
393
+ mid = (low + high).div(2)
394
+ mid_identifier = identifiers[mid]
395
+ cmp = mid_identifier <=> identifier
396
+
397
+ return mid_identifier if cmp == 0
398
+
399
+ if cmp > 0
400
+ high = mid
401
+ else
402
+ low = mid + 1
403
+ end
404
+ end
405
+
406
+ nil
183
407
  end
408
+ # :nocov_array_bsearch:
184
409
  end
185
410
 
186
- def raise_invalid_data_source(method_name)
187
- raise InvalidDataSource, "#{method_name} not defined"
411
+ # Tries an operation using `string` directly. If the operation fails, the
412
+ # string is copied and encoded with `encoding` and the operation is tried
413
+ # again.
414
+ #
415
+ # @param string [String] The `String` to perform the operation on.
416
+ # @param encoding [Encoding] The `Encoding` to use if the initial attempt
417
+ # fails.
418
+ # @yield [s] the caller will be yielded to once or twice to attempt the
419
+ # operation.
420
+ # @yieldparam s [String] either `string` or an encoded copy of `string`.
421
+ # @yieldreturn [Object] The result of the operation. Must be truthy if
422
+ # successful.
423
+ # @return [Object] the result of the operation or `nil` if the first attempt
424
+ # fails and `string` is already encoded with `encoding`.
425
+ def try_with_encoding(string, encoding)
426
+ result = yield string
427
+ return result if result
428
+
429
+ unless encoding == string.encoding
430
+ string = string.encode(encoding)
431
+ yield string
432
+ end
188
433
  end
189
434
  end
190
435
  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