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
+
+