changed parsing to use hash of regexp with optional processor blocks. Allows easy addition and removal of preffered formats

This commit is contained in:
Adam Meehan 2008-07-17 14:29:59 +10:00
parent 7cf8f2cbbc
commit 0dcc255901
2 changed files with 123 additions and 29 deletions

View File

@ -4,6 +4,9 @@ module ValidatesTimeliness
# The validity of values can be restricted to be before and/or certain dates # The validity of values can be restricted to be before and/or certain dates
# or times. # or times.
module Validations module Validations
mattr_accessor :valid_time_formats
mattr_accessor :valid_date_formats
mattr_accessor :valid_datetime_formats
# Error messages added to AR defaults to allow global override if you need. # Error messages added to AR defaults to allow global override if you need.
def self.included(base) def self.included(base)
@ -15,25 +18,114 @@ module ValidatesTimeliness
:on_or_before => "must be on or before %s", :on_or_before => "must be on or before %s",
:after => "must be after %s", :after => "must be after %s",
:on_or_after => "must be on or after %s" :on_or_after => "must be on or after %s"
} }
ActiveRecord::Errors.default_error_messages.update(error_messages) ActiveRecord::Errors.default_error_messages.update(error_messages)
end
module ClassMethods
base.class_inheritable_hash :valid_time_formats
base.class_inheritable_hash :valid_date_formats
base.class_inheritable_hash :valid_datetime_formats
base.valid_time_formats = self.valid_time_formats
base.valid_date_formats = self.valid_date_formats
base.valid_datetime_formats = self.valid_datetime_formats
end
# The if you want to combine a time regexp with a date regexp then you
# should not use line begin or end anchors in the expression. Pre and post
# match strings are still checked for validity, and fail the match if they
# are not empty.
#
# The proc object should return an array with 1-3 elements with values
# ordered like so [hour, minute, second]. The proc should have as many
# arguments as groups in the regexp or you will get an error.
self.valid_time_formats = {
:hhnnss_colons => /(\d{2}):(\d{2}):(\d{2})/,
:hhnnss_dashes => /(\d{2})-(\d{2})-(\d{2})/,
:hhnn_colons => /(\d{2}):(\d{2})/,
:hnn_dots => /(\d{1,2})\.(\d{2})/,
:hnn_spaces => /(\d{1,2})\s(\d{2})/,
:hnn_dashes => /(\d{1,2})-(\d{2})/,
:hnn_ampm_colons => [ /(\d{1,2}):(\d{2})\s?((?:a|p)\.?m\.?)/i, lambda {|h, n, md| [full_hour(h, md), n, 0] } ],
:hnn_ampm_dots => [ /(\d{1,2})\.(\d{2})\s?((?:a|p)\.?m\.?)/i, lambda {|h, n, md| [full_hour(h, md), n, 0] } ],
:hnn_ampm_spaces => [ /(\d{1,2})\s(\d{2})\s?((?:a|p)\.?m\.?)/i, lambda {|h, n, md| [full_hour(h, md), n, 0] } ],
:hnn_ampm_dashes => [ /(\d{1,2})-(\d{2})\s?((?:a|p)\.?m\.?)/i, lambda {|h, n, md| [full_hour(h, md), n, 0] } ],
:h_ampm => [ /(\d{1,2})\s?((?:a|p)\.?m\.?)/i, lambda {|h, md| [full_hour(h, md), 0, 0] } ]
}
# The proc object should return an array with 3 elements with values
# ordered like so year, month, day. The proc should have as many
# arguments as groups in the regexp or you will get an error.
self.valid_date_formats = {
:yyyymmdd_slashes => /(\d{4})\/(\d{2})\/(\d{2})/,
:yyyymmdd_dashes => /(\d{4})-(\d{2})-(\d{2})/,
:yyyymmdd_slashes => /(\d{4})\.(\d{2})\.(\d{2})/,
:mdyyyy_slashes => [ /(\d{1,2})\/(\d{1,2})\/(\d{4})/, lambda {|m, d, y| [y, m, d] } ],
:dmyyyy_slashes => [ /(\d{1,2})\/(\d{1,2})\/(\d{4})/, lambda {|d, m ,y| [y, m, d] } ],
:dmyyyy_dashes => [ /(\d{1,2})-(\d{1,2})-(\d{4})/, lambda {|d, m ,y| [y, m, d] } ],
:dmyyyy_dots => [ /(\d{1,2})\.(\d{1,2})\.(\d{4})/, lambda {|d, m ,y| [y, m, d] } ],
:mdyy_slashes => [ /(\d{1,2})\/(\d{1,2})\/(\d{2})/, lambda {|m, d ,y| [unambiguous_year(y), m, d] } ],
:dmyy_slashes => [ /(\d{1,2})\/(\d{1,2})\/(\d{2})/, lambda {|d, m ,y| [unambiguous_year(y), m, d] } ],
:dmyy_dashes => [ /(\d{1,2})-(\d{1,2})-(\d{2})/, lambda {|d, m ,y| [unambiguous_year(y), m, d] } ],
:dmyy_dots => [ /(\d{1,2})\.(\d{1,2})\.(\d{2})/, lambda {|d, m ,y| [unambiguous_year(y), m, d] } ],
:d_mmm_yyyy => [ /(\d{1,2}) (\w{3,9}) (\d{4})/, lambda {|d, m ,y| [y, m, d] } ],
:d_mmm_yy => [ /(\d{1,2}) (\w{3,9}) (\d{2})/, lambda {|d, m ,y| [unambiguous_year(y), m, d] } ]
}
self.valid_datetime_formats = {
:yyyymmdd_dashes_hhnnss_colons => /#{valid_date_formats[:yyyymmdd_dashes]}\s#{valid_time_formats[:hhnnss_colons]}/,
:yyyymmdd_dashes_hhnn_colons => /#{valid_date_formats[:yyyymmdd_dashes]}\s#{valid_time_formats[:hhnn_colons]}/,
:iso8601 => /#{valid_date_formats[:yyyymmdd_dashes]}T#{valid_time_formats[:hhnnss_colons]}(?:Z|[-+](\d{2}):(\d{2}))?/
}
module ClassMethods
def full_hour(hour, meridian)
hour = hour.to_i
if meridian.delete('.').downcase == 'am'
hour == 12 ? 0 : hour
else
hour == 12 ? hour : hour + 12
end
end
def unambiguous_year(year, threshold=30)
year = "#{year.to_i < threshold ? '20' : '19'}#{year}" if year.length == 2
year.to_i
end
# loop through regexp and call proc on matches if available. Allow pre or
# post match strings if bounded is false. Lastly fills out time_array to
# full 6 part datetime array.
def extract_date_time_values(time_string, formats, bounded=true)
time_array = nil
formats.each do |name, (regexp, processor)|
matches = regexp.match(time_string.strip)
if !matches.nil? && (!bounded || (matches.pre_match == "" && matches.post_match == ""))
time_array = matches[1..6] if processor.nil?
time_array = processor.call(matches[1..6]) unless processor.nil?
time_array = time_array.map {|i| i.to_i }
time_array += [nil] * (6 - time_array.length)
break
end
end
return time_array
end
# Override this method to use any date parsing algorithm you like such as # Override this method to use any date parsing algorithm you like such as
# Chronic. Just return nil for an invalid value and a Time object for a # Chronic. Just return nil for an invalid value and a Time object for a
# valid parsed value. # valid parsed value.
# #
# Remember Rails, since version 2, will automatically handle the fallback # Remember Rails, since version 2, will automatically handle the fallback
# to a DateTime when you create a time which is out of range. # to a DateTime when you create a time which is out of range.
def timeliness_date_time_parse(raw_value, type) def timeliness_date_time_parse(raw_value, type, strict=true)
return raw_value.to_time if raw_value.acts_like?(:time) || raw_value.is_a?(Date) return raw_value.to_time if raw_value.acts_like?(:time) || raw_value.is_a?(Date)
time_array = ParseDate.parsedate(raw_value, true) time_array = extract_date_time_values(raw_value, self.send("valid_#{type}_formats".to_sym), strict)
raise if time_array.nil?
if type == :time
if type == :time
time_array[3..5] = time_array[0..2]
# Rails dummy time date part is defined as 2000-01-01 # Rails dummy time date part is defined as 2000-01-01
time_array[0..2] = 2000, 1, 1 time_array[0..2] = 2000, 1, 1
elsif type == :date elsif type == :date
@ -136,7 +228,7 @@ module ValidatesTimeliness
when Proc when Proc
restriction.call(record) restriction.call(record)
else else
timeliness_date_time_parse(restriction, configuration[:type]) timeliness_date_time_parse(restriction, configuration[:type], false)
end end
next if compare.nil? next if compare.nil?

View File

@ -1,3 +1,5 @@
# TODO spec removing and adding formats
# TODO spec all formats
require File.dirname(__FILE__) + '/spec_helper' require File.dirname(__FILE__) + '/spec_helper'
describe ValidatesTimeliness::Validations do describe ValidatesTimeliness::Validations do
@ -146,11 +148,11 @@ describe ValidatesTimeliness::Validations do
end end
describe "for date type" do describe "for date type" do
it "should validate with invalid time part" do # it "should validate with invalid time part" do
person = BasicValidation.new # person = BasicValidation.new
person.birth_date = "1980-01-01 25:61:61" # person.birth_date = "1980-01-01 25:61:61"
person.should be_valid # person.should be_valid
end # end
describe "with before and after restrictions" do describe "with before and after restrictions" do
before :all do before :all do
@ -216,11 +218,11 @@ describe ValidatesTimeliness::Validations do
end end
describe "for time type" do describe "for time type" do
it "should validate with invalid date part" do # it "should validate with invalid date part" do
person = BasicValidation.new # person = BasicValidation.new
person.birth_time = "1980-02-30 23:59:59" # person.birth_time = "1980-02-30 23:59:59"
person.should be_valid # person.should be_valid
end # end
describe "with before and after restrictions" do describe "with before and after restrictions" do
before :all do before :all do
@ -242,7 +244,7 @@ describe ValidatesTimeliness::Validations do
end end
it "should have error when on boundary of :after restriction" do it "should have error when on boundary of :after restriction" do
@person.birth_time = "6:00" @person.birth_time = "06:00"
@person.should_not be_valid @person.should_not be_valid
@person.errors.on(:birth_time).should match(/must be after/) @person.errors.on(:birth_time).should match(/must be after/)
end end
@ -254,7 +256,7 @@ describe ValidatesTimeliness::Validations do
end end
it "should have error when before :after restriction" do it "should have error when before :after restriction" do
@person.birth_time = "5:59" @person.birth_time = "05:59"
@person.should_not be_valid @person.should_not be_valid
@person.errors.on(:birth_time).should match(/must be after/) @person.errors.on(:birth_time).should match(/must be after/)
end end
@ -265,7 +267,7 @@ describe ValidatesTimeliness::Validations do
end end
it "should have error when before :after restriction" do it "should have error when before :after restriction" do
@person.birth_time = "6:01" @person.birth_time = "06:01"
@person.should be_valid @person.should be_valid
end end
end end
@ -290,7 +292,7 @@ describe ValidatesTimeliness::Validations do
end end
it "should have error when before :on_or_after restriction" do it "should have error when before :on_or_after restriction" do
@person.birth_time = "5:59" @person.birth_time = "05:59"
@person.should_not be_valid @person.should_not be_valid
@person.errors.on(:birth_time).should match(/must be on or after/) @person.errors.on(:birth_time).should match(/must be on or after/)
end end
@ -301,7 +303,7 @@ describe ValidatesTimeliness::Validations do
end end
it "should be valid when on boundary of :on_or_after restriction" do it "should be valid when on boundary of :on_or_after restriction" do
@person.birth_time = "6:00" @person.birth_time = "06:00"
@person.should be_valid @person.should be_valid
end end
end end
@ -312,7 +314,7 @@ describe ValidatesTimeliness::Validations do
before :all do before :all do
class MixedBeforeAndAfter < Person class MixedBeforeAndAfter < Person
validates_timeliness_of :birth_date_and_time, :before => Date.new(2008,1,2), :after => lambda { Time.mktime(2008, 1, 1) } validates_timeliness_of :birth_date_and_time, :before => Date.new(2008,1,2), :after => lambda { Time.mktime(2008, 1, 1) }
validates_timeliness_of :birth_date, :on_or_before => Time.mktime(2008, 1, 2), :on_or_after => :birth_date_and_time validates_timeliness_of :birth_date, :type => :date, :on_or_before => Time.mktime(2008, 1, 2), :on_or_after => :birth_date_and_time
end end
end end
@ -321,13 +323,13 @@ describe ValidatesTimeliness::Validations do
end end
it "should correctly validate time attribute with Date restriction" do it "should correctly validate time attribute with Date restriction" do
@person.birth_date_and_time = "2008-01-03" @person.birth_date_and_time = "2008-01-03 00:00:00"
@person.should_not be_valid @person.should_not be_valid
@person.errors.on(:birth_date_and_time).should match(/must be before/) @person.errors.on(:birth_date_and_time).should match(/must be before/)
end end
it "should correctly validate with proc restriction" do it "should correctly validate with proc restriction" do
@person.birth_date_and_time = "2008-01-01" @person.birth_date_and_time = "2008-01-01 00:00:00"
@person.should_not be_valid @person.should_not be_valid
@person.errors.on(:birth_date_and_time).should match(/must be after/) @person.errors.on(:birth_date_and_time).should match(/must be after/)
end end