-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
Copy pathnamed_timezones.rb
184 lines (168 loc) · 7.39 KB
/
named_timezones.rb
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.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