Skip to content

Commit

Permalink
Add Sequel.tzinfo_disambiguator= to the named_timezones plugin for au…
Browse files Browse the repository at this point in the history
…tomatically handling TZInfo::AmbiguousTime exceptions (Fixes #616)
  • Loading branch information
jeremyevans committed Feb 15, 2013
1 parent de6874e commit e3e9b87
Show file tree
Hide file tree
Showing 3 changed files with 30 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
@@ -1,5 +1,7 @@
=== HEAD

* Add Sequel.tzinfo_disambiguator= to the named_timezones plugin for automatically handling TZInfo::AmbiguousTime exceptions (jeremyevans) (#616)

* Add Dataset#escape_like, for escaping LIKE metacharacters (jeremyevans) (#614)

* The LIKE operators now use an explicit ESCAPE '\' clause for similar behavior across databases (jeremyevans)
Expand Down
20 changes: 18 additions & 2 deletions lib/sequel/extensions/named_timezones.rb
Expand Up @@ -37,14 +37,21 @@ module Sequel
self.datetime_class = DateTime

module NamedTimezones
# 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

# 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_timzone.
# 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).utc_total_offset_rational
local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset_rational
(v - local_offset).new_offset(local_offset)
end

Expand All @@ -54,7 +61,7 @@ def convert_output_datetime_other(v, output_timezone)
# TZInfo 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).utc_total_offset_rational
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
Expand All @@ -63,6 +70,15 @@ def convert_output_datetime_other(v, output_timezone)
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
Expand Down
10 changes: 10 additions & 0 deletions spec/extensions/named_timezones_spec.rb
Expand Up @@ -19,6 +19,7 @@
Sequel.datetime_class = DateTime
end
after do
Sequel.tzinfo_disambiguator = nil
Sequel.default_timezone = nil
Sequel.datetime_class = Time
end
Expand Down Expand Up @@ -53,6 +54,15 @@ def ds.supports_timestamp_usecs?; false; end
dt.offset.should == -7/24.0
end

it "should raise an error for ambiguous timezones by default" do
proc{Sequel.database_to_application_timestamp('2004-10-31T01:30:00')}.should raise_error(Sequel::InvalidValue)
end

it "should support tzinfo_disambiguator= to handle ambiguous timezones automatically" do
Sequel.tzinfo_disambiguator = proc{|datetime, periods| periods.first}
Sequel.database_to_application_timestamp('2004-10-31T01:30:00').should == DateTime.parse('2004-10-30T22:30:00-07:00')
end

it "should assume datetimes coming out of the database that don't have an offset as coming from database_timezone" do
dt = Sequel.database_to_application_timestamp('2009-06-01 06:20:30')
dt.should == @dt
Expand Down

0 comments on commit e3e9b87

Please sign in to comment.