128

I have a string: "31-02-2010" and want to check whether or not it is a valid date. What is the best way to do it?

I need a method which which returns true if the string is a valid date and false if it is not.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Salil
  • 46,566
  • 21
  • 122
  • 156

15 Answers15

125
require 'date'
begin
   Date.parse("31-02-2010")
rescue ArgumentError
   # handle invalid date
end
Dennis
  • 56,821
  • 26
  • 143
  • 139
mpd
  • 2,223
  • 1
  • 19
  • 23
  • 30
    It's not a Date class feature, it's exception handling. – Pier-Olivier Thibault Aug 09 '12 at 21:02
  • 4
    Date.parse('2012-08-13== 00:00:00') # => Mon, 13 Aug 2012 – Bob Aug 14 '12 at 18:02
  • 2
    exceptions should be used only when you don't really expect an error – Arnis Lapsa Sep 16 '12 at 13:11
  • 14
    Using a "catch-it-all" rescue should be considered an anti-pattern. It can hide out other errors which we don't expect and make the debugging of the code extremely difficult. – yagooar Jan 29 '13 at 10:32
  • 9
    So... if it is indeed an anti-pattern, how would you answer the question? – Matthew Brown Apr 12 '13 at 04:35
  • 1
    As bad as it looks with the exception handling this is how the default Rails validators are written as well: i.e. https://github.com/rails/rails/blob/3c21237c37fed9d726bcf2816bf1adc4635f4956/activemodel/lib/active_model/validations/numericality.rb#L65 – mrt Oct 23 '13 at 13:02
  • 17
    Sorry this is a wrong answer. It doesn't check whether or not a string is a valid date, it merely checks of Date.parse can parse the string. Date.parse seems to be very forgiving when it comes to dates, e.g. it will parse "FOOBAR_09_2010" as the date 2012-09-09. – n13 Nov 04 '13 at 12:00
  • is there any valid_date? method for above solution because it gives error when I do Date.valid_date?(Date.parse('2015-07-13')) since parsed object is in the different date format – Ankita.P Jul 12 '15 at 19:20
  • Using error trapping as logic is really, really bad design. –  Dec 05 '16 at 14:32
  • @n13 points out, this one will bite you. It says explicitly in the docs that this is _not_ a validator[1]. Try typing `Date.parse('Monterey')` into IRB, you will get a valid date. 1: https://ruby-doc.org/stdlib-2.6.1/libdoc/date/rdoc/Date.html#method-c-_parse – oneWorkingHeadphone Mar 07 '19 at 00:20
  • As oneworkingheadphone said, that might lead you to some unexpected behaviour such as `Date.parse("jane mary") => Sun, 01 Jan 2023 ` or `"mary".to_date => Wed, 01 Mar 2023` – Marcelo Xavier Aug 17 '23 at 13:53
60

Here is a simple one liner:

DateTime.parse date rescue nil

I probably wouldn't recommend doing exactly this in every situation in real life as you force the caller to check for nil, eg. particularly when formatting. If you return a default date|error it may be friendlier.

the Tin Man
  • 158,662
  • 42
  • 215
  • 303
robert_murray
  • 2,070
  • 17
  • 9
59
d, m, y = date_string.split '-'
Date.valid_date? y.to_i, m.to_i, d.to_i
Max Schulze
  • 1,173
  • 7
  • 8
Sohan
  • 3,757
  • 2
  • 24
  • 24
  • This is pretty simple and good for enforcing a xx-xx-YYYY format – n13 Nov 04 '13 at 12:02
  • If you want to enforce a strict single format date string option, then this is the best option as it avoids Exceptions and is deterministic. Date.valid_date? is the method to use at least for Ruby 2. http://ruby-doc.org/stdlib-2.0.0/libdoc/date/rdoc/Date.html#method-c-valid_date-3F – Ashley Raiteri Nov 12 '13 at 05:38
  • 4
    What version of Ruby supports `is_valid?`? Tried 1.8, 2.0 and 2.1. None of them seem to have that one. All seem to have `valid_date?`, though. – Henrik N Jan 04 '14 at 17:58
30

Parsing dates can run into some gotcha's, especially when they are in a MM/DD/YYYY or DD/MM/YYYY format, such as short dates used in U.S. or Europe.

Date#parse attempts to figure out which to use, but there are many days in a month throughout the year when ambiguity between the formats can cause parsing problems.

I'd recommend finding out what the LOCALE of the user is, then, based on that, you'll know how to parse intelligently using Date.strptime. The best way to find where a user is located is to ask them during sign-up, and then provide a setting in their preferences to change it. Assuming you can dig it out by some clever heuristic and not bother the user for that information, is prone to failure so just ask.

This is a test using Date.parse. I'm in the U.S.:

>> Date.parse('01/31/2001')
ArgumentError: invalid date

>> Date.parse('31/01/2001') #=> #<Date: 2001-01-31 (4903881/2,0,2299161)>

The first was the correct format for the U.S.: mm/dd/yyyy, but Date didn't like it. The second was correct for Europe, but if your customers are predominately U.S.-based, you'll get a lot of badly parsed dates.

Ruby's Date.strptime is used like:

>> Date.strptime('12/31/2001', '%m/%d/%Y') #=> #<Date: 2001-12-31 (4904549/2,0,2299161)>
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • Your LOCALE seems off. I get this >> Date.parse('01/31/2001') => Wed, 31 Jan 2001 >> ?> Date.parse('31/01/2001') ArgumentError: invalid date – hoyhoy Aug 25 '11 at 18:35
  • Not sure if I'm being daft here, but this seems to explain only how to parse a date, not how to validate it. The accepted answer uses the ArgumentError exception, but that doesn't seem like a good idea, for reasons noted in a comment there. – cesoid Nov 06 '14 at 20:43
  • There is a known situation where you can't validate a date. It's where the day and month values are ambiguous, and you have to know the format of the incoming date string. And, that is what the answer is saying. If it looks like `01/01/2014`, which is the month and which is the day? In the US month would be first, the rest of the world it'd be second. – the Tin Man Nov 06 '14 at 21:00
  • The ambiguity that prevents you from validating the date only does so to the extent that it also prevents you from parsing the date, but you've made a good attempt at parsing it with known information. It would be nice to use some of what you have to try to validate as best as we can. Can you think of how to do that without using exceptions that Date.parse and Date.strptime create? – cesoid Aug 14 '15 at 15:01
  • Date parsing in "mm/dd/yyyy" or "dd/mm/yyyy" format *REQUIRES* foreknowledge of the date's format. Without that we can't determine which it is unless we have a sampling from one source/user, then parse using both forms and then see which form generated the most exceptions and use the other. This breaks down when parsing dates from multiple sources, especially when they're global or they've been entered by hand. The only accurate way to deal with dates is to not allow hand-entry, to force users to use date-pickers, or only allow ISO date formats that are unambiguous. – the Tin Man Aug 14 '15 at 16:12
  • Arguably `01/31/2001` is an invalid date and parse is working as intended. – Qwertie Apr 08 '19 at 03:48
29

Date.valid_date? *date_string.split('-').reverse.map(&:to_i)

Samer Buna
  • 8,821
  • 9
  • 38
  • 55
  • Excellent, thanks. This one seems to be available at least in 1.8, 2.0 and 2.1. Note that you need to `require "date"` first or you'll get "undefined method". – Henrik N Jan 04 '14 at 17:59
  • 2
    This method can raise an exception 'ArgumentError: wrong number of arguments' instead of returning false. For example if your string contains slashes instead of dashes – vincentp Jun 01 '15 at 07:43
  • 2
    Maybe we can do something like this, for handling bad formatted date : `Date.valid_date? *Array.new(3).zip(date_string.split('-')).transpose.last.reverse.map(&:to_i)` – vincentp Jun 01 '15 at 08:25
11

I'd like to extend Date class.

class Date
  def self.parsable?(string)
    begin
      parse(string)
      true
    rescue ArgumentError
      false
    end
  end
end

example

Date.parsable?("10-10-2010")
# => true
Date.parse("10-10-2010")
# => Sun, 10 Oct 2010
Date.parsable?("1")
# => false
Date.parse("1")
# ArgumentError: invalid date from (pry):106:in `parse'
ironsand
  • 14,329
  • 17
  • 83
  • 176
  • I like this method the best, personally. In my app, I opted to override String: https://stackoverflow.com/a/64190303/987115 – csalvato Oct 04 '20 at 01:05
6

Another way to validate date:

date_hash = Date._parse(date.to_s)
Date.valid_date?(date_hash[:year].to_i,
                 date_hash[:mon].to_i,
                 date_hash[:mday].to_i)
Slava Zharkov
  • 237
  • 3
  • 7
  • 3
    Building on top of that, you can simplify this to: `Date.valid_date?(*Date._parse(date).values)` – Sunny Oct 13 '20 at 08:28
4

A stricter solution

It's easier to verify the correctness of a date if you specify the date format you expect. However, even then, Ruby is a bit too tolerant for my use case:

Date.parse("Tue, 2017-01-17", "%a, %Y-%m-%d") # works
Date.parse("Wed, 2017-01-17", "%a, %Y-%m-%d") # works - !?

Clearly, at least one of these strings specifies the wrong weekday, but Ruby happily ignores that.

Here's a method that doesn't; it validates that date.strftime(format) converts back to the same input string that it parsed with Date.strptime according to format.

module StrictDateParsing
  # If given "Tue, 2017-01-17" and "%a, %Y-%m-%d", will return the parsed date.
  # If given "Wed, 2017-01-17" and "%a, %Y-%m-%d", will error because that's not
  # a Wednesday.
  def self.parse(input_string, format)
    date = Date.strptime(input_string, format)
    confirmation = date.strftime(format)
    if confirmation == input_string
      date
    else
      fail InvalidDate.new(
        "'#{input_string}' parsed as '#{format}' is inconsistent (eg, weekday doesn't match date)"
      )
    end
  end

  InvalidDate = Class.new(RuntimeError)
end
Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • That's exactly what I was looking for. Thank you! – Lucas Caton May 02 '18 at 09:20
  • this fails for the cases like "2019-1-1" which is a valid date but the strftime makes it 2019-01-01 – saGii May 23 '19 at 20:52
  • @saGii that doesn't fit the format specified (iso8601 - see `Date.today().iso8601`); `%m` is zero-padded. If you want something different, see https://ruby-doc.org/stdlib-2.4.0/libdoc/date/rdoc/Date.html#method-c-strptime – Nathan Long May 24 '19 at 17:19
3

Posting this because it might be of use to someone later. No clue if this is a "good" way to do it or not, but it works for me and is extendible.

class String

  def is_date?
  temp = self.gsub(/[-.\/]/, '')
  ['%m%d%Y','%m%d%y','%M%D%Y','%M%D%y'].each do |f|
  begin
    return true if Date.strptime(temp, f)
      rescue
       #do nothing
    end
  end

  return false
 end
end

This add-on for String class lets you specify your list of delimiters in line 4 and then your list of valid formats in line 5. Not rocket science, but makes it really easy to extend and lets you simply check a string like so:

"test".is_date?
"10-12-2010".is_date?
params[:some_field].is_date?
etc.
Dave Sanders
  • 3,135
  • 4
  • 33
  • 43
3

Try regex for all dates:

/(\d{1,2}[-\/]\d{1,2}[-\/]\d{4})|(\d{4}[-\/]\d{1,2}[-\/]\d{1,2})/.match("31-02-2010")

For only your format with leading zeroes, year last and dashes:

/(\d{2}-\d{2}-\d{4})/.match("31-02-2010")

the [-/] means either - or /, the forward slash must be escaped. You can test this on http://gskinner.com/RegExr/

add the following lines, they will all be highlighted if you use the first regex, without the first and last / (they are for use in ruby code).

2004-02-01
2004/02/01
01-02-2004
1-2-2004
2004-2-1
MrFox
  • 4,852
  • 7
  • 45
  • 81
1

You can try the following, which is the simple way:

"31-02-2010".try(:to_date)

But you need to handle the exception.

kellanburket
  • 12,250
  • 3
  • 46
  • 73
1

Similar to the solution by @ironsand, I prefer to create an overridden instance method on String:

class String
  def valid_datetime?
    to_datetime
    true
  rescue ArgumentError
    false
  end
end
csalvato
  • 490
  • 5
  • 13
0
require 'date'
#new_date and old_date should be String
# Note we need a ()
def time_between(new_date, old_date)
  new_date = (Date.parse new_date rescue nil)
  old_date = (Date.parse old_date rescue nil)
  return nil if new_date.nil? || old_date.nil?
  (new_date - old_date).to_i
end

puts time_between(1,2).nil?
#=> true
puts time_between(Time.now.to_s,Time.now.to_s).nil?
#=> false
sander
  • 21
  • 3
0

Date.parse not raised exception for this examples:

Date.parse("12!12*2012")
=> Thu, 12 Apr 2018

Date.parse("12!12&2012")
=> Thu, 12 Apr 2018

I prefer this solution:

Date.parse("12!12*2012".gsub(/[^\d,\.,\-]/, ''))
=> ArgumentError: invalid date

Date.parse("12-12-2012".gsub(/[^\d,\.,\-]/, ''))
=> Wed, 12 Dec 2012

Date.parse("12.12.2012".gsub(/[^\d,\.,\-]/, ''))
=> Wed, 12 Dec 2012
Igor Biryukov
  • 270
  • 4
  • 15
-1

Method:

require 'date'
def is_date_valid?(d)
  Date.valid_date? *"#{Date.strptime(d,"%m/%d/%Y")}".split('-').map(&:to_i) rescue nil
end

Usage:

config[:dates].split(",").all? { |x| is_date_valid?(x)}

This returns true or false if config[:dates] = "12/10/2012,05/09/1520"