ActiveRecord Validations are a well-known and widely used functionality of Rails. It provides handy methods like presence, uniqueness, numericality , etc., to validate an attribute's data.

Let's use these validation methods to validate our Event model attributes.

# == Schema Information
#
# Table name: events
#
#  id                 :integer
#  title              :string
#  start_date         :date
#  end_date           :date
#  participant_count  :integer
#  created_at         :datetime
#  updated_at         :datetime

class Event < ApplicationRecord
  validates :start_date, presence: true
  validates :end_date, presence: true
end

Here we have added presence check on start_date and end_date. So if any one of the fields is nil, Rails will throw an ActiveRecord::RecordInvalid error as below:

Loading development environment (Rails 7.0.0)

3.0.0 :001 > event = Event.new(title: 'Event 1', start_date: nil, end_date: nil)
 => #<Event id: nil, title: "Event 1", start_date: nil, end_date: nil, created_at: nil, updated_at: nil>
3.0.0 :002 > event.validate!
Traceback (most recent call last):
        1: from (irb):2:in `<main>'
ActiveRecord::RecordInvalid (Validation failed: Start date can't be blank, End date can't be blank)

Well, that was an easy check, and using ActiveRecord validation made it effortless for us.
Now let's say we also need validation on the end_date attribute to be always greater than the start_date.

Before Rails 7

For comparison of dates, there is no in-built validator provided by Rails. So to achieve this with Rails < 7 we have to write our own custom validator as below:

class Event < ApplicationRecord
  validates :start_date, presence: true
  validates :end_date, presence: true

  validate :end_date_is_after_start_date

  private

  def end_date_is_after_start_date
    if end_date < start_date
      errors.add(:end_date, 'cannot be before the start date')
    end
  end
end
Loading development environment (Rails 5.2.6)

2.6.6 :001 > event = Event.new(title: 'Event 1', start_date: Date.today, end_date: Date.yesterday)
 => #<Event id: nil, title: "Event 1", start_date: "2021-11-14", end_date: "2021-11-13", created_at: nil, updated_at: nil>
2.6.6 :002 > event.validate!
Traceback (most recent call last):
        1: from (irb):2:in `<main>'
ActiveRecord::RecordInvalid (Validation failed: End date cannot be before the start date)

With Rails >= 7

Rails 7 adds ComparisonValidator to allow the comparison of dates without using a custom validator or any external gem. Let's rewrite the end_date validation using ComparisonValidator.

class Event < ApplicationRecord
  validates :start_date, presence: true
  validates :end_date, presence: true
  
  validates :end_date, comparison: { greater_than: :start_date }
  # OR
  validates_comparison_of :end_date, greater_than: :start_date
end
Loading development environment (Rails 7.0.0)

3.0.0 :001 > event = Event.new(title: 'Event 1', start_date: Date.today, end_date: Date.yesterday)
   (2.7ms)  SELECT sqlite_version(*)
 => #<Event id: nil, title: "Event 1", start_date: "2021-11-14", end_date: "2021-11-13", created_at: nil, updated_at: nil>
3.0.0 :002 > event.validate!
Traceback (most recent call last):
        1: from (irb):2:in `<main>'
ActiveRecord::RecordInvalid (Validation failed: End date must be greater than 2021-11-14)

ComparisonValidator provides a lot of comparisons option like greater_than, greater_than_or_equal_to, equal_to, less_than, less_than_or_equal_to and other_than. Each option can accept a value, proc, or symbol.

Can we combine multiple compare options?

Yes! we can use one or more compare options at the same time.
Let's say along with ensuring end_date is always greater than start_date, we also want to validate that the end_date is not today's date.

class Event < ApplicationRecord
  validates :start_date, presence: true
  validates :end_date, presence: true
  
  validates_comparison_of :end_date, greater_than: :start_date, other_than: Date.today
end

Is ComparisonValidator only for date comparison?

No!, ComparisonValidator allows comparing numeric, date as well as string values.  

class Event < ApplicationRecord  
  validates_comparison_of :participant_count, greater_than_or_equal_to: 2
end

Final Thoughts

ComparisonValidator is definitely a great addition to ActiveRecord as it will allow us to compare and validate attribute data effortlessly. I hope this article has given you a good idea about this new validator introduced with Rails 7.

Thank you for reading! ❤️

References

  1. PR#40095 to add ComparisonValidator
  2. Rails 7 Release Notes CHANGELOG.md