Skip to content

Commit

Permalink
Time handling improved to ensure UTC always used
Browse files Browse the repository at this point in the history
  • Loading branch information
samlown committed Apr 1, 2011
1 parent 3a3fc3c commit 38257f4
Show file tree
Hide file tree
Showing 9 changed files with 662 additions and 536 deletions.
5 changes: 5 additions & 0 deletions history.txt
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
File renamed without changes.
43 changes: 43 additions & 0 deletions lib/couchrest/model/core_extensions/time_parsing.rb
Original file line number Diff line number Diff line change
@@ -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

6 changes: 4 additions & 2 deletions lib/couchrest/model/properties.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ def property(name, *options, &block)
end

# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> 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)
Expand Down
36 changes: 8 additions & 28 deletions lib/couchrest/model/typecast.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -29,7 +6,11 @@ def typecast_value(value, property) # klass, init_method)
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
Expand Down Expand Up @@ -127,12 +108,11 @@ def typecast_to_time(value)
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

Expand All @@ -150,13 +130,13 @@ def typecast_hash_to_date(value)
# 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
Expand Down
4 changes: 3 additions & 1 deletion lib/couchrest_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@

# 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"
Expand Down
77 changes: 77 additions & 0 deletions spec/couchrest/core_extensions/time_parsing.rb
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 38257f4

Please sign in to comment.