How to validate dates in Rails 4

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

 

11 Replies to “How to validate dates in Rails 4”

  1. @Emil, it doesn’t work your way because when the lambda is called, my code doesn’t pass any argument to it. You should be able to get what you mean by means of an instance method, though.

  2. Shouldn’t we also check for valid date? Something like: Date.parse(value.to_s) rescue record.errors[attribute] << 'must be a valid date' ?

Leave a Reply

Your email address will not be published. Required fields are marked *