# frozen-string-literal: true # # Allows the use of named timezones via TZInfo (requires tzinfo). # Forces the use of DateTime as Sequel's datetime_class, since # historically, Ruby's Time class doesn't support timezones other # than local and UTC. To continue using Ruby's Time class when using # the named_timezones extension: # # # Load the extension # Sequel.extension :named_timezones # # # Set Sequel.datetime_class back to Time # Sequel.datetime_class = Time # # This allows you to either pass strings or TZInfo::Timezone # instance to Sequel.database_timezone=, application_timezone=, and # typecast_timezone=. If a string is passed, it is converted to a # TZInfo::Timezone using TZInfo::Timezone.get. # # Let's say you have the database server in New York and the # application server in Los Angeles. For historical reasons, data # is stored in local New York time, but the application server only # services clients in Los Angeles, so you want to use New York # time in the database and Los Angeles time in the application. This # is easily done via: # # Sequel.database_timezone = 'America/New_York' # Sequel.application_timezone = 'America/Los_Angeles' # # Then, before data is stored in the database, it is converted to New # York time. When data is retrieved from the database, it is # converted to Los Angeles time. # # If you are using database specific timezones, you may want to load # this extension into the database in order to support similar API: # # DB.extension :named_timezones # DB.timezone = 'America/New_York' # # Note that typecasting from the database timezone to the application # timezone when fetching rows is dependent on the database adapter, # and only works on adapters where Sequel itself does the conversion. # It should work with the mysql, postgres, sqlite, ibmdb, and jdbc # adapters. # # Related module: Sequel::NamedTimezones require 'tzinfo' # module Sequel self.datetime_class = DateTime module NamedTimezones module DatabaseMethods def timezone=(tz) super(Sequel.send(:convert_timezone_setter_arg, tz)) end end # Handles TZInfo::AmbiguousTime exceptions automatically by providing a # proc called with both the datetime value being converted as well as # the array of TZInfo::TimezonePeriod results. Example: # # Sequel.tzinfo_disambiguator = proc{|datetime, periods| periods.first} attr_accessor :tzinfo_disambiguator private if RUBY_VERSION >= '2.6' # Whether Time.at with :nsec and :in is broken. True on JRuby < 9.3.9.0. BROKEN_TIME_AT_WITH_NSEC = defined?(JRUBY_VERSION) && (JRUBY_VERSION < '9.3' || (JRUBY_VERSION < '9.4' && JRUBY_VERSION.split('.')[2].to_i < 9)) private_constant :BROKEN_TIME_AT_WITH_NSEC # Convert the given input Time (which must be in UTC) to the given input timezone, # which should be a TZInfo::Timezone instance. def convert_input_time_other(v, input_timezone) Time.new(v.year, v.mon, v.day, v.hour, v.min, (v.sec + Rational(v.nsec, 1000000000)), input_timezone) rescue TZInfo::AmbiguousTime raise unless disamb = tzinfo_disambiguator_for(v) period = input_timezone.period_for_local(v, &disamb) offset = period.utc_total_offset # :nocov: if BROKEN_TIME_AT_WITH_NSEC Time.at(v.to_i - offset, :in => input_timezone) + v.nsec/1000000000.0 # :nocov: else Time.at(v.to_i - offset, v.nsec, :nsec, :in => input_timezone) end end # Convert the given input Time to the given output timezone, # which should be a TZInfo::Timezone instance. def convert_output_time_other(v, output_timezone) # :nocov: if BROKEN_TIME_AT_WITH_NSEC Time.at(v.to_i, :in => output_timezone) + v.nsec/1000000000.0 # :nocov: else Time.at(v.to_i, v.nsec, :nsec, :in => output_timezone) end end # :nodoc: # :nocov: else def convert_input_time_other(v, input_timezone) local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0 end if defined?(TZInfo::VERSION) && TZInfo::VERSION > '2' def convert_output_time_other(v, output_timezone) v = output_timezone.utc_to_local(v.getutc) local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0 + local_offset end else def convert_output_time_other(v, output_timezone) v = output_timezone.utc_to_local(v.getutc) local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i + v.nsec/1000000000.0 end end # :nodoc: # :nocov: end # Handle both TZInfo 1 and TZInfo 2 if defined?(TZInfo::VERSION) && TZInfo::VERSION > '2' def convert_input_datetime_other(v, input_timezone) local_offset = Rational(input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset, 86400) (v - local_offset).new_offset(local_offset) end def convert_output_datetime_other(v, output_timezone) v = output_timezone.utc_to_local(v.new_offset(0)) # Force DateTime output instead of TZInfo::DateTimeWithOffset DateTime.civil(v.year, v.month, v.day, v.hour, v.minute, v.second + v.sec_fraction, v.offset, v.start) end # :nodoc: # :nocov: else # Assume the given DateTime has a correct time but a wrong timezone. It is # currently in UTC timezone, but it should be converted to the input_timezone. # Keep the time the same but convert the timezone to the input_timezone. # Expects the input_timezone to be a TZInfo::Timezone instance. def convert_input_datetime_other(v, input_timezone) local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset_rational (v - local_offset).new_offset(local_offset) end # Convert the given DateTime to use the given output_timezone. # Expects the output_timezone to be a TZInfo::Timezone instance. def convert_output_datetime_other(v, output_timezone) # TZInfo 1 converts times, but expects the given DateTime to have an offset # of 0 and always leaves the timezone offset as 0 v = output_timezone.utc_to_local(v.new_offset(0)) local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset_rational # Convert timezone offset from UTC to the offset for the output_timezone (v - local_offset).new_offset(local_offset) end # :nodoc: # :nocov: end # Returns TZInfo::Timezone instance if given a String. def convert_timezone_setter_arg(tz) tz.is_a?(String) ? TZInfo::Timezone.get(tz) : super end # Return a disambiguation proc that provides both the datetime value # and the periods, in order to allow the choice of period to depend # on the datetime value. def tzinfo_disambiguator_for(v) if pr = @tzinfo_disambiguator proc{|periods| pr.call(v, periods)} end end end extend NamedTimezones Database.register_extension(:named_timezones, NamedTimezones::DatabaseMethods) end