File: //usr/local/rvm/gems/ruby-2.4.10/gems/tzinfo-2.0.4/lib/tzinfo/data_source.rb
# encoding: UTF-8
# frozen_string_literal: true
require 'concurrent'
require 'thread'
module TZInfo
# {InvalidDataSource} is raised if the selected {DataSource} doesn't implement
# one of the required methods.
class InvalidDataSource < StandardError
end
# {DataSourceNotFound} is raised if no data source could be found (i.e. if
# `'tzinfo/data'` cannot be found on the load path and no valid zoneinfo
# directory can be found on the system).
class DataSourceNotFound < StandardError
end
# TZInfo can be used with different data sources for time zone and country
# data. Each source of data is implemented as a subclass of {DataSource}.
#
# To choose a data source and override the default selection, use the
# {DataSource.set} method.
#
# @abstract To create a custom data source, create a subclass of {DataSource}
# and implement the {load_timezone_info}, {data_timezone_identifiers},
# {linked_timezone_identifiers}, {load_country_info} and {country_codes}
# methods.
class DataSource
# The currently selected data source.
#
# @private
@@instance = nil
# A `Mutex` used to ensure the default data source is only created once.
#
# @private
@@default_mutex = Mutex.new
class << self
# @return [DataSource] the currently selected source of data.
def get
# If a DataSource hasn't been manually set when the first request is
# made to obtain a DataSource, then a default data source is created.
#
# This is done at the first request rather than when TZInfo is loaded to
# avoid unnecessary attempts to find a suitable DataSource.
#
# A `Mutex` is used to ensure that only a single default instance is
# created (this avoiding the possibility of retaining two copies of the
# same data in memory).
unless @@instance
@@default_mutex.synchronize do
set(create_default_data_source) unless @@instance
end
end
@@instance
end
# Sets the currently selected data source for time zone and country data.
#
# This should usually be set to one of the two standard data source types:
#
# * `:ruby` - read data from the Ruby modules included in the TZInfo::Data
# library (tzinfo-data gem).
# * `:zoneinfo` - read data from the zoneinfo files included with most
# Unix-like operating systems (e.g. in /usr/share/zoneinfo).
#
# To set TZInfo to use one of the standard data source types, call
# `TZInfo::DataSource.set`` in one of the following ways:
#
# TZInfo::DataSource.set(:ruby)
# TZInfo::DataSource.set(:zoneinfo)
# TZInfo::DataSource.set(:zoneinfo, zoneinfo_dir)
# TZInfo::DataSource.set(:zoneinfo, zoneinfo_dir, iso3166_tab_file)
#
# `DataSource.set(:zoneinfo)` will automatically search for the zoneinfo
# directory by checking the paths specified in
# {DataSources::ZoneinfoDataSource.search_path}.
# {DataSources::ZoneinfoDirectoryNotFound} will be raised if no valid
# zoneinfo directory could be found.
#
# `DataSource.set(:zoneinfo, zoneinfo_dir)` uses the specified
# `zoneinfo_dir` directory as the data source. If the directory is not a
# valid zoneinfo directory, a {DataSources::InvalidZoneinfoDirectory}
# exception will be raised.
#
# `DataSource.set(:zoneinfo, zoneinfo_dir, iso3166_tab_file)` uses the
# specified `zoneinfo_dir` directory as the data source, but loads the
# `iso3166.tab` file from the path given by `iso3166_tab_file`. If the
# directory is not a valid zoneinfo directory, a
# {DataSources::InvalidZoneinfoDirectory} exception will be raised.
#
# Custom data sources can be created by subclassing TZInfo::DataSource and
# implementing the following methods:
#
# * {load_timezone_info}
# * {data_timezone_identifiers}
# * {linked_timezone_identifiers}
# * {load_country_info}
# * {country_codes}
#
# To have TZInfo use the custom data source, call {DataSource.set},
# passing an instance of the custom data source implementation as follows:
#
# TZInfo::DataSource.set(CustomDataSource.new)
#
# Calling {DataSource.set} will only affect instances of {Timezone} and
# {Country} obtained with {Timezone.get} and {Country.get} subsequent to
# the {DataSource.set} call. Existing {Timezone} and {Country} instances
# will be unaffected.
#
# If {DataSource.set} is not called, TZInfo will by default attempt to use
# TZInfo::Data as the data source. If TZInfo::Data is not available (i.e.
# if `require 'tzinfo/data'` fails), then TZInfo will search for a
# zoneinfo directory instead (using the search path specified by
# {DataSources::ZoneinfoDataSource.search_path}).
#
# @param data_source_or_type [Object] either `:ruby`, `:zoneinfo` or an
# instance of a {DataSource}.
# @param args [Array<Object>] when `data_source_or_type` is a symbol,
# optional arguments to use when initializing the data source.
# @raise [ArgumentError] if `data_source_or_type` is not `:ruby`,
# `:zoneinfo` or an instance of {DataSource}.
def set(data_source_or_type, *args)
if data_source_or_type.kind_of?(DataSource)
@@instance = data_source_or_type
elsif data_source_or_type == :ruby
@@instance = DataSources::RubyDataSource.new
elsif data_source_or_type == :zoneinfo
@@instance = DataSources::ZoneinfoDataSource.new(*args)
else
raise ArgumentError, 'data_source_or_type must be a DataSource instance or a data source type (:ruby or :zoneinfo)'
end
end
private
# Creates a {DataSource} instance for use as the default. Used if no
# preference has been specified manually.
#
# @return [DataSource] the newly created default {DataSource} instance.
def create_default_data_source
has_tzinfo_data = false
begin
require 'tzinfo/data'
has_tzinfo_data = true
rescue LoadError
end
return DataSources::RubyDataSource.new if has_tzinfo_data
begin
return DataSources::ZoneinfoDataSource.new
rescue DataSources::ZoneinfoDirectoryNotFound
raise DataSourceNotFound, "No source of timezone data could be found.\nPlease refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error."
end
end
end
# Initializes a new {DataSource} instance. Typically only called via
# subclasses of {DataSource}.
def initialize
@timezones = Concurrent::Map.new
end
# Returns a {DataSources::TimezoneInfo} instance for the given identifier.
# The result will derive from either {DataSources::DataTimezoneInfo} for
# time zones that define their own data or {DataSources::LinkedTimezoneInfo}
# for links or aliases to other time zones.
#
# {get_timezone_info} calls {load_timezone_info} to create the
# {DataSources::TimezoneInfo} instance. The returned instance is cached and
# returned in subsequent calls to {get_timezone_info} for the identifier.
#
# @param identifier [String] A time zone identifier.
# @return [DataSources::TimezoneInfo] a {DataSources::TimezoneInfo} instance
# for a given identifier.
# @raise [InvalidTimezoneIdentifier] if the time zone is not found or the
# identifier is invalid.
def get_timezone_info(identifier)
result = @timezones[identifier]
unless result
# Thread-safety: It is possible that multiple equivalent TimezoneInfo
# instances could be created here in concurrently executing threads. The
# consequences of this are that the data may be loaded more than once
# (depending on the data source). The performance benefit of ensuring
# that only a single instance is created is unlikely to be worth the
# overhead of only allowing one TimezoneInfo to be loaded at a time.
result = load_timezone_info(identifier)
@timezones[result.identifier] = result
end
result
end
# @return [Array<String>] a frozen `Array`` of all the available time zone
# identifiers. The identifiers are sorted according to `String#<=>`.
def timezone_identifiers
# Thread-safety: It is possible that the value of @timezone_identifiers
# may be calculated multiple times in concurrently executing threads. It
# is not worth the overhead of locking to ensure that
# @timezone_identifiers is only calculated once.
@timezone_identifiers ||= build_timezone_identifiers
end
# Returns a frozen `Array` of all the available time zone identifiers for
# data time zones (i.e. those that actually contain definitions). The
# identifiers are sorted according to `String#<=>`.
#
# @return [Array<String>] a frozen `Array` of all the available time zone
# identifiers for data time zones.
def data_timezone_identifiers
raise_invalid_data_source('data_timezone_identifiers')
end
# Returns a frozen `Array` of all the available time zone identifiers that
# are links to other time zones. The identifiers are sorted according to
# `String#<=>`.
#
# @return [Array<String>] a frozen `Array` of all the available time zone
# identifiers that are links to other time zones.
def linked_timezone_identifiers
raise_invalid_data_source('linked_timezone_identifiers')
end
# @param code [String] an ISO 3166-1 alpha-2 country code.
# @return [DataSources::CountryInfo] a {DataSources::CountryInfo} instance
# for the given ISO 3166-1 alpha-2 country code.
# @raise [InvalidCountryCode] if the country could not be found or the code
# is invalid.
def get_country_info(code)
load_country_info(code)
end
# Returns a frozen `Array` of all the available ISO 3166-1 alpha-2 country
# codes. The identifiers are sorted according to `String#<=>`.
#
# @return [Array<String>] a frozen `Array` of all the available ISO 3166-1
# alpha-2 country codes.
def country_codes
raise_invalid_data_source('country_codes')
end
# @return [String] a description of the {DataSource}.
def to_s
"Default DataSource"
end
# @return [String] the internal object state as a programmer-readable
# `String`.
def inspect
"#<#{self.class}>"
end
protected
# Returns a {DataSources::TimezoneInfo} instance for the given time zone
# identifier. The result should derive from either
# {DataSources::DataTimezoneInfo} for time zones that define their own data
# or {DataSources::LinkedTimezoneInfo} for links to or aliases for other
# time zones.
#
# @param identifier [String] A time zone identifier.
# @return [DataSources::TimezoneInfo] a {DataSources::TimezoneInfo} instance
# for the given time zone identifier.
# @raise [InvalidTimezoneIdentifier] if the time zone is not found or the
# identifier is invalid.
def load_timezone_info(identifier)
raise_invalid_data_source('load_timezone_info')
end
# @param code [String] an ISO 3166-1 alpha-2 country code.
# @return [DataSources::CountryInfo] a {DataSources::CountryInfo} instance
# for the given ISO 3166-1 alpha-2 country code.
# @raise [InvalidCountryCode] if the country could not be found or the code
# is invalid.
def load_country_info(code)
raise_invalid_data_source('load_country_info')
end
# @return [Encoding] the `Encoding` used by the `String` instances returned
# by {data_timezone_identifiers} and {linked_timezone_identifiers}.
def timezone_identifier_encoding
Encoding::UTF_8
end
# Checks that the given identifier is a valid time zone identifier (can be
# found in the {timezone_identifiers} `Array`). If the identifier is valid,
# the `String` instance representing that identifier from
# `timezone_identifiers` is returned. Otherwise an
# {InvalidTimezoneIdentifier} exception is raised.
#
# @param identifier [String] a time zone identifier to be validated.
# @return [String] the `String` instance equivalent to `identifier` from
# {timezone_identifiers}.
# @raise [InvalidTimezoneIdentifier] if `identifier` was not found in
# {timezone_identifiers}.
def validate_timezone_identifier(identifier)
raise InvalidTimezoneIdentifier, "Invalid identifier: #{identifier.nil? ? 'nil' : identifier}" unless identifier.kind_of?(String)
valid_identifier = try_with_encoding(identifier, timezone_identifier_encoding) {|id| find_timezone_identifier(id) }
return valid_identifier if valid_identifier
raise InvalidTimezoneIdentifier, "Invalid identifier: #{identifier.encode(Encoding::UTF_8)}"
end
# Looks up a given code in the given hash of code to
# {DataSources::CountryInfo} mappings. If the code is found the
# {DataSources::CountryInfo} is returned. Otherwise an {InvalidCountryCode}
# exception is raised.
#
# @param hash [String, DataSources::CountryInfo] a mapping from ISO 3166-1
# alpha-2 country codes to {DataSources::CountryInfo} instances.
# @param code [String] a country code to lookup.
# @param encoding [Encoding] the encoding used for the country codes in
# `hash`.
# @return [DataSources::CountryInfo] the {DataSources::CountryInfo} instance
# corresponding to `code`.
# @raise [InvalidCountryCode] if `code` was not found in `hash`.
def lookup_country_info(hash, code, encoding = Encoding::UTF_8)
raise InvalidCountryCode, "Invalid country code: #{code.nil? ? 'nil' : code}" unless code.kind_of?(String)
info = try_with_encoding(code, encoding) {|c| hash[c] }
return info if info
raise InvalidCountryCode, "Invalid country code: #{code.encode(Encoding::UTF_8)}"
end
private
# Raises {InvalidDataSource} to indicate that a method has not been
# overridden by a particular data source implementation.
#
# @raise [InvalidDataSource] always.
def raise_invalid_data_source(method_name)
raise InvalidDataSource, "#{method_name} not defined"
end
# Combines {data_timezone_identifiers} and {linked_timezone_identifiers}
# to create an `Array` containing all valid time zone identifiers. If
# {linked_timezone_identifiers} is empty, the {data_timezone_identifiers}
# instance is returned.
#
# The returned `Array` is frozen. The identifiers are sorted according to
# `String#<=>`.
#
# @return [Array<String>] an `Array` containing all valid time zone
# identifiers.
def build_timezone_identifiers
data = data_timezone_identifiers
linked = linked_timezone_identifiers
linked.empty? ? data : (data + linked).sort!.freeze
end
if [].respond_to?(:bsearch)
# If the given `identifier` is contained within the {timezone_identifiers}
# `Array`, the `String` instance representing that identifier from
# {timezone_identifiers} is returned. Otherwise, `nil` is returned.
#
# @param identifier [String] A time zone identifier to search for.
# @return [String] the `String` instance representing `identifier` from
# {timezone_identifiers} if found, or `nil` if not found.
#
# :nocov_no_array_bsearch:
def find_timezone_identifier(identifier)
result = timezone_identifiers.bsearch {|i| i >= identifier }
result == identifier ? result : nil
end
# :nocov_no_array_bsearch:
else
# If the given `identifier` is contained within the {timezone_identifiers}
# `Array`, the `String` instance representing that identifier from
# {timezone_identifiers} is returned. Otherwise, `nil` is returned.
#
# @param identifier [String] A time zone identifier to search for.
# @return [String] the `String` instance representing `identifier` from
# {timezone_identifiers} if found, or `nil` if not found.
#
# :nocov_array_bsearch:
def find_timezone_identifier(identifier)
identifiers = timezone_identifiers
low = 0
high = identifiers.length
while low < high do
mid = (low + high).div(2)
mid_identifier = identifiers[mid]
cmp = mid_identifier <=> identifier
return mid_identifier if cmp == 0
if cmp > 0
high = mid
else
low = mid + 1
end
end
nil
end
# :nocov_array_bsearch:
end
# Tries an operation using `string` directly. If the operation fails, the
# string is copied and encoded with `encoding` and the operation is tried
# again.
#
# @param string [String] The `String` to perform the operation on.
# @param encoding [Encoding] The `Encoding` to use if the initial attempt
# fails.
# @yield [s] the caller will be yielded to once or twice to attempt the
# operation.
# @yieldparam s [String] either `string` or an encoded copy of `string`.
# @yieldreturn [Object] The result of the operation. Must be truthy if
# successful.
# @return [Object] the result of the operation or `nil` if the first attempt
# fails and `string` is already encoded with `encoding`.
def try_with_encoding(string, encoding)
result = yield string
return result if result
unless encoding == string.encoding
string = string.encode(encoding)
yield string
end
end
end
end