开发者

How to validate a model's date attribute against a specific range (evaluated at run time)

I have several models with a date attribute and for each model I'd like to validate these dates against a given range. A basic example would be:

validates_inclusion_of  :dated_on, :in => Date.new(2000,1,1)..Date(2020,1,1)

Ideally I'd like to evaluate the date range at runtime, using a similar approach as named_scope uses, e.g:

validates_inclusion_of  :dated_on, :in => lambda {{ (Date.today - 2.years)..(Date.today + 2.years)}}

The above doesn't work of course, so what is the best w开发者_开发技巧ay of achieving the same result?


You can use the date validator:

    validates :dated_on, :date => {:after => Proc.new { Time.now + 2.years },
                                   :before => Proc.new { Time.now - 2.years } }


If the validation is the same for each class, the answer is fairly simple: put a validation method in a module and mix it in to each model, then use validate to add the validation:

# in lib/validates_dated_on_around_now
module ValidatesDatedOnAroundNow
  protected

  def validate_dated_around_now
    # make sure dated_on isn't more than five years in the past or future
    self.errors.add(:dated_on, "is not valid") unless ((5.years.ago)..(5.years.from_now)).include?(self.dated_on)
  end
end

class FirstModel
  include ValidatesDatedOnAroundNow
  validate :validate_dated_around_now
end

class SecondModel
  include ValidatesDatedOnAroundNow
  validate :validate_dated_around_now
end

If you want different ranges for each model, you probably want something more like this:

module ValidatesDateOnWithin
  def validates_dated_on_within(&range_lambda)
    validates_each :dated_on do |record, attr, value|
      range = range_lambda.call
      record.errors.add(attr_name, :inclusion, :value => value) unless range.include?(value)
    end
  end
end

class FirstModel
  extend ValidatesDatedOnWithin
  validates_dated_on_within { ((5.years.ago)..(5.years.from_now)) }
end

class SecondModel
  extend ValidatesDatedOnWithin
  validates_dated_on_within { ((2.years.ago)..(2.years.from_now)) }
end


validates :future_date, inclusion: { in: ->(g){ (Date.tomorrow..Float::INFINITY) }


A different solution is to rely on the fact that validates_inclusion_of only requires an :in object that responds to include?. Build a delayed-evaluated range as follows:

class DelayedEvalRange
  def initialize(&range_block)
    @range_block = range_block
  end
  def include?(x)
    @range_block.call.include?(x)
  end
end

class FirstModel
  validates_inclusion_of :dated_on, :in => (DelayedEvalRange.new() { ((5.years.ago)..(5.years.from_now)) })
end


The simplest and working solution is to use the in-built validation from Rails. Just validates it like that:

validates :dated_on, 
          inclusion: { in: (Date.new(2000,1,1)..Date(2020,1,1)) }

if you need to validate the presence as well just add another validation:

validates :occurred_at, 
          presence: true, 
          inclusion: { in: (Date.new(2000,1,1)..Date(2020,1,1)) }

Remember that you can always use helpers like 1.day.ago or 1.year.from_now to define ranges.

A special care should be taken when using relative dates. Consider the following example where the age should be in range 18..65:

validates :birthdate, 
          inclusion: { in: 65.years.ago..18.years.ago }

A person which is valid? # => true today, will suddenly become invalid once they turn 66.

This might lead to many unexpected cases, where your model cannot be updated anymore because this validation fails.

A clever way to handle this case is to validate the field only when it actually changes:

validates :birthdate, 
          inclusion: { in: 65.years.ago..18.years.ago }, 
          if: ->(model) { model.birthdate.present? && model.birthdate_changed? }


Ask yourself if you need to evaluate the range at runtime. A good developer doesn't always go for correctness, if the price is complexity, imho.

I wanted to sanity check that a date was reasonable, i.e. within a few years from now. It doesn't really matter if that turns out to be a range of several years around today or two weeks ago, and my server processes aren't likely to still be running in a few years. In the end, I decided that the simpler solution (just a straight inclusion :in => reasonable_date_range) was preferable to a correct, but more complicated solution.

That said, I like James A. Rosen's solution with DelayedEvalRange. If your range is just a couple of days, or correctness is important for some other reason, I'd go with that.


The simplest solution for me was:

validate :validate_dob

def validate_dob
  unless age_range.cover?(dob.present? && dob.to_date)
    errors.add(:dob, "must be between %s and %s" % age_range.minmax)
  end
end

However, if you want to remove duplications, I suggest moving it in your own validator.

And yes, I want runtime execution because my code will run in a couple of years, and after more than a year I didn't deploy, I surely don't want to bring up the project again.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜