1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
# 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.jd(v.jd, 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
|