Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
sequel/lib/sequel/extensions/named_timezones.rb
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
184 lines (168 sloc)
7.37 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |