From 6329daf5acc8f5f2f040cf46f33098fb21a622b5 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Wed, 23 Feb 2005 12:56:16 +0000 Subject: Added TimeZone as the first of a number of value objects that Active Record will start shipping to provide incentatives to use rich value objects using composed_of #688 [Jamis Buck] git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@760 5ecf4fe2-1ee6-0310-87b1-e25e094e27de --- activerecord/CHANGELOG | 2 + activerecord/lib/active_record.rb | 1 + activerecord/lib/active_record/values/time_zone.rb | 155 +++++++++++++++++++++ activerecord/test/time_zone_test.rb | 78 +++++++++++ 4 files changed, 236 insertions(+) create mode 100644 activerecord/lib/active_record/values/time_zone.rb create mode 100644 activerecord/test/time_zone_test.rb (limited to 'activerecord') diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index 04b8374bee..667ea44d3b 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,7 @@ *SVN* +* Added TimeZone as the first of a number of value objects that Active Record will start shipping to provide incentatives to use rich value objects using composed_of #688 [Jamis Buck] + * Added option :schema_order to the PostgreSQL adapter to support the use of multiple schemas per database #697 [YuriSchimke] * Optimized the SQL used to generate has_and_belongs_to_many queries by listing the join table first #693 [yerejm] diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 04d16b41f3..682fec2b4a 100755 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -47,6 +47,7 @@ require 'active_record/timestamp' require 'active_record/acts/list' require 'active_record/acts/tree' require 'active_record/locking' +require 'active_record/values/time_zone' ActiveRecord::Base.class_eval do include ActiveRecord::Validations diff --git a/activerecord/lib/active_record/values/time_zone.rb b/activerecord/lib/active_record/values/time_zone.rb new file mode 100644 index 0000000000..7766727a40 --- /dev/null +++ b/activerecord/lib/active_record/values/time_zone.rb @@ -0,0 +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 + + 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). + def initialize(name, utc_offset) + @name = name + @utc_offset = utc_offset + end + + # Returns the offset of this time zone as a formatted string, of the + # format "+HH:MM". If the offset is zero, this returns the empty + # string. + def formatted_offset + return "" if utc_offset == 0 + sign = (utc_offset < 0 ? -1 : 1) + hours = utc_offset.abs / 3600 + minutes = (utc_offset.abs % 3600) / 60 + "%+03d:%02d" % [ hours * sign, minutes ] + end + + # Compute and return the current time, in the time zone represented by + # +self+. + def now + adjust(Time.now) + end + + # Adjust the given time to the time zone represented by +self+. + def adjust(time) + offset = time.utc_offset + time + utc_offset - offset + end + + # Compare this time zone to the parameter. The two are comapred first on + # their offsets, and then by name. + def <=>(zone) + result = (utc_offset <=> zone.utc_offset) + result = (name <=> zone.name) if result == 0 + result + end + + # Returns a textual representation of this time zone. + def to_s + "(GMT#{formatted_offset}) #{name}" + 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 + + # Return a TimeZone instance with the given name, or +nil+ if no + # such TimeZone instance exists. (This exists to support the use of + # this class with the #composed_of macro.) + def new(name) + 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. + def all + unless @@zones + @@zones = [] + [[-43_200, "International Date Line West" ], + [-39_600, "Midway Island", "Samoa" ], + [-36_000, "Hawaii" ], + [-32_400, "Alaska" ], + [-28_800, "Pacific Time (US & Canada)", "Tijuana" ], + [-25_200, "Mountain Time (US & Canada)", "Chihuahua", "La Paz", + "Mazatlan", "Arizona" ], + [-21_600, "Central Time (US & Canada)", "Saskatchewan", "Guadalajara", + "Mexico City", "Monterrey", "Central America" ], + [-18_000, "Eastern Time (US & Canada)", "Indiana (East)", "Bogota", + "Lima", "Quito" ], + [-14_400, "Atlantic Time (Canada)", "Caracas", "La Paz", "Santiago" ], + [-12_600, "Newfoundland" ], + [-10_800, "Brasilia", "Buenos Aires", "Georgetown", "Greenland" ], + [ -7_200, "Mid-Atlantic" ], + [ -3_600, "Azores", "Cape Verde Is." ], + [ 0, "Dublin", "Edinburgh", "Lisbon", "London", "Casablanca", + "Monrovia" ], + [ 3_600, "Belgrade", "Bratislava", "Budapest", "Ljubljana", "Prague", + "Sarajevo", "Skopje", "Warsaw", "Zagreb", "Brussels", + "Copenhagen", "Madrid", "Paris", "Amsterdam", "Berlin", + "Bern", "Rome", "Stockholm", "Vienna", + "West Central Africa" ], + [ 7_200, "Bucharest", "Cairo", "Helsinki", "Kyev", "Riga", "Sofia", + "Tallinn", "Vilnius", "Athens", "Istanbul", "Minsk", + "Jerusalem", "Harare", "Pretoria" ], + [ 10_800, "Moscow", "St. Petersburg", "Volgograd", "Kuwait", "Riyadh", + "Nairobi", "Baghdad" ], + [ 12_600, "Tehran" ], + [ 14_400, "Abu Dhabi", "Muscat", "Baku", "Tbilisi", "Yerevan" ], + [ 16_200, "Kabul" ], + [ 18_000, "Ekaterinburg", "Islamabad", "Karachi", "Tashkent" ], + [ 19_800, "Chennai", "Kolkata", "Mumbai", "New Delhi" ], + [ 20_700, "Kathmandu" ], + [ 21_600, "Astana", "Dhaka", "Sri Jayawardenepura", "Almaty", + "Novosibirsk" ], + [ 23_400, "Rangoon" ], + [ 25_200, "Bangkok", "Hanoi", "Jakarta", "Krasnoyarsk" ], + [ 28_800, "Beijing", "Chongqing", "Hong Kong", "Urumqi", + "Kuala Lumpur", "Singapore", "Taipei", "Perth", "Irkutsk", + "Ulaan Bataar" ], + [ 32_400, "Seoul", "Osaka", "Sapporo", "Tokyo", "Yakutsk" ], + [ 34_200, "Darwin", "Adelaide" ], + [ 36_000, "Canberra", "Melbourne", "Sydney", "Brisbane", "Hobart", + "Vladivostok", "Guam", "Port Moresby" ], + [ 39_600, "Magadan", "Solomon Is.", "New Caledonia" ], + [ 43_200, "Fiji", "Kamchatka", "Marshall Is.", "Auckland", + "Wellington" ], + [ 46_800, "Nuku'alofa" ]]. + each do |offset, *places| + places.each { |place| @@zones << create(place, offset).freeze } + end + @@zones.sort! + end + @@zones + end + + # Locate a specific time zone object by the name it was given. Returns + # +nil+ if no such time zone is known to the system. + def [](name) + all.find { |z| z.name == name } + end + + # A regular expression that matches the names of all time zones in + # the USA. + US_ZONES = /US|Arizona|Indiana|Hawaii|Alaska/ + + # A convenience method for returning a collection of TimeZone objects + # for time zones in the USA. + def us_zones + all.find_all { |z| z.name =~ US_ZONES } + end + end +end \ No newline at end of file diff --git a/activerecord/test/time_zone_test.rb b/activerecord/test/time_zone_test.rb new file mode 100644 index 0000000000..1fbfb95a62 --- /dev/null +++ b/activerecord/test/time_zone_test.rb @@ -0,0 +1,78 @@ +require 'test/unit' +require File.dirname(__FILE__)+'/../lib/active_record/values/time_zone' + +class TimeZoneTest < Test::Unit::TestCase + + class MockTime + def self.now + Time.utc( 2004, 7, 25, 14, 49, 00 ) + end + end + + TimeZone::Time = MockTime + + def test_formatted_offset_positive + zone = TimeZone.create( "Test", 4200 ) + assert_equal "+01:10", zone.formatted_offset + 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_adjust_negative + zone = TimeZone.create( "Test", -4200 ) + assert_equal Time.utc(2004,7,24,23,55,0), zone.adjust(Time.utc(2004,7,25,1,5,0)) + 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_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 ) + 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 "(GMT+01:10) Test", zone.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)"] + end + + def test_new + a = TimeZone.new("Berlin") + b = TimeZone.new("Berlin") + assert_same a, b + assert_nil TimeZone.new("bogus") + end + +end -- cgit v1.2.3