Greg Blass
Greg Blass

Reputation: 3660

Validating time in rails

My app has the user input two times, and I calculate the amount of time between them as a convenience for the user. So the user enters 1:00 AM as the start time and 2:00 AM as the stop time, it will output "1 hour". I have a helper method that does this, using a gem for the heavy lifting. The times are stored as a Time data type in the database in order for that to work.

That all works fine, and now I want to validate that the time the user enters is valid - but also NOT require it. So if the user enters nothing for those times, the record still saves - but if the user enters, say "asdfasdf", I want the validations to get triggered and say that it is an invalid time, rather than just silently not storing the time.

I created my own custom time validation like so:

class TimeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    Time.parse(value.to_s)
  rescue ArgumentError
    record.errors[attribute] << (options[:message] || "is not a valid time")
  end
end

Then in my model, I'm doing:

validates :start_time, time: true
validates :stop_time, time: true

This does what I want as far as making sure the time can be correctly parsed, at it works pretty well. The user could enter 14:00, 3:00 PM, etc...and Rails can parse it. If they enter "asdfasdf", or any value that cannot be parsed as a valid time, the user is notified via a form validation error and the record is not saved.

But as that code stands, it will also trigger the validation error if the value is left blank.

So again, I want it to pass validations with a nil value, but fail if there is some value that cannot be parsed.

I tried:

validates :start_time, time: true, allow_blank: true

but then "asdfasdf" will pass. I also tried

Time.parse(value.to_s) unless value.nil?

but again, that breaks the whole thing and allows invalid dates to pass.

Any ideas?

Upvotes: 4

Views: 6583

Answers (3)

iamhubi
iamhubi

Reputation: 31

I have to admit first that I am not a Rails/Ruby expert.

I had the very same Problem today and I think i figured it out. The Problem is that Rails performs a type cast. If you enter an invalid parameter (e.g. starttime="asdf"), a dummy Time object will be created. When you validate it, it seems as everything went okay although it clearly didn't. You can access the "real" parameter value with :starttime_before_type_cast.

I adjusted Yule's code accordingly:

class TimeValidator < ActiveModel::EachValidator

  def validate_each(record, attribute, value)
    return true unless value
    begin
      Time.parse(value.to_s)
    rescue ArgumentError
      y = attribute.to_s
      y.slice! "_before_type_cast"
      record.errors[y] << (options[:message] || "invalid time format entered")
    end
  end

end

The validation in the model should look something like this:

validates :starttime, presence: true
validates :starttime_before_type_cast, time: true

The only snag with this approach is that the field in the form will contain the time 00:00 and not the malformed value after validation. I.e. The user won't see what he entered and what lead to the error message.

An article about Rails Type Casting

Upvotes: 3

phlegx
phlegx

Reputation: 2742

If you want to use a Gem, you can use this one.

This validates times and dates. See here some examples:

validates_datetime :occurred_at

validates_date :date_of_birth, :before => lambda { 18.years.ago },
                               :before_message => "must be at least 18 years old"

validates_datetime :finish_time, :after => :start_time # Method symbol

validates_date :booked_at, :on => :create, :on_or_after => :today # See Restriction Shorthand.

validates_time :booked_at, :between => ['9:00am', '5:00pm'] # On or after 9:00AM and on or before 5:00PM
validates_time :booked_at, :between => '9:00am'..'5:00pm' # The same as previous example
validates_time :booked_at, :between => '9:00am'...'5:00pm' # On or after 9:00AM and strictly before 5:00PM

validates_time :breakfast_time, :on_or_after => '6:00am',
                                :on_or_after_message => 'must be after opening time',
                                :before => :lunchtime,
                                :before_message => 'must be before lunch time'

Upvotes: 2

Yule
Yule

Reputation: 9764

You're almost there, you just need to explitly say that nil is valid

class TimeValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return true unless value #nil value is valid
    Time.parse(value.to_s)
  rescue ArgumentError
    record.errors[attribute] << (options[:message] || "is not a valid time")
  end
end

Upvotes: 1

Related Questions