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
@@ -1,135 +1,93 @@
1
+ # encoding: UTF-8
2
+
1
3
  module TZInfo
2
- # A Timezone within a Country. This contains extra information about the
3
- # Timezone that is specific to the Country (a Timezone could be used by
4
- # multiple countries).
4
+ # Information about a time zone used by a {Country}.
5
5
  class CountryTimezone
6
- # The zone identifier.
7
- attr_reader :identifier
8
-
9
- # A description of this timezone in relation to the country, e.g.
10
- # "Eastern Time". This is usually nil for countries having only a single
11
- # Timezone.
6
+ # @return [String] the identifier of the {Timezone} being described.
7
+ attr_reader :identifier
8
+
9
+ # The latitude of this time zone in degrees. Positive numbers are degrees
10
+ # north and negative numbers are degrees south.
11
+ #
12
+ # Note that depending on the data source, the position given by {#latitude}
13
+ # and {#longitude} may not be within the country.
14
+ #
15
+ # @return [Rational] the latitude in degrees.
16
+ attr_reader :latitude
17
+
18
+ # The longitude of this time zone in degrees. Positive numbers are degrees
19
+ # east and negative numbers are degrees west.
20
+ #
21
+ # Note that depending on the data source, the position given by {#latitude}
22
+ # and {#longitude} may not be within the country.
23
+ #
24
+ # @return [Rational] the longitude in degrees.
25
+ attr_reader :longitude
26
+
27
+ # A description of this time zone in relation to the country, e.g. "Eastern
28
+ # Time". This is usually `nil` for countries that have a single time zone.
29
+ #
30
+ # @return [String] an optional description of the time zone.
12
31
  attr_reader :description
13
-
14
- class << self
15
- # Creates a new CountryTimezone with a timezone identifier, latitude,
16
- # longitude and description. The latitude and longitude are specified as
17
- # rationals - a numerator and denominator. For performance reasons, the
18
- # numerators and denominators must be specified in their lowest form.
19
- #
20
- # For use internally within TZInfo.
21
- #
22
- # @!visibility private
23
- alias :new! :new
24
-
25
- # Creates a new CountryTimezone with a timezone identifier, latitude,
26
- # longitude and description. The latitude and longitude must be specified
27
- # as instances of Rational.
28
- #
29
- # CountryTimezone instances should normally only be constructed when
30
- # creating new DataSource implementations.
31
- def new(identifier, latitude, longitude, description = nil)
32
- super(identifier, latitude, nil, longitude, nil, description)
33
- end
34
- end
35
-
36
- # Creates a new CountryTimezone with a timezone identifier, latitude,
37
- # longitude and description. The latitude and longitude are specified as
38
- # rationals - a numerator and denominator. For performance reasons, the
39
- # numerators and denominators must be specified in their lowest form.
32
+
33
+ # Creates a new {CountryTimezone}.
34
+ #
35
+ # The passed in identifier and description instances will be frozen.
36
+ #
37
+ # {CountryTimezone} instances should normally only be constructed
38
+ # by implementations of {DataSource}.
40
39
  #
41
- # @!visibility private
42
- def initialize(identifier, latitude_numerator, latitude_denominator,
43
- longitude_numerator, longitude_denominator, description = nil) #:nodoc:
44
- @identifier = identifier
45
-
46
- if latitude_numerator.kind_of?(Rational)
47
- @latitude = latitude_numerator
48
- else
49
- @latitude = nil
50
- @latitude_numerator = latitude_numerator
51
- @latitude_denominator = latitude_denominator
52
- end
53
-
54
- if longitude_numerator.kind_of?(Rational)
55
- @longitude = longitude_numerator
56
- else
57
- @longitude = nil
58
- @longitude_numerator = longitude_numerator
59
- @longitude_denominator = longitude_denominator
60
- end
61
-
62
- @description = description
40
+ # @param identifier [String] the {Timezone} identifier.
41
+ # @param latitude [Rational] the latitude of the time zone.
42
+ # @param longitude [Rational] the longitude of the time zone.
43
+ # @param description [String] an optional description of the time zone.
44
+ def initialize(identifier, latitude, longitude, description = nil)
45
+ @identifier = identifier.freeze
46
+ @latitude = latitude
47
+ @longitude = longitude
48
+ @description = description && description.freeze
63
49
  end
64
-
65
- # The Timezone (actually a TimezoneProxy for performance reasons).
50
+
51
+ # Returns the associated {Timezone}.
52
+ #
53
+ # The result is actually an instance of {TimezoneProxy} in order to defer
54
+ # loading of the time zone transition data until it is first needed.
55
+ #
56
+ # @return [Timezone] the associated {Timezone}.
66
57
  def timezone
67
58
  Timezone.get_proxy(@identifier)
68
59
  end
69
-
70
- # if description is not nil, this method returns description; otherwise it
71
- # returns timezone.friendly_identifier(true).
60
+
61
+ # @return [String] the {description} if present, otherwise a human-readable
62
+ # representation of the identifier (using {Timezone#friendly_identifier}).
72
63
  def description_or_friendly_identifier
73
64
  description || timezone.friendly_identifier(true)
74
65
  end
75
-
76
- # The latitude of this timezone in degrees as a Rational.
77
- def latitude
78
- # Thread-safety: It is possible that the value of @latitude may be
79
- # calculated multiple times in concurrently executing threads. It is not
80
- # worth the overhead of locking to ensure that @latitude is only
81
- # calculated once.
82
- unless @latitude
83
- result = RubyCoreSupport.rational_new!(@latitude_numerator, @latitude_denominator)
84
- return result if frozen?
85
- @latitude = result
86
- end
87
-
88
- @latitude
89
- end
90
-
91
- # The longitude of this timezone in degrees as a Rational.
92
- def longitude
93
- # Thread-safety: It is possible that the value of @longitude may be
94
- # calculated multiple times in concurrently executing threads. It is not
95
- # worth the overhead of locking to ensure that @longitude is only
96
- # calculated once.
97
- unless @longitude
98
- result = RubyCoreSupport.rational_new!(@longitude_numerator, @longitude_denominator)
99
- return result if frozen?
100
- @longitude = result
101
- end
102
66
 
103
- @longitude
104
- end
105
-
106
- # Returns true if and only if the given CountryTimezone is equal to the
107
- # current CountryTimezone (has the same identifer, latitude, longitude
108
- # and description).
67
+ # Tests if the given object is equal to the current instance (has the same
68
+ # identifier, latitude, longitude and description).
69
+ #
70
+ # @param ct [Object] the object to be compared.
71
+ # @return [TrueClass] `true` if `ct` is equal to the current instance.
109
72
  def ==(ct)
110
73
  ct.kind_of?(CountryTimezone) &&
111
74
  identifier == ct.identifier && latitude == ct.latitude &&
112
- longitude == ct.longitude && description == ct.description
75
+ longitude == ct.longitude && description == ct.description
113
76
  end
114
-
115
- # Returns true if and only if the given CountryTimezone is equal to the
116
- # current CountryTimezone (has the same identifer, latitude, longitude
117
- # and description).
77
+
78
+ # Tests if the given object is equal to the current instance (has the same
79
+ # identifier, latitude, longitude and description).
80
+ #
81
+ # @param ct [Object] the object to be compared.
82
+ # @return [Boolean] `true` if `ct` is equal to the current instance.
118
83
  def eql?(ct)
119
84
  self == ct
120
85
  end
121
-
122
- # Returns a hash of this CountryTimezone.
86
+
87
+ # @return [Integer] a hash based on the {identifier}, {latitude},
88
+ # {longitude} and {description}.
123
89
  def hash
124
- @identifier.hash ^
125
- (@latitude ? @latitude.numerator.hash ^ @latitude.denominator.hash : @latitude_numerator.hash ^ @latitude_denominator.hash) ^
126
- (@longitude ? @longitude.numerator.hash ^ @longitude.denominator.hash : @longitude_numerator.hash ^ @longitude_denominator.hash) ^
127
- @description.hash
128
- end
129
-
130
- # Returns internal object state as a programmer-readable string.
131
- def inspect
132
- "#<#{self.class}: #@identifier>"
90
+ [@identifier, @latitude, @longitude, @description].hash
133
91
  end
134
92
  end
135
93
  end
@@ -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