Never been to DZone Snippets before?

Snippets is a public source code repository. Easily build up your personal collection of code snippets, categorize them with tags / keywords, and share them with the world

Date Validator (See related posts)

The date_validator.rb snippet adds a validates_dates date validator to Rails that accepts a range of user friendly date formats.

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


You need to create an account or log in to post comments to this site.


Click here to browse all 5147 code snippets

Related Posts