# encoding: UTF-8# frozen_string_literal: truerequire'concurrent'require'thread'moduleTZInfo# {InvalidDataSource} is raised if the selected {DataSource} doesn't implement# one of the required methods.classInvalidDataSource<StandardErrorend# {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).classDataSourceNotFound<StandardErrorend# 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.classDataSource# 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.newclass<<self# @return [DataSource] the currently selected source of data.defget# 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.synchronizedoset(create_default_data_source)unless@@instanceendend@@instanceend# 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}.defset(data_source_or_type,*args)ifdata_source_or_type.kind_of?(DataSource)@@instance=data_source_or_typeelsifdata_source_or_type==:ruby@@instance=DataSources::RubyDataSource.newelsifdata_source_or_type==:zoneinfo@@instance=DataSources::ZoneinfoDataSource.new(*args)elseraiseArgumentError,'data_source_or_type must be a DataSource instance or a data source type (:ruby or :zoneinfo)'endendprivate# 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.defcreate_default_data_sourcehas_tzinfo_data=falsebeginrequire'tzinfo/data'has_tzinfo_data=truerescueLoadErrorendreturnDataSources::RubyDataSource.newifhas_tzinfo_databeginreturnDataSources::ZoneinfoDataSource.newrescueDataSources::ZoneinfoDirectoryNotFoundraiseDataSourceNotFound,"No source of timezone data could be found.\nPlease refer to https://tzinfo.github.io/datasourcenotfound for help resolving this error."endendend# Initializes a new {DataSource} instance. Typically only called via# subclasses of {DataSource}.definitialize@timezones=Concurrent::Map.newend# 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.defget_timezone_info(identifier)result=@timezones[identifier]unlessresult# 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]=resultendresultend# @return [Array<String>] a frozen `Array`` of all the available time zone# identifiers. The identifiers are sorted according to `String#<=>`.deftimezone_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_identifiersend# 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.defdata_timezone_identifiersraise_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.deflinked_timezone_identifiersraise_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.defget_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.defcountry_codesraise_invalid_data_source('country_codes')end# Loads all timezone and country data into memory.## This may be desirable in production environments to improve copy-on-write# performance and to avoid flushing the constant cache every time a new# timezone or country is loaded from {DataSources::RubyDataSource}.defeager_load!timezone_identifiers.each{|identifier|load_timezone_info(identifier)}country_codes.each{|code|load_country_info(code)}nilend# @return [String] a description of the {DataSource}.defto_s"Default DataSource"end# @return [String] the internal object state as a programmer-readable# `String`.definspect"#<#{self.class}>"endprotected# 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.defload_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.defload_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}.deftimezone_identifier_encodingEncoding::UTF_8end# 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}.defvalidate_timezone_identifier(identifier)raiseInvalidTimezoneIdentifier,"Invalid identifier: #{identifier.nil??'nil':identifier}"unlessidentifier.kind_of?(String)valid_identifier=try_with_encoding(identifier,timezone_identifier_encoding){|id|find_timezone_identifier(id)}returnvalid_identifierifvalid_identifierraiseInvalidTimezoneIdentifier,"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`.deflookup_country_info(hash,code,encoding=Encoding::UTF_8)raiseInvalidCountryCode,"Invalid country code: #{code.nil??'nil':code}"unlesscode.kind_of?(String)info=try_with_encoding(code,encoding){|c|hash[c]}returninfoifinforaiseInvalidCountryCode,"Invalid country code: #{code.encode(Encoding::UTF_8)}"endprivate# Raises {InvalidDataSource} to indicate that a method has not been# overridden by a particular data source implementation.## @raise [InvalidDataSource] always.defraise_invalid_data_source(method_name)raiseInvalidDataSource,"#{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.defbuild_timezone_identifiersdata=data_timezone_identifierslinked=linked_timezone_identifierslinked.empty??data:(data+linked).sort!.freezeendif[].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:deffind_timezone_identifier(identifier)result=timezone_identifiers.bsearch{|i|i>=identifier}result==identifier?result:nilend# :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:deffind_timezone_identifier(identifier)identifiers=timezone_identifierslow=0high=identifiers.lengthwhilelow<highdomid=(low+high).div(2)mid_identifier=identifiers[mid]cmp=mid_identifier<=>identifierreturnmid_identifierifcmp==0ifcmp>0high=midelselow=mid+1endendnilend# :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`.deftry_with_encoding(string,encoding)result=yieldstringreturnresultifresultunlessencoding==string.encodingstring=string.encode(encoding)yieldstringendendendend