diff options
Diffstat (limited to 'activesupport')
-rw-r--r-- | activesupport/CHANGELOG | 2 | ||||
-rw-r--r-- | activesupport/lib/active_support/values/time_zone.rb | 273 | ||||
-rw-r--r-- | activesupport/test/abstract_unit.rb | 25 | ||||
-rw-r--r-- | activesupport/test/time_zone_test.rb | 139 |
4 files changed, 314 insertions, 125 deletions
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index 5cd4a483e4..53ae421131 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Replace non-dst-aware TimeZone class with dst-aware class from tzinfo_timezone plugin. TimeZone#adjust and #unadjust are no longer available; tzinfo gem must now be present in order to perform time zone calculations, via #local_to_utc and #utc_to_local methods. [Geoff Buesing] + * Extract ActiveSupport::Callbacks from Active Record, test case setup and teardown, and ActionController::Dispatcher. #10727 [Josh Peek] * Introducing DateTime #utc, #utc? and #utc_offset, for duck-typing compatibility with Time. Closes #10002 [Geoff Buesing] diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 09896c0aee..92d6febda3 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,20 +1,155 @@ -# A value object representing a time zone. A time zone is simply a named -# offset (in seconds) from GMT. Note that two time zone objects are only -# equivalent if they have both the same offset, and the same name. -# -# A TimeZone instance may be used to convert a Time value to the corresponding -# time zone. -# -# The class also includes #all, which returns a list of all TimeZone objects. class TimeZone - include Comparable + MAPPING = { + "International Date Line West" => "Pacific/Midway", + "Midway Island" => "Pacific/Midway", + "Samoa" => "Pacific/Pago_Pago", + "Hawaii" => "Pacific/Honolulu", + "Alaska" => "America/Juneau", + "Pacific Time (US & Canada)" => "America/Los_Angeles", + "Tijuana" => "America/Tijuana", + "Mountain Time (US & Canada)" => "America/Denver", + "Arizona" => "America/Phoenix", + "Chihuahua" => "America/Chihuahua", + "Mazatlan" => "America/Mazatlan", + "Central Time (US & Canada)" => "America/Chicago", + "Saskatchewan" => "America/Regina", + "Guadalajara" => "America/Mexico_City", + "Mexico City" => "America/Mexico_City", + "Monterrey" => "America/Monterrey", + "Central America" => "America/Guatemala", + "Eastern Time (US & Canada)" => "America/New_York", + "Indiana (East)" => "America/Indiana/Indianapolis", + "Bogota" => "America/Bogota", + "Lima" => "America/Lima", + "Quito" => "America/Lima", + "Atlantic Time (Canada)" => "America/Halifax", + "Caracas" => "America/Caracas", + "La Paz" => "America/La_Paz", + "Santiago" => "America/Santiago", + "Newfoundland" => "America/St_Johns", + "Brasilia" => "America/Argentina/Buenos_Aires", + "Buenos Aires" => "America/Argentina/Buenos_Aires", + "Georgetown" => "America/Argentina/San_Juan", + "Greenland" => "America/Godthab", + "Mid-Atlantic" => "Atlantic/South_Georgia", + "Azores" => "Atlantic/Azores", + "Cape Verde Is." => "Atlantic/Cape_Verde", + "Dublin" => "Europe/Dublin", + "Edinburgh" => "Europe/Dublin", + "Lisbon" => "Europe/Lisbon", + "London" => "Europe/London", + "Casablanca" => "Africa/Casablanca", + "Monrovia" => "Africa/Monrovia", + "Belgrade" => "Europe/Belgrade", + "Bratislava" => "Europe/Bratislava", + "Budapest" => "Europe/Budapest", + "Ljubljana" => "Europe/Ljubljana", + "Prague" => "Europe/Prague", + "Sarajevo" => "Europe/Sarajevo", + "Skopje" => "Europe/Skopje", + "Warsaw" => "Europe/Warsaw", + "Zagreb" => "Europe/Zagreb", + "Brussels" => "Europe/Brussels", + "Copenhagen" => "Europe/Copenhagen", + "Madrid" => "Europe/Madrid", + "Paris" => "Europe/Paris", + "Amsterdam" => "Europe/Amsterdam", + "Berlin" => "Europe/Berlin", + "Bern" => "Europe/Berlin", + "Rome" => "Europe/Rome", + "Stockholm" => "Europe/Stockholm", + "Vienna" => "Europe/Vienna", + "West Central Africa" => "Africa/Algiers", + "Bucharest" => "Europe/Bucharest", + "Cairo" => "Africa/Cairo", + "Helsinki" => "Europe/Helsinki", + "Kyev" => "Europe/Kiev", + "Riga" => "Europe/Riga", + "Sofia" => "Europe/Sofia", + "Tallinn" => "Europe/Tallinn", + "Vilnius" => "Europe/Vilnius", + "Athens" => "Europe/Athens", + "Istanbul" => "Europe/Istanbul", + "Minsk" => "Europe/Minsk", + "Jerusalem" => "Asia/Jerusalem", + "Harare" => "Africa/Harare", + "Pretoria" => "Africa/Johannesburg", + "Moscow" => "Europe/Moscow", + "St. Petersburg" => "Europe/Moscow", + "Volgograd" => "Europe/Moscow", + "Kuwait" => "Asia/Kuwait", + "Riyadh" => "Asia/Riyadh", + "Nairobi" => "Africa/Nairobi", + "Baghdad" => "Asia/Baghdad", + "Tehran" => "Asia/Tehran", + "Abu Dhabi" => "Asia/Muscat", + "Muscat" => "Asia/Muscat", + "Baku" => "Asia/Baku", + "Tbilisi" => "Asia/Tbilisi", + "Yerevan" => "Asia/Yerevan", + "Kabul" => "Asia/Kabul", + "Ekaterinburg" => "Asia/Yekaterinburg", + "Islamabad" => "Asia/Karachi", + "Karachi" => "Asia/Karachi", + "Tashkent" => "Asia/Tashkent", + "Chennai" => "Asia/Calcutta", + "Kolkata" => "Asia/Calcutta", + "Mumbai" => "Asia/Calcutta", + "New Delhi" => "Asia/Calcutta", + "Kathmandu" => "Asia/Katmandu", + "Astana" => "Asia/Dhaka", + "Dhaka" => "Asia/Dhaka", + "Sri Jayawardenepura" => "Asia/Dhaka", + "Almaty" => "Asia/Almaty", + "Novosibirsk" => "Asia/Novosibirsk", + "Rangoon" => "Asia/Rangoon", + "Bangkok" => "Asia/Bangkok", + "Hanoi" => "Asia/Bangkok", + "Jakarta" => "Asia/Jakarta", + "Krasnoyarsk" => "Asia/Krasnoyarsk", + "Beijing" => "Asia/Shanghai", + "Chongqing" => "Asia/Chongqing", + "Hong Kong" => "Asia/Hong_Kong", + "Urumqi" => "Asia/Urumqi", + "Kuala Lumpur" => "Asia/Kuala_Lumpur", + "Singapore" => "Asia/Singapore", + "Taipei" => "Asia/Taipei", + "Perth" => "Australia/Perth", + "Irkutsk" => "Asia/Irkutsk", + "Ulaan Bataar" => "Asia/Ulaanbaatar", + "Seoul" => "Asia/Seoul", + "Osaka" => "Asia/Tokyo", + "Sapporo" => "Asia/Tokyo", + "Tokyo" => "Asia/Tokyo", + "Yakutsk" => "Asia/Yakutsk", + "Darwin" => "Australia/Darwin", + "Adelaide" => "Australia/Adelaide", + "Canberra" => "Australia/Melbourne", + "Melbourne" => "Australia/Melbourne", + "Sydney" => "Australia/Sydney", + "Brisbane" => "Australia/Brisbane", + "Hobart" => "Australia/Hobart", + "Vladivostok" => "Asia/Vladivostok", + "Guam" => "Pacific/Guam", + "Port Moresby" => "Pacific/Port_Moresby", + "Magadan" => "Asia/Magadan", + "Solomon Is." => "Asia/Magadan", + "New Caledonia" => "Pacific/Noumea", + "Fiji" => "Pacific/Fiji", + "Kamchatka" => "Asia/Kamchatka", + "Marshall Is." => "Pacific/Majuro", + "Auckland" => "Pacific/Auckland", + "Wellington" => "Pacific/Auckland", + "Nuku'alofa" => "Pacific/Tongatapu" + } + include Comparable attr_reader :name, :utc_offset - # Create a new TimeZone object with the given name and offset. The offset is - # the number of seconds that this time zone is offset from UTC (GMT). Seconds - # were chosen as the offset unit because that is the unit that Ruby uses - # to represent time zone offsets (see Time#utc_offset). + # Create a new TimeZone object with the given name and offset. The + # offset is the number of seconds that this time zone is offset from UTC + # (GMT). Seconds were chosen as the offset unit because that is the unit that + # Ruby uses to represent time zone offsets (see Time#utc_offset). def initialize(name, utc_offset) @name = name @utc_offset = utc_offset @@ -24,35 +159,14 @@ class TimeZone # format "+HH:MM". If the offset is zero, this returns the empty # string. If +colon+ is false, a colon will not be inserted into the # result. - def formatted_offset( colon=true ) - return "" if utc_offset == 0 - utc_offset.to_utc_offset_s(colon) - end - - # Compute and return the current time, in the time zone represented by - # +self+. - def now - adjust(Time.now) - end - - # Return the current date in this time zone. - def today - now.to_date + def formatted_offset(colon=true) + utc_offset == 0 ? '' : offset(colon) end - - # Adjust the given time to the time zone represented by +self+. - def adjust(time) - time = time.to_time unless time.is_a?(::Time) - time + utc_offset - time.utc_offset - end - - # Reinterprets the given time value as a time in the current time - # zone, and then adjusts it to return the corresponding time in the - # local time zone. - def unadjust(time) - time = time.to_time unless time.is_a?(::Time) - time = time.localtime - time - utc_offset - time.utc_offset + + # Returns the offset of this time zone as a formatted string, of the + # format "+HH:MM". + def offset(colon=true) + utc_offset.to_utc_offset_s(colon) end # Compare this time zone to the parameter. The two are comapred first on @@ -65,18 +179,58 @@ class TimeZone # Returns a textual representation of this time zone. def to_s - "(UTC#{formatted_offset}) #{name}" + "(GMT#{formatted_offset}) #{name}" + end + + begin # the following methods depend on the tzinfo gem + require_library_or_gem "tzinfo" unless Object.const_defined?(:TZInfo) + + # Compute and return the current time, in the time zone represented by + # +self+. + def now + tzinfo.now + end + + # Return the current date in this time zone. + def today + now.to_date + end + + # Adjust the given time to the time zone represented by +self+. + def utc_to_local(time) + tzinfo.utc_to_local(time) + end + + def local_to_utc(time, dst=true) + tzinfo.local_to_utc(time, dst) + end + + # Available so that TimeZone instances respond like TZInfo::Timezone instances + def period_for_local(time, dst=true) + tzinfo.period_for_local(time, dst) + end + + def tzinfo + return @tzinfo if @tzinfo + @tzinfo = MAPPING[name] + if String === @tzinfo + @tzinfo = TZInfo::Timezone.get(@tzinfo) + MAPPING[name] = @tzinfo + end + @tzinfo + end + + rescue LoadError # Tzinfo gem is not available + # re-raise LoadError only when a tzinfo-dependent method is called: + %w(now today utc_to_local local_to_utc period_for_local tzinfo).each do |method| + define_method(method) {|*args| raise LoadError, "TZInfo gem is required for TimeZone##{method}. `gem install tzinfo` and try again."} + end end @@zones = nil class << self - # Create a new TimeZone instance with the given name and offset. - def create(name, offset) - zone = allocate - zone.send!(:initialize, name, offset) - zone - end + alias_method :create, :new # Return a TimeZone instance with the given name, or +nil+ if no # such TimeZone instance exists. (This exists to support the use of @@ -85,18 +239,18 @@ class TimeZone self[name] end - # Return an array of all TimeZone objects. There are multiple TimeZone - # objects per time zone, in many cases, to make it easier for users to - # find their own time zone. + # Return an array of all TimeZone objects. There are multiple + # TimeZone objects per time zone, in many cases, to make it easier + # for users to find their own time zone. def all unless @@zones @@zones = [] - [[-43_200, "International Date Line West" ], - [-39_600, "Midway Island", "Samoa" ], + @@zones_map = {} + [[-39_600, "International Date Line West", "Midway Island", "Samoa" ], [-36_000, "Hawaii" ], [-32_400, "Alaska" ], [-28_800, "Pacific Time (US & Canada)", "Tijuana" ], - [-25_200, "Mountain Time (US & Canada)", "Chihuahua", "Mazatlan", + [-25_200, "Mountain Time (US & Canada)", "Chihuahua", "Mazatlan", "Arizona" ], [-21_600, "Central Time (US & Canada)", "Saskatchewan", "Guadalajara", "Mexico City", "Monterrey", "Central America" ], @@ -141,7 +295,11 @@ class TimeZone "Wellington" ], [ 46_800, "Nuku'alofa" ]]. each do |offset, *places| - places.each { |place| @@zones << create(place, offset).freeze } + places.each do |place| + zone = create(place, offset) + @@zones << zone + @@zones_map[place] = zone + end end @@zones.sort! end @@ -156,7 +314,8 @@ class TimeZone def [](arg) case arg when String - all.find { |z| z.name == arg } + all # force the zones to be loaded + @@zones_map[arg] when Numeric arg *= 3600 if arg.abs <= 13 all.find { |z| z.utc_offset == arg.to_i } @@ -167,7 +326,7 @@ class TimeZone # A regular expression that matches the names of all time zones in # the USA. - US_ZONES = /US|Arizona|Indiana|Hawaii|Alaska/ unless defined?(US_ZONES) + US_ZONES = /US|Arizona|Indiana|Hawaii|Alaska/ # A convenience method for returning a collection of TimeZone objects # for time zones in the USA. diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 2cfa245bc8..c2fd943ca0 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -4,15 +4,26 @@ $:.unshift "#{File.dirname(__FILE__)}/../lib" $:.unshift File.dirname(__FILE__) require 'active_support' +def uses_gem(gem_name, test_name, version = '> 0') + require 'rubygems' + gem gem_name.to_s, version + require gem_name.to_s + yield +rescue LoadError + $stderr.puts "Skipping #{test_name} tests. `gem install #{gem_name}` and try again." +end + # Wrap tests that use Mocha and skip if unavailable. unless defined? uses_mocha - def uses_mocha(test_name) - require 'rubygems' - gem 'mocha', '>= 0.5.5' - require 'mocha' - yield - rescue LoadError - $stderr.puts "Skipping #{test_name} tests. `gem install mocha` and try again." + def uses_mocha(test_name, &block) + uses_gem('mocha', test_name, '>= 0.5.5', &block) + end +end + +# Wrap tests that use TZInfo and skip if unavailable. +unless defined? uses_tzinfo + def uses_tzinfo(test_name, &block) + uses_gem('tzinfo', test_name, &block) end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index a974d85dd7..939d928b3e 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -1,94 +1,110 @@ require 'abstract_unit' class TimeZoneTest < Test::Unit::TestCase - class MockTime - def self.now - Time.utc( 2004, 7, 25, 14, 49, 00 ) + + uses_tzinfo 'TestTimeZoneCalculations' do + + def test_utc_to_local + silence_warnings do # silence warnings raised by tzinfo gem + zone = TimeZone['Eastern Time (US & Canada)'] + assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500 + assert_equal Time.utc(2000, 6, 30, 20), zone.utc_to_local(Time.utc(2000, 7)) # dst offset -0400 + end end - - def self.local(*args) - Time.utc(*args) + + def test_local_to_utc + silence_warnings do # silence warnings raised by tzinfo gem + zone = TimeZone['Eastern Time (US & Canada)'] + assert_equal Time.utc(2000, 1, 1, 5), zone.local_to_utc(Time.utc(2000, 1)) # standard offset -0500 + assert_equal Time.utc(2000, 7, 1, 4), zone.local_to_utc(Time.utc(2000, 7)) # dst offset -0400 + end + end + + def test_period_for_local + silence_warnings do # silence warnings raised by tzinfo gem + zone = TimeZone['Eastern Time (US & Canada)'] + assert_instance_of TZInfo::TimezonePeriod, zone.period_for_local(Time.utc(2000)) + end + end + + TimeZone::MAPPING.keys.each do |name| + define_method("test_map_#{name.downcase.gsub(/[^a-z]/, '_')}_to_tzinfo") do + silence_warnings do # silence warnings raised by tzinfo gem + zone = TimeZone[name] + assert zone.tzinfo.respond_to?(:period_for_local) + end + end end - end - TimeZone::Time = MockTime + TimeZone.all.each do |zone| + name = zone.name.downcase.gsub(/[^a-z]/, '_') + define_method("test_from_#{name}_to_map") do + silence_warnings do # silence warnings raised by tzinfo gem + assert_instance_of TimeZone, TimeZone[zone.name] + end + end + + define_method("test_utc_offset_for_#{name}") do + silence_warnings do # silence warnings raised by tzinfo gem + period = zone.tzinfo.period_for_utc(Time.utc(2006,1,1,0,0,0)) + assert_equal period.utc_offset, zone.utc_offset + end + end + end + uses_mocha 'TestTimeZoneNowAndToday' do + def test_now + TZInfo::DataTimezone.any_instance.stubs(:now).returns(Time.utc(2000)) + assert_equal Time.utc(2000), TimeZone['Eastern Time (US & Canada)'].now + end + + def test_today + TZInfo::DataTimezone.any_instance.stubs(:now).returns(Time.utc(2000)) + assert_equal Date.new(2000), TimeZone['Eastern Time (US & Canada)'].today + end + end + end + def test_formatted_offset_positive - zone = TimeZone.create( "Test", 4200 ) - assert_equal "+01:10", zone.formatted_offset + zone = TimeZone['Moscow'] + assert_equal "+03:00", zone.formatted_offset + assert_equal "+0300", zone.formatted_offset(false) end - + def test_formatted_offset_negative - zone = TimeZone.create( "Test", -4200 ) - assert_equal "-01:10", zone.formatted_offset - end - - def test_now - zone = TimeZone.create( "Test", 4200 ) - assert_equal Time.local(2004,7,25,15,59,00).to_a[0,6], zone.now.to_a[0,6] - end - - def test_today - zone = TimeZone.create( "Test", 43200 ) - assert_equal Date.new(2004,7,26), zone.today - end - - def test_adjust_negative - zone = TimeZone.create( "Test", -4200 ) # 4200s == 70 mins - assert_equal Time.utc(2004,7,24,23,55,0), zone.adjust(Time.utc(2004,7,25,1,5,0)) + zone = TimeZone['Eastern Time (US & Canada)'] + assert_equal "-05:00", zone.formatted_offset + assert_equal "-0500", zone.formatted_offset(false) end - - def test_adjust_positive - zone = TimeZone.create( "Test", 4200 ) - assert_equal Time.utc(2004,7,26,1,5,0), zone.adjust(Time.utc(2004,7,25,23,55,0)) - end - - def test_unadjust - zone = TimeZone.create( "Test", 4200 ) - expect = Time.utc(2004,7,24,23,55,0).to_a[0,6] - actual = zone.unadjust(Time.utc(2004,7,25,1,5,0)).to_a[0,6] - assert_equal expect, actual - end - + def test_zone_compare - zone1 = TimeZone.create( "Test1", 4200 ) - zone2 = TimeZone.create( "Test1", 5600 ) - assert zone1 < zone2 - assert zone2 > zone1 - - zone1 = TimeZone.create( "Able", 10000 ) - zone2 = TimeZone.create( "Zone", 10000 ) + zone1 = TimeZone['Central Time (US & Canada)'] # offset -0600 + zone2 = TimeZone['Eastern Time (US & Canada)'] # offset -0500 assert zone1 < zone2 assert zone2 > zone1 - - zone1 = TimeZone.create( "Able", 10000 ) assert zone1 == zone1 end - + def test_to_s - zone = TimeZone.create( "Test", 4200 ) - assert_equal "(UTC+01:10) Test", zone.to_s + assert_equal "(GMT+03:00) Moscow", TimeZone['Moscow'].to_s end - + def test_all_sorted all = TimeZone.all 1.upto( all.length-1 ) do |i| assert all[i-1] < all[i] end end - + def test_index assert_nil TimeZone["bogus"] - assert_not_nil TimeZone["Central Time (US & Canada)"] - assert_not_nil TimeZone[8] + assert_instance_of TimeZone, TimeZone["Central Time (US & Canada)"] + assert_instance_of TimeZone, TimeZone[8] assert_raises(ArgumentError) { TimeZone[false] } end def test_new - a = TimeZone.new("Berlin") - b = TimeZone.new("Berlin") - assert_same a, b - assert_nil TimeZone.new("bogus") + assert_equal TimeZone["Central Time (US & Canada)"], TimeZone.new("Central Time (US & Canada)") end def test_us_zones @@ -96,3 +112,4 @@ class TimeZoneTest < Test::Unit::TestCase assert !TimeZone.us_zones.include?(TimeZone["Kuala Lumpur"]) end end + |