proper timezone awareness and plugin parser hooks

Parser uses method compilation technique namely to a method not a proc
This commit is contained in:
Adam Meehan
2010-09-16 22:35:38 +10:00
parent 9ddd150b2f
commit 5d495505d9
7 changed files with 813 additions and 60 deletions

View File

@@ -1,6 +1,3 @@
$:.unshift(File.dirname(__FILE__) + '/../lib')
$:.unshift(File.dirname(__FILE__))
require 'rspec'
require 'rspec/autorun'
@@ -19,6 +16,7 @@ ValidatesTimeliness.setup do |c|
c.extend_orms = [ :active_record ]
c.enable_date_time_select_extension!
c.enable_multiparameter_extension!
c.default_timezone = :utc
end
Time.zone = 'Australia/Melbourne'

View File

@@ -24,64 +24,49 @@ describe ValidatesTimeliness::Conversion do
describe "for time type" do
it "should return same value for time value matching dummy date part" do
type_cast_value(Time.mktime(2000, 1, 1, 0, 0, 0), :time).should == Time.mktime(2000, 1, 1, 0, 0, 0)
type_cast_value(Time.utc(2000, 1, 1, 0, 0, 0), :time).should == Time.utc(2000, 1, 1, 0, 0, 0)
end
it "should return dummy time value with same time part for time value with different date" do
type_cast_value(Time.mktime(2010, 1, 1, 0, 0, 0), :time).should == Time.mktime(2000, 1, 1, 0, 0, 0)
type_cast_value(Time.utc(2010, 1, 1, 0, 0, 0), :time).should == Time.utc(2000, 1, 1, 0, 0, 0)
end
it "should return dummy time only for date value" do
type_cast_value(Date.new(2010, 1, 1), :time).should == Time.mktime(2000, 1, 1, 0, 0, 0)
type_cast_value(Date.new(2010, 1, 1), :time).should == Time.utc(2000, 1, 1, 0, 0, 0)
end
it "should return dummy date with shifted local time for UTC datetime value" do
type_cast_value(DateTime.new(2010, 1, 1, 12, 34, 56), :time).should == Time.mktime(2000, 1, 1, 23, 34, 56)
end
it "should return dummy date with time part for local datetime value" do
type_cast_value(DateTime.civil_from_format(:local, 2010, 1, 1, 12, 34, 56), :time).should == Time.mktime(2000, 1, 1, 12, 34, 56)
it "should return dummy date with time part for datetime value" do
type_cast_value(DateTime.civil_from_format(:utc, 2010, 1, 1, 12, 34, 56), :time).should == Time.utc(2000, 1, 1, 12, 34, 56)
end
end
describe "for datetime type" do
it "should return time in local offset for date value" do
it "should return Date as Time value" do
type_cast_value(Date.new(2010, 1, 1), :datetime).should == Time.local_time(2010, 1, 1, 0, 0, 0)
end
it "should return same value for same time value in local offset" do
type_cast_value(Time.local_time(2010, 1, 1, 12, 34, 56), :datetime).should == Time.local_time(2010, 1, 1, 12, 34, 56)
it "should return same Time value" do
value = Time.utc(2010, 1, 1, 12, 34, 56)
type_cast_value(Time.utc(2010, 1, 1, 12, 34, 56), :datetime).should == value
end
it "should return shifted local time value for UTC time value" do
type_cast_value(Time.utc(2010, 1, 1, 12, 34, 56), :datetime).should == Time.local_time(2010, 1, 1, 23, 34, 56)
end
it "should return shifted local time value for UTC datetime value" do
type_cast_value(DateTime.new(2010, 1, 1, 12, 34, 56), :datetime).should == Time.local_time(2010, 1, 1, 23, 34, 56)
end
it "should return time value with same component values for local datetime value" do
type_cast_value(DateTime.civil_from_format(:local, 2010, 1, 1, 12, 34, 56), :datetime).should == Time.local_time(2010, 1, 1, 12, 34, 56)
it "should return as Time with same component values" do
type_cast_value(DateTime.civil_from_format(:utc, 2010, 1, 1, 12, 34, 56), :datetime).should == Time.utc(2010, 1, 1, 12, 34, 56)
end
end
end
describe "#dummy_time" do
it 'should return dummy date with shifted local time part for UTC time value' do
dummy_time(Time.utc(2010, 11, 22, 12, 34, 56)).should == Time.local_time(2000, 1, 1, 23, 34, 56)
it 'should return Time with dummy date values but same time components' do
dummy_time(Time.utc(2010, 11, 22, 12, 34, 56)).should == Time.utc(2000, 1, 1, 12, 34, 56)
end
it 'should return dummy date with same time part part for local time value with non-dummy date' do
dummy_time(Time.local_time(2010, 11, 22, 12, 34, 56)).should == Time.local_time(2000, 1, 1, 12, 34, 56)
it 'should return same value for Time which already has dummy date values' do
dummy_time(Time.utc(2000, 1, 1, 12, 34, 56)).should == Time.utc(2000, 1, 1, 12, 34, 56)
end
it 'should return same value for local time with dummy date' do
dummy_time(Time.local_time(2000, 1, 1, 12, 34, 56)).should == Time.local_time(2000, 1, 1, 12, 34, 56)
end
it 'should return exact dummy time value for date value' do
dummy_time(Date.new(2010, 11, 22)).should == Time.mktime(2000, 1, 1, 0, 0, 0)
it 'should return base dummy time value for Date value' do
dummy_time(Date.new(2010, 11, 22)).should == Time.utc(2000, 1, 1, 0, 0, 0)
end
describe "with custom dummy date" do
@@ -91,7 +76,7 @@ describe ValidatesTimeliness::Conversion do
end
it 'should return dummy time with custom dummy date' do
dummy_time(Time.local_time(1999, 11, 22, 12, 34, 56)).should == Time.local_time(2010, 1, 1, 12, 34, 56)
dummy_time(Time.utc(1999, 11, 22, 12, 34, 56)).should == Time.utc(2010, 1, 1, 12, 34, 56)
end
after(:all) do
@@ -118,11 +103,6 @@ describe ValidatesTimeliness::Conversion do
evaluate_option_value(value, person).should == value
end
it 'should return local Time value from string time value' do
value = '2010-01-01 12:00:00'
evaluate_option_value(value, person).should == Time.mktime(2010,1,1,12,0,0)
end
it 'should return Time value returned from proc with 0 arity' do
value = Time.mktime(2010,1,1)
evaluate_option_value(lambda { value }, person).should == value
@@ -134,21 +114,37 @@ describe ValidatesTimeliness::Conversion do
evaluate_option_value(lambda {|r| r.birth_time }, person).should == value
end
it 'should return Time value from proc which returns string time' do
value = '2010-01-01 12:00:00'
evaluate_option_value(lambda { value }, person).should == Time.mktime(2010,1,1,12,0,0)
end
it 'should return Time value for attribute method symbol which returns Time' do
value = Time.mktime(2010,1,1)
person.birth_time = value
evaluate_option_value(:birth_time, person).should == value
end
it 'should return Time value for attribute method symbol which returns string time value' do
it 'should return Time value is default zone from string time value' do
value = '2010-01-01 12:00:00'
evaluate_option_value(value, person).should == Time.utc(2010,1,1,12,0,0)
end
it 'should return Time value is current zone from string time value if timezone aware' do
value = '2010-01-01 12:00:00'
evaluate_option_value(value, person, true).should == Time.zone.local(2010,1,1,12,0,0)
end
it 'should return Time value in default zone from proc which returns string time' do
value = '2010-01-01 12:00:00'
evaluate_option_value(lambda { value }, person).should == Time.utc(2010,1,1,12,0,0)
end
it 'should return Time value in default zone for attribute method symbol which returns string time value' do
value = '2010-01-01 12:00:00'
person.birth_time = value
evaluate_option_value(:birth_time, person).should == Time.mktime(2010,1,1,12,0,0)
evaluate_option_value(:birth_time, person).should == Time.utc(2010,1,1,12,0,0)
end
it 'should return Time value in current zone attribute method symbol which returns string time value if timezone aware' do
value = '2010-01-01 12:00:00'
person.birth_time = value
evaluate_option_value(:birth_time, person, true).should == Time.zone.local(2010,1,1,12,0,0)
end
context "restriction shorthand" do

View File

@@ -0,0 +1,340 @@
require 'spec_helper'
describe ValidatesTimeliness::Parser do
describe "format proc generator" do
it "should generate proc which outputs date array with values in correct order" do
generate_method('yyyy-mm-dd').call('2000', '1', '2').should == [2000,1,2,0,0,0,0]
end
it "should generate proc which outputs date array from format with different order" do
generate_method('dd/mm/yyyy').call('2', '1', '2000').should == [2000,1,2,0,0,0,0]
end
it "should generate proc which outputs time array" do
generate_method('hh:nn:ss').call('01', '02', '03').should == [0,0,0,1,2,3,0]
end
it "should generate proc which outputs time array with meridian 'pm' adjusted hour" do
generate_method('hh:nn:ss ampm').call('01', '02', '03', 'pm').should == [0,0,0,13,2,3,0]
end
it "should generate proc which outputs time array with meridian 'am' unadjusted hour" do
generate_method('hh:nn:ss ampm').call('01', '02', '03', 'am').should == [0,0,0,1,2,3,0]
end
it "should generate proc which outputs time array with microseconds" do
generate_method('hh:nn:ss.u').call('01', '02', '03', '99').should == [0,0,0,1,2,3,990000]
end
it "should generate proc which outputs datetime array with zone offset" do
generate_method('yyyy-mm-dd hh:nn:ss.u zo').call('2001', '02', '03', '04', '05', '06', '99', '+10:00').should == [2001,2,3,4,5,6,990000,36000]
end
end
describe "validate regexps" do
describe "for time formats" do
format_tests = {
'hh:nn:ss' => {:pass => ['12:12:12', '01:01:01'], :fail => ['1:12:12', '12:1:12', '12:12:1', '12-12-12']},
'hh-nn-ss' => {:pass => ['12-12-12', '01-01-01'], :fail => ['1-12-12', '12-1-12', '12-12-1', '12:12:12']},
'h:nn' => {:pass => ['12:12', '1:01'], :fail => ['12:2', '12-12']},
'h.nn' => {:pass => ['2.12', '12.12'], :fail => ['2.1', '12:12']},
'h nn' => {:pass => ['2 12', '12 12'], :fail => ['2 1', '2.12', '12:12']},
'h-nn' => {:pass => ['2-12', '12-12'], :fail => ['2-1', '2.12', '12:12']},
'h:nn_ampm' => {:pass => ['2:12am', '2:12 pm', '2:12 AM', '2:12PM'], :fail => ['1:2am', '1:12 pm', '2.12am']},
'h.nn_ampm' => {:pass => ['2.12am', '2.12 pm'], :fail => ['1:2am', '1:12 pm', '2:12am']},
'h nn_ampm' => {:pass => ['2 12am', '2 12 pm'], :fail => ['1 2am', '1 12 pm', '2:12am']},
'h-nn_ampm' => {:pass => ['2-12am', '2-12 pm'], :fail => ['1-2am', '1-12 pm', '2:12am']},
'h_ampm' => {:pass => ['2am', '2 am', '12 pm'], :fail => ['1.am', '12 pm', '2:12am']},
}
format_tests.each do |format, values|
it "should correctly validate times in format '#{format}'" do
regexp = generate_regexp(format)
values[:pass].each {|value| value.should match(regexp)}
values[:fail].each {|value| value.should_not match(regexp)}
end
end
end
describe "for date formats" do
format_tests = {
'yyyy/mm/dd' => {:pass => ['2000/02/01'], :fail => ['2000\02\01', '2000/2/1', '00/02/01']},
'yyyy-mm-dd' => {:pass => ['2000-02-01'], :fail => ['2000\02\01', '2000-2-1', '00-02-01']},
'yyyy.mm.dd' => {:pass => ['2000.02.01'], :fail => ['2000\02\01', '2000.2.1', '00.02.01']},
'm/d/yy' => {:pass => ['2/1/01', '02/01/00', '02/01/2000'], :fail => ['2/1/0', '2.1.01']},
'd/m/yy' => {:pass => ['1/2/01', '01/02/00', '01/02/2000'], :fail => ['1/2/0', '1.2.01']},
'm\d\yy' => {:pass => ['2\1\01', '2\01\00', '02\01\2000'], :fail => ['2\1\0', '2/1/01']},
'd\m\yy' => {:pass => ['1\2\01', '1\02\00', '01\02\2000'], :fail => ['1\2\0', '1/2/01']},
'd-m-yy' => {:pass => ['1-2-01', '1-02-00', '01-02-2000'], :fail => ['1-2-0', '1/2/01']},
'd.m.yy' => {:pass => ['1.2.01', '1.02.00', '01.02.2000'], :fail => ['1.2.0', '1/2/01']},
'd mmm yy' => {:pass => ['1 Feb 00', '1 Feb 2000', '1 February 00', '01 February 2000'],
:fail => ['1 Fe 00', 'Feb 1 2000', '1 Feb 0']}
}
format_tests.each do |format, values|
it "should correctly validate dates in format '#{format}'" do
regexp = generate_regexp(format)
values[:pass].each {|value| value.should match(regexp)}
values[:fail].each {|value| value.should_not match(regexp)}
end
end
end
describe "for datetime formats" do
format_tests = {
'ddd mmm d hh:nn:ss zo yyyy' => {:pass => ['Sat Jul 19 12:00:00 +1000 2008'], :fail => []},
'yyyy-mm-ddThh:nn:ss(?:Z|zo)' => {:pass => ['2008-07-19T12:00:00+10:00', '2008-07-19T12:00:00Z'], :fail => ['2008-07-19T12:00:00Z+10:00']},
}
format_tests.each do |format, values|
it "should correctly validate datetimes in format '#{format}'" do
regexp = generate_regexp(format)
values[:pass].each {|value| value.should match(regexp)}
values[:fail].each {|value| value.should_not match(regexp)}
end
end
end
end
describe "_parse" do
it "should return time array from date string" do
time_array = formats._parse('12:13:14', :time, :strict => true)
time_array.should == [2000,1,1,12,13,14,0]
end
it "should return date array from time string" do
time_array = formats._parse('2000-02-01', :date, :strict => true)
time_array.should == [2000,2,1,0,0,0,0]
end
it "should return datetime array from string value" do
time_array = formats._parse('2000-02-01 12:13:14', :datetime, :strict => true)
time_array.should == [2000,2,1,12,13,14,0]
end
it "should parse date string when type is datetime" do
time_array = formats._parse('2000-02-01', :datetime, :strict => false)
time_array.should == [2000,2,1,0,0,0,0]
end
it "should ignore time when extracting date and strict is false" do
time_array = formats._parse('2000-02-01 12:13', :date, :strict => false)
time_array.should == [2000,2,1,0,0,0,0]
end
it "should ignore time when extracting date from format with trailing year and strict is false" do
time_array = formats._parse('01-02-2000 12:13', :date, :strict => false)
time_array.should == [2000,2,1,0,0,0,0]
end
it "should ignore date when extracting time and strict is false" do
time_array = formats._parse('2000-02-01 12:13', :time, :strict => false)
time_array.should == [2000,1,1,12,13,0,0]
end
it "should return zone offset when :include_offset option is true" do
time_array = formats._parse('2000-02-01T12:13:14-10:30', :datetime, :include_offset => true)
time_array.should == [2000,2,1,12,13,14,0,-37800]
end
context "with format option" do
it "should return values if string matches specified format" do
time_array = formats._parse('2000-02-01 12:13:14', :datetime, :format => 'yyyy-mm-dd hh:nn:ss')
time_array.should == [2000,2,1,12,13,14,0]
end
it "should return nil if string does not match specified format" do
time_array = formats._parse('2000-02-01 12:13', :datetime, :format => 'yyyy-mm-dd hh:nn:ss')
time_array.should be_nil
end
end
context "date with ambiguous year" do
it "should return year in current century if year below threshold" do
time_array = formats._parse('01-02-29', :date)
time_array.should == [2029,2,1,0,0,0,0]
end
it "should return year in last century if year at or above threshold" do
time_array = formats._parse('01-02-30', :date)
time_array.should == [1930,2,1,0,0,0,0]
end
it "should allow custom threshold" do
default = ValidatesTimeliness::Parser.ambiguous_year_threshold
ValidatesTimeliness::Parser.ambiguous_year_threshold = 40
time_array = formats._parse('01-02-39', :date)
time_array.should == [2039,2,1,0,0,0,0]
time_array = formats._parse('01-02-40', :date)
time_array.should == [1940,2,1,0,0,0,0]
ValidatesTimeliness::Parser.ambiguous_year_threshold = default
end
end
context "with custom dummy date values" do
before(:all) do
@old_dummy_date = ValidatesTimeliness.dummy_date_for_time_type
ValidatesTimeliness.dummy_date_for_time_type = [2009,1,1]
end
it "should return time array with custom dummy date" do
time_array = formats._parse('12:13:14', :time, :strict => true)
time_array.should == [2009,1,1,12,13,14,0]
end
after(:all) do
ValidatesTimeliness.dummy_date_for_time_type = @old_dummy_date
end
end
end
describe "parse" do
it "should return time object for valid time string" do
parse("2000-01-01 12:13:14", :datetime).should be_kind_of(Time)
end
it "should return nil for time string with invalid date part" do
parse("2000-02-30 12:13:14", :datetime).should be_nil
end
it "should return nil for time string with invalid time part" do
parse("2000-02-01 25:13:14", :datetime).should be_nil
end
it "should return Time object when passed a Time object" do
parse(Time.now, :datetime).should be_kind_of(Time)
end
it "should convert time string into current timezone" do
Time.zone = 'Melbourne'
time = parse("2000-01-01 12:13:14", :datetime, :timezone_aware => true)
Time.zone.utc_offset.should == 10.hours
end
it "should return nil for invalid date string" do
parse("2000-02-30", :date).should be_nil
end
def parse(*args)
ValidatesTimeliness::Parser.parse(*args)
end
end
describe "make_time" do
it "should create time using current timezone" do
time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0])
time.zone.should == "UTC"
end
it "should create time using current timezone" do
Time.zone = 'Melbourne'
time = ValidatesTimeliness::Parser.make_time([2000,1,1,12,0,0], true)
time.zone.should == "EST"
end
end
describe "removing formats" do
it "should remove format from format array" do
formats.remove_formats(:time, 'h.nn_ampm')
formats.time_formats.should_not include("h o'clock")
end
it "should not match time after its format is removed" do
validate('2.12am', :time).should be_true
formats.remove_formats(:time, 'h.nn_ampm')
validate('2.12am', :time).should be_false
end
it "should raise error if format does not exist" do
lambda { formats.remove_formats(:time, "ss:hh:nn") }.should raise_error()
end
after do
formats.time_formats << 'h.nn_ampm'
formats.compile_format_expressions
end
end
describe "adding formats" do
before do
formats.compile_format_expressions
end
it "should add format to format array" do
formats.add_formats(:time, "h o'clock")
formats.time_formats.should include("h o'clock")
end
it "should match new format after its added" do
validate("12 o'clock", :time).should be_false
formats.add_formats(:time, "h o'clock")
validate("12 o'clock", :time).should be_true
end
it "should add format before specified format and be higher precedence" do
formats.add_formats(:time, "ss:hh:nn", :before => 'hh:nn:ss')
validate("59:23:58", :time).should be_true
time_array = formats._parse('59:23:58', :time)
time_array.should == [2000,1,1,23,58,59,0]
end
it "should raise error if format exists" do
lambda { formats.add_formats(:time, "hh:nn:ss") }.should raise_error()
end
it "should raise error if format exists" do
lambda { formats.add_formats(:time, "ss:hh:nn", :before => 'nn:hh:ss') }.should raise_error()
end
after do
formats.time_formats.delete("h o'clock")
formats.time_formats.delete("ss:hh:nn")
# reload class instead
end
end
describe "removing US formats" do
it "should validate a date as European format when US formats removed" do
time_array = formats._parse('01/02/2000', :date)
time_array.should == [2000, 1, 2,0,0,0,0]
formats.remove_us_formats
time_array = formats._parse('01/02/2000', :date)
time_array.should == [2000, 2, 1,0,0,0,0]
end
after do
# reload class
end
end
def formats
ValidatesTimeliness::Parser
end
def validate(time_string, type)
valid = false
formats.send("#{type}_expressions").each do |format, regexp, processor|
valid = true and break if /\A#{regexp}\Z/ =~ time_string
end
valid
end
def generate_regexp(format)
# wrap in line start and end anchors to emulate extract values method
/\A#{formats.send(:generate_format_expression, format)}\Z/
end
def generate_regexp_str(format)
formats.send(:generate_format_expression, format).inspect
end
def generate_method(format)
formats.send(:generate_format_expression, format)
ValidatesTimeliness::Parser.method(:"format_#{format}")
end
def delete_format(type, format)
formats.send("#{type}_formats").delete(format)
end
end