During the past few days I’ve been busy looking for existing gems doing date validation in Rails 4. I’ve tried a couple of the most famous, but they were compatible with Rails only up to 3.2, so I decided to write a solution myself.

It works fine for me. I’m not going to convert it to a gem because I don’t have time to learn how to right now. If you want to help, you’re welcome.

Docs

It’s all very straightforward.

  • The validator symbol is :date.
  • The validator really does nothing if you don’t provide any options…
  • Supported options are :after, :before, :on_or_after, and :on_or_before, plus :message.
  • If the message is not provided a fine default one is used instead.
  • Values of supported options can be date-castable objects, lambdas, or symbols.
  • Symbols must be method names of the validating object or its class.
  • Values are computed (if needed) and converted to date on each validation.

Code

Here is my DateValidator class, to be put into the app/validators folder.

class DateValidator < ActiveModel::EachValidator

  attr_accessor :computed_options

  def before(a, b);       a < b;  end
  def after(a, b);        a > b;  end
  def on_or_before(a, b); a <= b; end
  def on_or_after(a, b);  a >= b; end

  def checks
    %w(before after on_or_before on_or_after)
  end

  def message_limits
    needs_and =
        (computed_options[:on_or_after]  || computed_options[:after]) &&
        (computed_options[:on_or_before] || computed_options[:before])

    result = ['must be a date']
    result.push('on or after',  computed_options[:on_or_after])  if computed_options[:on_or_after]
    result.push('after',        computed_options[:after])        if computed_options[:after]
    result.push('and')                                           if needs_and
    result.push('on or before', computed_options[:on_or_before]) if computed_options[:on_or_before]
    result.push('before',       computed_options[:before])       if computed_options[:before]
    result.join(' ')
  end

  def compute_options(record)
    result = {}
    options.each do |key, val|
      next unless checks.include?(key.to_s)
      if val.respond_to?(:lambda?) and val.lambda?
        val = val.call
      elsif val.is_a? Symbol
        if record.respond_to?(val)
          val = record.send(val)
        elsif record.class.respond_to?(val)
          val = record.class.send(val)
        end
      end
      result[key] = val.to_date
    end
    self.computed_options = result
  end

  def validate_each(record, attribute, value)
    return unless value.present?

    return unless options
    compute_options(record) # do not cache this
                            # otherwise all the 'compute' thing is useless... #
    computed_options.each do |key, val|
      unless self.send(key, value, val)
        record.errors[attribute] << (computed_options[:message] || message_limits)
        return
      end
    end
  end
end

Here is an example of how to use it into a model.

class UserProfile < ActiveRecord::Base

  validates :name,       presence: true
  validates :birth_date, presence: true, date: {on_or_after: :birth_date_first, on_or_before: :birth_date_last}

  def self.birth_date_first
    118.years.ago
  end

  def self.birth_date_last
    18.years.ago
  end

end

Here is the form helper snippet (only what differs from scaffolding).

  <div class="field">
    <%= f.label :birth_date %><br>
    <%= f.date_field :birth_date, min: UserProfile.birth_date_first, max: UserProfile.birth_date_last %>
  </div>

Here is the controller snippet (only what differs from scaffolding).

  # GET /user_profiles/new
  def new
    @user_profile = UserProfile.new
    @user_profile.birth_date = 25.years.ago # this is the default value for new records
                                            # I tried to use the :value option in the date_field helper
                                            # but it overrides the value in the record also when editing... #
  end