diff --git a/history.txt b/history.txt index 37086b8..98b9c15 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,8 @@ +== 1.1.0.beta2 + +* Minor enhancements: + * Time handling improved in accordance with CouchRest 1.0.3. Always set to UTC. + == 1.1.0.beta * Epic enhancements: diff --git a/lib/couchrest/model/support/hash.rb b/lib/couchrest/model/core_extensions/hash.rb similarity index 100% rename from lib/couchrest/model/support/hash.rb rename to lib/couchrest/model/core_extensions/hash.rb diff --git a/lib/couchrest/model/core_extensions/time_parsing.rb b/lib/couchrest/model/core_extensions/time_parsing.rb new file mode 100644 index 0000000..32e6087 --- /dev/null +++ b/lib/couchrest/model/core_extensions/time_parsing.rb @@ -0,0 +1,43 @@ +module CouchRest + module Model + module CoreExtensions + module TimeParsing + + # Attemtps to parse a time string in ISO8601 format. + # If no match is found, the standard time parse will be used. + # + # Times, unless provided with a time zone, are assumed to be in + # UTC. + # + def parse_iso8601(string) + if (string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})(Z| ?([\+|\s|\-])?(\d{2}):?(\d{2}))?/) + # $1 = year + # $2 = month + # $3 = day + # $4 = hours + # $5 = minutes + # $6 = seconds + # $7 = UTC or Timezone + # $8 = time zone direction + # $9 = tz difference hours + # $10 = tz difference minutes + + if (!$7.to_s.empty? && $7 != 'Z') + new($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, "#{$8 == '-' ? '-' : '+'}#{$9}:#{$10}") + else + utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i) + end + else + parse(string) + end + end + + end + end + end +end + +Time.class_eval do + extend CouchRest::Model::CoreExtensions::TimeParsing +end + diff --git a/lib/couchrest/model/properties.rb b/lib/couchrest/model/properties.rb index 481d135..8e0d452 100644 --- a/lib/couchrest/model/properties.rb +++ b/lib/couchrest/model/properties.rb @@ -131,8 +131,10 @@ module CouchRest end # Automatically set updated_at and created_at fields - # on the document whenever saving occurs. CouchRest uses a pretty - # decent time format by default. See Time#to_json + # on the document whenever saving occurs. + # + # These properties are casted as Time objects, so they should always + # be set to UTC. def timestamps! class_eval <<-EOS, __FILE__, __LINE__ property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false) diff --git a/lib/couchrest/model/typecast.rb b/lib/couchrest/model/typecast.rb index 6e9499a..6e1113d 100644 --- a/lib/couchrest/model/typecast.rb +++ b/lib/couchrest/model/typecast.rb @@ -1,26 +1,3 @@ -class Time - # returns a local time value much faster than Time.parse - def self.mktime_with_offset(string) - string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})(([\+|\s|\-])*(\d{2}):?(\d{2}))?/ - # $1 = year - # $2 = month - # $3 = day - # $4 = hours - # $5 = minutes - # $6 = seconds - # $8 = time zone direction - # $9 = tz difference - # utc time with wrong TZ info: - time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6) - if ($7) - tz_difference = ("#{$8 == '-' ? '+' : '-'}#{$9}".to_i * 3600) - time + tz_difference + zone_offset(time.zone) - else - time - end - end -end - module CouchRest module Model module Typecast @@ -29,7 +6,11 @@ module CouchRest return nil if value.nil? klass = property.type_class if value.instance_of?(klass) || klass == Object - value + if klass == Time && !value.utc? + value.utc # Ensure Time is always in UTC + else + value + end elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass) send('typecast_to_'+klass.to_s.downcase, value) else @@ -127,12 +108,11 @@ module CouchRest if value.is_a?(Hash) typecast_hash_to_time(value) else - Time.mktime_with_offset(value.to_s) + Time.parse_iso8601(value.to_s) end rescue ArgumentError value rescue TypeError - # After failures, resort to normal time parse value end @@ -150,13 +130,13 @@ module CouchRest # Creates a Time instance from a Hash with keys :year, :month, :day, # :hour, :min, :sec def typecast_hash_to_time(value) - Time.local(*extract_time(value)) + Time.utc(*extract_time(value)) end # Extracts the given args from the hash. If a value does not exist, it # uses the value of Time.now. def extract_time(value) - now = Time.now + now = Time.now [:year, :month, :day, :hour, :min, :sec].map do |segment| typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i) end diff --git a/lib/couchrest_model.rb b/lib/couchrest_model.rb index f43ecba..00a6add 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -46,7 +46,9 @@ require "couchrest/model/designs/view" # Monkey patches applied to couchrest require "couchrest/model/support/couchrest" -require "couchrest/model/support/hash" +# Core Extensions +require "couchrest/model/core_extensions/hash" +require "couchrest/model/core_extensions/time_parsing" # Base libraries require "couchrest/model/casted_model" diff --git a/spec/couchrest/core_extensions/time_parsing.rb b/spec/couchrest/core_extensions/time_parsing.rb new file mode 100644 index 0000000..92c1489 --- /dev/null +++ b/spec/couchrest/core_extensions/time_parsing.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 +require File.expand_path('../../../spec_helper', __FILE__) + +describe "Time Parsing core extension" do + + describe "Time" do + + it "should respond to .parse_iso8601" do + Time.respond_to?("parse_iso8601").should be_true + end + + describe ".parse_iso8601" do + + describe "parsing" do + + before :each do + # Time.parse should not be called for these tests! + Time.stub!(:parse).and_return(nil) + end + + it "should parse JSON time" do + txt = "2011-04-01T19:05:30Z" + Time.parse_iso8601(txt).should eql(Time.utc(2011, 04, 01, 19, 05, 30)) + end + + it "should parse JSON time as UTC without Z" do + txt = "2011-04-01T19:05:30" + Time.parse_iso8601(txt).should eql(Time.utc(2011, 04, 01, 19, 05, 30)) + end + + it "should parse basic time as UTC" do + txt = "2011-04-01 19:05:30" + Time.parse_iso8601(txt).should eql(Time.utc(2011, 04, 01, 19, 05, 30)) + end + + it "should parse JSON time with zone" do + txt = "2011-04-01T19:05:30 +02:00" + Time.parse_iso8601(txt).should eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:00")) + end + + it "should parse JSON time with zone 2" do + txt = "2011-04-01T19:05:30-0200" + Time.parse_iso8601(txt).should eql(Time.new(2011, 04, 01, 19, 05, 30, "-02:00")) + end + + it "should parse dodgy time with zone" do + txt = "2011-04-01 19:05:30 +0200" + Time.parse_iso8601(txt).should eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:00")) + end + + it "should parse dodgy time with zone 2" do + txt = "2011-04-01 19:05:30+0230" + Time.parse_iso8601(txt).should eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:30")) + end + + it "should parse dodgy time with zone 3" do + txt = "2011-04-01 19:05:30 0230" + Time.parse_iso8601(txt).should eql(Time.new(2011, 04, 01, 19, 05, 30, "+02:30")) + end + + end + + describe "resorting back to normal parse" do + before :each do + Time.should_receive(:parse) + end + it "should work with weird time" do + txt = "16/07/1981 05:04:00" + Time.parse_iso8601(txt) + end + + end + end + + end + +end diff --git a/spec/couchrest/property_spec.rb b/spec/couchrest/property_spec.rb index 1e3548d..adbdacf 100644 --- a/spec/couchrest/property_spec.rb +++ b/spec/couchrest/property_spec.rb @@ -211,510 +211,6 @@ describe "Model properties" do end end - describe "casting" do - before(:each) do - @course = Course.new(:title => 'Relaxation') - end - - describe "when value is nil" do - it "leaves the value unchanged" do - @course.title = nil - @course['title'].should == nil - end - end - - describe "when type primitive is an Object" do - it "it should not cast given value" do - @course.participants = [{}, 'q', 1] - @course['participants'].should == [{}, 'q', 1] - end - - it "should cast started_on to Date" do - @course.started_on = Date.today - @course['started_on'].should be_an_instance_of(Date) - end - end - - describe "when type primitive is a String" do - it "keeps string value unchanged" do - value = "1.0" - @course.title = value - @course['title'].should equal(value) - end - - it "it casts to string representation of the value" do - @course.title = 1.0 - @course['title'].should eql("1.0") - end - end - - describe 'when type primitive is a Float' do - it 'returns same value if a float' do - value = 24.0 - @course.estimate = value - @course['estimate'].should equal(value) - end - - it 'returns float representation of a zero string integer' do - @course.estimate = '0' - @course['estimate'].should eql(0.0) - end - - it 'returns float representation of a positive string integer' do - @course.estimate = '24' - @course['estimate'].should eql(24.0) - end - - it 'returns float representation of a negative string integer' do - @course.estimate = '-24' - @course['estimate'].should eql(-24.0) - end - - it 'returns float representation of a zero string float' do - @course.estimate = '0.0' - @course['estimate'].should eql(0.0) - end - - it 'returns float representation of a positive string float' do - @course.estimate = '24.35' - @course['estimate'].should eql(24.35) - end - - it 'returns float representation of a negative string float' do - @course.estimate = '-24.35' - @course['estimate'].should eql(-24.35) - end - - it 'returns float representation of a zero string float, with no leading digits' do - @course.estimate = '.0' - @course['estimate'].should eql(0.0) - end - - it 'returns float representation of a positive string float, with no leading digits' do - @course.estimate = '.41' - @course['estimate'].should eql(0.41) - end - - it 'returns float representation of a zero integer' do - @course.estimate = 0 - @course['estimate'].should eql(0.0) - end - - it 'returns float representation of a positive integer' do - @course.estimate = 24 - @course['estimate'].should eql(24.0) - end - - it 'returns float representation of a negative integer' do - @course.estimate = -24 - @course['estimate'].should eql(-24.0) - end - - it 'returns float representation of a zero decimal' do - @course.estimate = BigDecimal('0.0') - @course['estimate'].should eql(0.0) - end - - it 'returns float representation of a positive decimal' do - @course.estimate = BigDecimal('24.35') - @course['estimate'].should eql(24.35) - end - - it 'returns float representation of a negative decimal' do - @course.estimate = BigDecimal('-24.35') - @course['estimate'].should eql(-24.35) - end - - it 'return float of a number with commas instead of points for decimals' do - @course.estimate = '23,35' - @course['estimate'].should eql(23.35) - end - - it "should handle numbers with commas and points" do - @course.estimate = '1,234.00' - @course.estimate.should eql(1234.00) - end - - it "should handle a mis-match of commas and points and maintain the last one" do - @course.estimate = "1,232.434.123,323" - @course.estimate.should eql(1232434123.323) - end - - it "should handle numbers with whitespace" do - @course.estimate = " 24.35 " - @course.estimate.should eql(24.35) - end - - [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| - it "does not typecast non-numeric value #{value.inspect}" do - @course.estimate = value - @course['estimate'].should equal(value) - end - end - - end - - describe 'when type primitive is a Integer' do - it 'returns same value if an integer' do - value = 24 - @course.hours = value - @course['hours'].should equal(value) - end - - it 'returns integer representation of a zero string integer' do - @course.hours = '0' - @course['hours'].should eql(0) - end - - it 'returns integer representation of a positive string integer' do - @course.hours = '24' - @course['hours'].should eql(24) - end - - it 'returns integer representation of a negative string integer' do - @course.hours = '-24' - @course['hours'].should eql(-24) - end - - it 'returns integer representation of a zero string float' do - @course.hours = '0.0' - @course['hours'].should eql(0) - end - - it 'returns integer representation of a positive string float' do - @course.hours = '24.35' - @course['hours'].should eql(24) - end - - it 'returns integer representation of a negative string float' do - @course.hours = '-24.35' - @course['hours'].should eql(-24) - end - - it 'returns integer representation of a zero string float, with no leading digits' do - @course.hours = '.0' - @course['hours'].should eql(0) - end - - it 'returns integer representation of a positive string float, with no leading digits' do - @course.hours = '.41' - @course['hours'].should eql(0) - end - - it 'returns integer representation of a zero float' do - @course.hours = 0.0 - @course['hours'].should eql(0) - end - - it 'returns integer representation of a positive float' do - @course.hours = 24.35 - @course['hours'].should eql(24) - end - - it 'returns integer representation of a negative float' do - @course.hours = -24.35 - @course['hours'].should eql(-24) - end - - it 'returns integer representation of a zero decimal' do - @course.hours = '0.0' - @course['hours'].should eql(0) - end - - it 'returns integer representation of a positive decimal' do - @course.hours = '24.35' - @course['hours'].should eql(24) - end - - it 'returns integer representation of a negative decimal' do - @course.hours = '-24.35' - @course['hours'].should eql(-24) - end - - it "should handle numbers with whitespace" do - @course.hours = " 24 " - @course['hours'].should eql(24) - end - - [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| - it "does not typecast non-numeric value #{value.inspect}" do - @course.hours = value - @course['hours'].should equal(value) - end - end - end - - describe 'when type primitive is a BigDecimal' do - it 'returns same value if a decimal' do - value = BigDecimal('24.0') - @course.profit = value - @course['profit'].should equal(value) - end - - it 'returns decimal representation of a zero string integer' do - @course.profit = '0' - @course['profit'].should eql(BigDecimal('0.0')) - end - - it 'returns decimal representation of a positive string integer' do - @course.profit = '24' - @course['profit'].should eql(BigDecimal('24.0')) - end - - it 'returns decimal representation of a negative string integer' do - @course.profit = '-24' - @course['profit'].should eql(BigDecimal('-24.0')) - end - - it 'returns decimal representation of a zero string float' do - @course.profit = '0.0' - @course['profit'].should eql(BigDecimal('0.0')) - end - - it 'returns decimal representation of a positive string float' do - @course.profit = '24.35' - @course['profit'].should eql(BigDecimal('24.35')) - end - - it 'returns decimal representation of a negative string float' do - @course.profit = '-24.35' - @course['profit'].should eql(BigDecimal('-24.35')) - end - - it 'returns decimal representation of a zero string float, with no leading digits' do - @course.profit = '.0' - @course['profit'].should eql(BigDecimal('0.0')) - end - - it 'returns decimal representation of a positive string float, with no leading digits' do - @course.profit = '.41' - @course['profit'].should eql(BigDecimal('0.41')) - end - - it 'returns decimal representation of a zero integer' do - @course.profit = 0 - @course['profit'].should eql(BigDecimal('0.0')) - end - - it 'returns decimal representation of a positive integer' do - @course.profit = 24 - @course['profit'].should eql(BigDecimal('24.0')) - end - - it 'returns decimal representation of a negative integer' do - @course.profit = -24 - @course['profit'].should eql(BigDecimal('-24.0')) - end - - it 'returns decimal representation of a zero float' do - @course.profit = 0.0 - @course['profit'].should eql(BigDecimal('0.0')) - end - - it 'returns decimal representation of a positive float' do - @course.profit = 24.35 - @course['profit'].should eql(BigDecimal('24.35')) - end - - it 'returns decimal representation of a negative float' do - @course.profit = -24.35 - @course['profit'].should eql(BigDecimal('-24.35')) - end - - it "should handle numbers with whitespace" do - @course.profit = " 24.35 " - @course['profit'].should eql(BigDecimal('24.35')) - end - - [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| - it "does not typecast non-numeric value #{value.inspect}" do - @course.profit = value - @course['profit'].should equal(value) - end - end - end - - describe 'when type primitive is a DateTime' do - describe 'and value given as a hash with keys like :year, :month, etc' do - it 'builds a DateTime instance from hash values' do - @course.updated_at = { - :year => '2006', - :month => '11', - :day => '23', - :hour => '12', - :min => '0', - :sec => '0' - } - result = @course['updated_at'] - - result.should be_kind_of(DateTime) - result.year.should eql(2006) - result.month.should eql(11) - result.day.should eql(23) - result.hour.should eql(12) - result.min.should eql(0) - result.sec.should eql(0) - end - end - - describe 'and value is a string' do - it 'parses the string' do - @course.updated_at = 'Dec, 2006' - @course['updated_at'].month.should == 12 - end - end - - it 'does not typecast non-datetime values' do - @course.updated_at = 'not-datetime' - @course['updated_at'].should eql('not-datetime') - end - end - - describe 'when type primitive is a Date' do - describe 'and value given as a hash with keys like :year, :month, etc' do - it 'builds a Date instance from hash values' do - @course.started_on = { - :year => '2007', - :month => '3', - :day => '25' - } - result = @course['started_on'] - - result.should be_kind_of(Date) - result.year.should eql(2007) - result.month.should eql(3) - result.day.should eql(25) - end - end - - describe 'and value is a string' do - it 'parses the string' do - @course.started_on = 'Dec 20th, 2006' - @course.started_on.month.should == 12 - @course.started_on.day.should == 20 - @course.started_on.year.should == 2006 - end - end - - it 'does not typecast non-date values' do - @course.started_on = 'not-date' - @course['started_on'].should eql('not-date') - end - end - - describe 'when type primitive is a Time' do - describe 'and value given as a hash with keys like :year, :month, etc' do - it 'builds a Time instance from hash values' do - @course.ends_at = { - :year => '2006', - :month => '11', - :day => '23', - :hour => '12', - :min => '0', - :sec => '0' - } - result = @course['ends_at'] - - result.should be_kind_of(Time) - result.year.should eql(2006) - result.month.should eql(11) - result.day.should eql(23) - result.hour.should eql(12) - result.min.should eql(0) - result.sec.should eql(0) - end - end - - describe 'and value is a string' do - it 'parses the string' do - t = Time.now - @course.ends_at = t.strftime('%Y/%m/%d %H:%M:%S %z') - @course['ends_at'].year.should eql(t.year) - @course['ends_at'].month.should eql(t.month) - @course['ends_at'].day.should eql(t.day) - @course['ends_at'].hour.should eql(t.hour) - @course['ends_at'].min.should eql(t.min) - @course['ends_at'].sec.should eql(t.sec) - end - it 'parses the string without offset' do - t = Time.now - @course.ends_at = t.strftime("%Y-%m-%d %H:%M:%S") - @course['ends_at'].year.should eql(t.year) - @course['ends_at'].month.should eql(t.month) - @course['ends_at'].day.should eql(t.day) - @course['ends_at'].hour.should eql(t.hour) - @course['ends_at'].min.should eql(t.min) - @course['ends_at'].sec.should eql(t.sec) - end - end - - it 'does not typecast non-time values' do - @course.ends_at = 'not-time' - @course['ends_at'].should eql('not-time') - end - end - - describe 'when type primitive is a Class' do - it 'returns same value if a class' do - value = Course - @course.klass = value - @course['klass'].should equal(value) - end - - it 'returns the class if found' do - @course.klass = 'Course' - @course['klass'].should eql(Course) - end - - it 'does not typecast non-class values' do - @course.klass = 'NoClass' - @course['klass'].should eql('NoClass') - end - end - - describe 'when type primitive is a Boolean' do - - [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].each do |value| - it "returns true when value is #{value.inspect}" do - @course.active = value - @course['active'].should be_true - end - end - - [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].each do |value| - it "returns false when value is #{value.inspect}" do - @course.active = value - @course['active'].should be_false - end - end - - [ 'string', 2, 1.0, BigDecimal('1.0'), DateTime.now, Time.now, Date.today, Class, Object.new, ].each do |value| - it "does not typecast value #{value.inspect}" do - @course.active = value - @course['active'].should equal(value) - end - end - - it "should respond to requests with ? modifier" do - @course.active = nil - @course.active?.should be_false - @course.active = false - @course.active?.should be_false - @course.active = true - @course.active?.should be_true - end - - it "should respond to requests with ? modifier on TrueClass" do - @course.very_active = nil - @course.very_active?.should be_false - @course.very_active = false - @course.very_active?.should be_false - @course.very_active = true - @course.very_active?.should be_true - end - end - - end end describe "properties of array of casted models" do @@ -836,7 +332,7 @@ describe "Property Class" do property.init_method.should eql('parse') end - ## Property Casting method. More thoroughly tested earlier. + ## Property Casting method. More thoroughly tested in typecast_spec. describe "casting" do it "should cast a value" do diff --git a/spec/couchrest/typecast_spec.rb b/spec/couchrest/typecast_spec.rb new file mode 100644 index 0000000..913764f --- /dev/null +++ b/spec/couchrest/typecast_spec.rb @@ -0,0 +1,521 @@ +# encoding: utf-8 +require File.expand_path('../../spec_helper', __FILE__) +require File.join(FIXTURE_PATH, 'more', 'cat') +require File.join(FIXTURE_PATH, 'more', 'person') +require File.join(FIXTURE_PATH, 'more', 'course') + +describe "Type Casting" do + + before(:each) do + @course = Course.new(:title => 'Relaxation') + end + + describe "when value is nil" do + it "leaves the value unchanged" do + @course.title = nil + @course['title'].should == nil + end + end + + describe "when type primitive is an Object" do + it "it should not cast given value" do + @course.participants = [{}, 'q', 1] + @course['participants'].should == [{}, 'q', 1] + end + + it "should cast started_on to Date" do + @course.started_on = Date.today + @course['started_on'].should be_an_instance_of(Date) + end + end + + describe "when type primitive is a String" do + it "keeps string value unchanged" do + value = "1.0" + @course.title = value + @course['title'].should equal(value) + end + + it "it casts to string representation of the value" do + @course.title = 1.0 + @course['title'].should eql("1.0") + end + end + + describe 'when type primitive is a Float' do + it 'returns same value if a float' do + value = 24.0 + @course.estimate = value + @course['estimate'].should equal(value) + end + + it 'returns float representation of a zero string integer' do + @course.estimate = '0' + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive string integer' do + @course.estimate = '24' + @course['estimate'].should eql(24.0) + end + + it 'returns float representation of a negative string integer' do + @course.estimate = '-24' + @course['estimate'].should eql(-24.0) + end + + it 'returns float representation of a zero string float' do + @course.estimate = '0.0' + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive string float' do + @course.estimate = '24.35' + @course['estimate'].should eql(24.35) + end + + it 'returns float representation of a negative string float' do + @course.estimate = '-24.35' + @course['estimate'].should eql(-24.35) + end + + it 'returns float representation of a zero string float, with no leading digits' do + @course.estimate = '.0' + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive string float, with no leading digits' do + @course.estimate = '.41' + @course['estimate'].should eql(0.41) + end + + it 'returns float representation of a zero integer' do + @course.estimate = 0 + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive integer' do + @course.estimate = 24 + @course['estimate'].should eql(24.0) + end + + it 'returns float representation of a negative integer' do + @course.estimate = -24 + @course['estimate'].should eql(-24.0) + end + + it 'returns float representation of a zero decimal' do + @course.estimate = BigDecimal('0.0') + @course['estimate'].should eql(0.0) + end + + it 'returns float representation of a positive decimal' do + @course.estimate = BigDecimal('24.35') + @course['estimate'].should eql(24.35) + end + + it 'returns float representation of a negative decimal' do + @course.estimate = BigDecimal('-24.35') + @course['estimate'].should eql(-24.35) + end + + it 'return float of a number with commas instead of points for decimals' do + @course.estimate = '23,35' + @course['estimate'].should eql(23.35) + end + + it "should handle numbers with commas and points" do + @course.estimate = '1,234.00' + @course.estimate.should eql(1234.00) + end + + it "should handle a mis-match of commas and points and maintain the last one" do + @course.estimate = "1,232.434.123,323" + @course.estimate.should eql(1232434123.323) + end + + it "should handle numbers with whitespace" do + @course.estimate = " 24.35 " + @course.estimate.should eql(24.35) + end + + [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| + it "does not typecast non-numeric value #{value.inspect}" do + @course.estimate = value + @course['estimate'].should equal(value) + end + end + + end + + describe 'when type primitive is a Integer' do + it 'returns same value if an integer' do + value = 24 + @course.hours = value + @course['hours'].should equal(value) + end + + it 'returns integer representation of a zero string integer' do + @course.hours = '0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive string integer' do + @course.hours = '24' + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative string integer' do + @course.hours = '-24' + @course['hours'].should eql(-24) + end + + it 'returns integer representation of a zero string float' do + @course.hours = '0.0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive string float' do + @course.hours = '24.35' + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative string float' do + @course.hours = '-24.35' + @course['hours'].should eql(-24) + end + + it 'returns integer representation of a zero string float, with no leading digits' do + @course.hours = '.0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive string float, with no leading digits' do + @course.hours = '.41' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a zero float' do + @course.hours = 0.0 + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive float' do + @course.hours = 24.35 + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative float' do + @course.hours = -24.35 + @course['hours'].should eql(-24) + end + + it 'returns integer representation of a zero decimal' do + @course.hours = '0.0' + @course['hours'].should eql(0) + end + + it 'returns integer representation of a positive decimal' do + @course.hours = '24.35' + @course['hours'].should eql(24) + end + + it 'returns integer representation of a negative decimal' do + @course.hours = '-24.35' + @course['hours'].should eql(-24) + end + + it "should handle numbers with whitespace" do + @course.hours = " 24 " + @course['hours'].should eql(24) + end + + [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| + it "does not typecast non-numeric value #{value.inspect}" do + @course.hours = value + @course['hours'].should equal(value) + end + end + end + + describe 'when type primitive is a BigDecimal' do + it 'returns same value if a decimal' do + value = BigDecimal('24.0') + @course.profit = value + @course['profit'].should equal(value) + end + + it 'returns decimal representation of a zero string integer' do + @course.profit = '0' + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive string integer' do + @course.profit = '24' + @course['profit'].should eql(BigDecimal('24.0')) + end + + it 'returns decimal representation of a negative string integer' do + @course.profit = '-24' + @course['profit'].should eql(BigDecimal('-24.0')) + end + + it 'returns decimal representation of a zero string float' do + @course.profit = '0.0' + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive string float' do + @course.profit = '24.35' + @course['profit'].should eql(BigDecimal('24.35')) + end + + it 'returns decimal representation of a negative string float' do + @course.profit = '-24.35' + @course['profit'].should eql(BigDecimal('-24.35')) + end + + it 'returns decimal representation of a zero string float, with no leading digits' do + @course.profit = '.0' + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive string float, with no leading digits' do + @course.profit = '.41' + @course['profit'].should eql(BigDecimal('0.41')) + end + + it 'returns decimal representation of a zero integer' do + @course.profit = 0 + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive integer' do + @course.profit = 24 + @course['profit'].should eql(BigDecimal('24.0')) + end + + it 'returns decimal representation of a negative integer' do + @course.profit = -24 + @course['profit'].should eql(BigDecimal('-24.0')) + end + + it 'returns decimal representation of a zero float' do + @course.profit = 0.0 + @course['profit'].should eql(BigDecimal('0.0')) + end + + it 'returns decimal representation of a positive float' do + @course.profit = 24.35 + @course['profit'].should eql(BigDecimal('24.35')) + end + + it 'returns decimal representation of a negative float' do + @course.profit = -24.35 + @course['profit'].should eql(BigDecimal('-24.35')) + end + + it "should handle numbers with whitespace" do + @course.profit = " 24.35 " + @course['profit'].should eql(BigDecimal('24.35')) + end + + [ Object.new, true, '00.0', '0.', '-.0', 'string' ].each do |value| + it "does not typecast non-numeric value #{value.inspect}" do + @course.profit = value + @course['profit'].should equal(value) + end + end + end + + describe 'when type primitive is a DateTime' do + describe 'and value given as a hash with keys like :year, :month, etc' do + it 'builds a DateTime instance from hash values' do + @course.updated_at = { + :year => '2006', + :month => '11', + :day => '23', + :hour => '12', + :min => '0', + :sec => '0' + } + result = @course['updated_at'] + + result.should be_kind_of(DateTime) + result.year.should eql(2006) + result.month.should eql(11) + result.day.should eql(23) + result.hour.should eql(12) + result.min.should eql(0) + result.sec.should eql(0) + end + end + + describe 'and value is a string' do + it 'parses the string' do + @course.updated_at = 'Dec, 2006' + @course['updated_at'].month.should == 12 + end + end + + it 'does not typecast non-datetime values' do + @course.updated_at = 'not-datetime' + @course['updated_at'].should eql('not-datetime') + end + end + + describe 'when type primitive is a Date' do + describe 'and value given as a hash with keys like :year, :month, etc' do + it 'builds a Date instance from hash values' do + @course.started_on = { + :year => '2007', + :month => '3', + :day => '25' + } + result = @course['started_on'] + + result.should be_kind_of(Date) + result.year.should eql(2007) + result.month.should eql(3) + result.day.should eql(25) + end + end + + describe 'and value is a string' do + it 'parses the string' do + @course.started_on = 'Dec 20th, 2006' + @course.started_on.month.should == 12 + @course.started_on.day.should == 20 + @course.started_on.year.should == 2006 + end + end + + it 'does not typecast non-date values' do + @course.started_on = 'not-date' + @course['started_on'].should eql('not-date') + end + end + + describe 'when type primitive is a Time' do + describe 'and value given as a hash with keys like :year, :month, etc' do + it 'builds a Time instance from hash values' do + @course.ends_at = { + :year => '2006', + :month => '11', + :day => '23', + :hour => '12', + :min => '0', + :sec => '0' + } + result = @course['ends_at'] + + result.should be_kind_of(Time) + result.year.should eql(2006) + result.month.should eql(11) + result.day.should eql(23) + result.hour.should eql(12) + result.min.should eql(0) + result.sec.should eql(0) + end + end + + describe 'and value is a string' do + it 'parses the string' do + t = Time.new(2011, 4, 1, 18, 50, 32, "+02:00") + @course.ends_at = t.strftime('%Y/%m/%d %H:%M:%S %z') + @course['ends_at'].year.should eql(t.year) + @course['ends_at'].month.should eql(t.month) + @course['ends_at'].day.should eql(t.day) + @course['ends_at'].hour.should eql(t.hour) + @course['ends_at'].min.should eql(t.min) + @course['ends_at'].sec.should eql(t.sec) + end + it 'parses the string without offset as UTC' do + t = Time.now.utc + @course.ends_at = t.strftime("%Y-%m-%d %H:%M:%S") + @course.ends_at.utc?.should be_true + @course['ends_at'].year.should eql(t.year) + @course['ends_at'].month.should eql(t.month) + @course['ends_at'].day.should eql(t.day) + @course['ends_at'].hour.should eql(t.hour) + @course['ends_at'].min.should eql(t.min) + @course['ends_at'].sec.should eql(t.sec) + end + end + + it "converts a time value into utc" do + t = Time.new(2011, 4, 1, 18, 50, 32, "+02:00") + @course.ends_at = t + @course.ends_at.utc?.should be_true + @course.ends_at.should eql(Time.utc(2011, 4, 1, 16, 50, 32)) + end + + it 'does not typecast non-time values' do + @course.ends_at = 'not-time' + @course['ends_at'].should eql('not-time') + end + end + + describe 'when type primitive is a Class' do + it 'returns same value if a class' do + value = Course + @course.klass = value + @course['klass'].should equal(value) + end + + it 'returns the class if found' do + @course.klass = 'Course' + @course['klass'].should eql(Course) + end + + it 'does not typecast non-class values' do + @course.klass = 'NoClass' + @course['klass'].should eql('NoClass') + end + end + + describe 'when type primitive is a Boolean' do + + [ true, 'true', 'TRUE', '1', 1, 't', 'T' ].each do |value| + it "returns true when value is #{value.inspect}" do + @course.active = value + @course['active'].should be_true + end + end + + [ false, 'false', 'FALSE', '0', 0, 'f', 'F' ].each do |value| + it "returns false when value is #{value.inspect}" do + @course.active = value + @course['active'].should be_false + end + end + + [ 'string', 2, 1.0, BigDecimal('1.0'), DateTime.now, Time.now, Date.today, Class, Object.new, ].each do |value| + it "does not typecast value #{value.inspect}" do + @course.active = value + @course['active'].should equal(value) + end + end + + it "should respond to requests with ? modifier" do + @course.active = nil + @course.active?.should be_false + @course.active = false + @course.active?.should be_false + @course.active = true + @course.active?.should be_true + end + + it "should respond to requests with ? modifier on TrueClass" do + @course.very_active = nil + @course.very_active?.should be_false + @course.very_active = false + @course.very_active?.should be_false + @course.very_active = true + @course.very_active?.should be_true + end + end + +end + +