Dates are particularly vexing in a global web environment as there are many acceptable abbreviated and locale dependent formats. Rather than try to support multiple locale dependent formats I side-stepped the issue and choose two unambiguous formats along with abbreviations and date intervals.
These formats coupled with a date picker calendar control result in fairly painless date input.
# This code replumbs a few classes so that ActiveRecord accepts a wider range # of user friendly date formats. Change Date.parse_date if you need different # date input formats. To use: drop this file into ./lib, require it into # your model and then validate dates with the validates_dates validator. # # Written by Stuart Rackham <srackham@methods.co.nz> # Current version tested on Rails 1.0.0 # # Add date string parser class method to Date class. # class Date # Parse date string with one of the following formats: # # * ISO date format: yyyy-mm-dd, for example '2001-12-25' # * d[ mmm[ yy[yy]]]: examples: '22', '22 feb', '22 feb 2003', # '22 feb 03', '22 February 2003' # * +n[units] or -n[units]: Date from today: examples: '+22', '+22 days', # '+22d', '-4 weeks', '-4w', '-4week', '+6 months', '+6m', '+6month', # '-2 years', '-2y' # # The string argument is first converted to a string with #to_s. # Returns nil if passed nil or an empty string. # Raises ArgumentError if string can't be parsed. # def self.parse_date(string) string = string.to_s.strip.downcase return nil if string.empty? today = Date.today if string =~ /^(\d{4})-(\d{2})-(\d{2})$/ # ISO date format. date_array = ParseDate.parsedate(string, true) begin result = Date.new(date_array[0], date_array[1], date_array[2]) rescue raise ArgumentError end elsif string =~ /^(\d{1,2})(?:(?:\s+|-)([a-zA-Z]{3,8})(?:(?:\s+|-)(\d{2}(?:\d{2})?))?)?$/ # 'd mmmm yyyy' format and abbreviations. day = $1 month = $2 || Date::ABBR_MONTHNAMES[today.month] year = $3 || today.year date_array = ParseDate.parsedate("#{day} #{month} #{year}", true) begin result = Date.new(date_array[0], date_array[1], date_array[2]) rescue raise ArgumentError end elsif string =~ /^([+-]\d+)(?:\s*(d|days?|w|weeks?|m|months?|y|years?))?$/ # Date intervals. n = $1.to_i units = $2 || 'days' case units.first when 'd' result = today + n when 'w' result = today + n*7 when 'm' sign = n <=> 0 month = today.month + sign * (n.abs % 12) year = today.year + sign * (n.abs / 12) if month <1 month += 12 year -= 1 elsif month > 12 month -= 12 year += 1 end result = Date.new(year, month, today.day) when 'y' result = Date.new(today.year + n, today.month, today.day) end else raise ArgumentError end result end end ActiveRecord::Validations::ClassMethods.class_eval do # Validates date values, these can be dates or any formats accepted by # Date.parse_date. # # For example: # # class Person < ActiveRecord::Base # require_dependency 'date_validator' # validates_dates :birthday, # :from => '1 Jan 1920', # :to => Date.today, # :allow_nil => true # end # # Options: # * from - Minium allowed date. May be a date or a string recognized # by Date.parse_date. # * to - Maxumum allowed date. May be a date or a string recognized # by Date.parse_date. # * allow_nil - Attribute may be nil; skip validation. # def validates_dates(*attr_names) configuration = { :message => 'is an invalid date ' \ '(here are some valid examples: 23, 23 feb, 23 feb 06, ' \ '6 feb 2006, 2006-02-23, +6days, +6d, +6, +2w, -6m, +1y)', :on => :save, } configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash) # Don't let validates_each handle allow_nils, it checks the cast value. allow_nil = configuration.delete(:allow_nil) from = Date.parse_date(configuration.delete(:from)) to = Date.parse_date(configuration.delete(:to)) validates_each(attr_names, configuration) do |record, attr_name, value| before_cast = record.send("#{attr_name}_before_type_cast") next if allow_nil and (before_cast.nil? or before_cast == '') begin date = Date.parse_date(before_cast) rescue record.errors.add(attr_name, configuration[:message]) else if from and date < from record.errors.add(attr_name, "cannot be less than #{from.strftime('%e-%b-%Y')}") end if to and date > to record.errors.add(attr_name, "cannot be greater than #{to.strftime('%e-%b-%Y')}") end end end end end # Override default date type cast class method to handle Date.parse_date # formats(the default implementation returns nil if passed an unrecognized date # format). # class ActiveRecord::ConnectionAdapters::Column def self.string_to_date(string) return string unless string.is_a?(String) Date.parse_date(string) rescue nil end end