11

I've recently been looking for a regular expression to do some client side date checking, and I haven't been able to find one that can satisfy the following criteria:

  • Has a range from 1800 - Now
  • Performs proper date checking with leap years
  • MM/DD/YYYY Form
  • Invalid Date Checking

(These constraints were outside of my scope and are a requirement as per the client, despite my efforts to convince them this wasn't the best route)

Current code:

$('input').keyup(function()
{
       var regex = /^(?:(0[1-9]|1[012])[\/.](0[1-9]|[12][0-9]|3[01])[\/.](18|19|20)[0-9]{2})$/;
       $(this).toggleClass('invalid',!regex.test($(this).val()));    
});

Update:

I should note that this is primarily to see if a regular expression like this would be possible (as the use of a Regex is not my choice in this matter). I am aware of the other (and better) options for validating a date, however as previously mentioned - this is to see if it was possible through a regular expression.

Rion Williams
  • 74,820
  • 37
  • 200
  • 327
  • 3
    Why would you use regex instead of the `Date` class? – Demian Brecht Dec 27 '11 at 18:25
  • 1
    Is Feb. 30 valid in non-leap years? ;-P – Álvaro González Dec 27 '11 at 18:30
  • Regular expressions are not the best tool for many jobs. a Regular Expression for this would have to consider years, and implement the leap year calculation manually, which would triple the size of the regex? – McKay Dec 27 '11 at 18:31
  • 1
    A regular expression that can validate dates including leap years? That would impress me.... – Šime Vidas Dec 27 '11 at 18:32
  • Your current regex does not currently handle other invalid dates like Apr 31, and has a larger range than you specify ("range 1800 - Now) as it actually allows through the end of the century. What is it you actually want? – McKay Dec 27 '11 at 18:34
  • @ŠimeVidas Oh a regex like that is possible, just dumb. – McKay Dec 27 '11 at 18:37
  • [Now you have two problems](http://www.codinghorror.com/blog/2008/06/regular-expressions-now-you-have-two-problems.html). – cmbuckley Dec 27 '11 at 18:40
  • As mentioned in the update - This is primarily just to see if a regex like the one mentioned would be feasible, and to see what one would look like. (As it is obviously not the best method of solving a problem like this) – Rion Williams Dec 27 '11 at 18:47
  • @ŠimeVidas I hope I have impressed? My original guesstimate of triple length was short sighted, it is closer to 5 times the original length. – McKay Dec 27 '11 at 18:51
  • Sometimes I think people lose sight of the objective to have reliable, readable, maintainable code and that fewer lines is NOT always better. A regex makes no sense here at all, other than as a throw-away coding puzzle. I'd rather spend my time on coding puzzles that might discover something useful. – jfriend00 Dec 27 '11 at 18:57
  • 1
    Yeah, it was not a "useful" exercise. – McKay Dec 27 '11 at 19:00
  • For code golf, this is an interesting exercise. For anything else, please don't use a regular expression. It is not possible because of things like leap seconds and leap years are irregular too. The syntax for formatting ISO 8601/RFC 3339 is well defined, follow that, do not invent your own syntax. For a similar case of inappropriately trying to use regular expressions, see https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags – awwright Apr 27 '20 at 07:32

13 Answers13

30

As is mentioned elsewhere, regular expressions almost certanily not what you want. But, having said that, if you really want a regular expression, here is how it is built:

31 day months

(0[13578]|1[02])[\/.](0[1-9]|[12][0-9]|3[01])[\/.](18|19|20)[0-9]{2}

30 day months

(0[469]|11)[\/.](0[1-9]|[12][0-9]|30)[\/.](18|19|20)[0-9]{2}

February 1-28 always valid

(02)[\/.](0[1-9]|1[0-9]|2[0-8])[\/.](18|19|20)[0-9]{2}

February 29 also valid on leap years

(02)[\/.]29[\/.](((18|19|20)(04|08|[2468][048]|[13579][26]))|2000)

which means it would be this if you put it all together:

((0[13578]|1[02])[\/.](0[1-9]|[12][0-9]|3[01])[\/.](18|19|20)[0-9]{2})|((0[469]|11)[\/.](0[1-9]|[12][0-9]|30)[\/.](18|19|20)[0-9]{2})|((02)[\/.](0[1-9]|1[0-9]|2[0-8])[\/.](18|19|20)[0-9]{2})|((02)[\/.]29[\/.](((18|19|20)(04|08|[2468][048]|[13579][26]))|2000))

This version is a little shorter, but a little harder to understand.

((0[13578]|1[02])[\/.]31[\/.](18|19|20)[0-9]{2})|((01|0[3-9]|1[1-2])[\/.](29|30)[\/.](18|19|20)[0-9]{2})|((0[1-9]|1[0-2])[\/.](0[1-9]|1[0-9]|2[0-8])[\/.](18|19|20)[0-9]{2})|((02)[\/.]29[\/.](((18|19|20)(04|08|[2468][048]|[13579][26]))|2000))

These scripts are long and unmaintainable. It should be clear that this isn't a good idea, but it is possible.

Caveats:

  • range 1800-2099 (more can be added without too much difficulty, but requires changes in 4-6 disparate places)
  • requires 2 digit months and days (the strictness could be removed from the expression in ~8 places)
  • [\/.] as seperators (8 places)
  • Hasn't been tested (we could check it against all digit combinations and compare with the javascript date function? [proof that we're reinventing the wheel])
ErikE
  • 48,881
  • 23
  • 151
  • 196
McKay
  • 12,334
  • 7
  • 53
  • 76
  • Slightly updated. I found a (performance-only?) bug that I've fixed. Why it's bad to write regular expressions like this. – McKay Dec 27 '11 at 18:55
  • It also has the caveat that it works for dates in the future through 2099 – McKay Dec 27 '11 at 18:55
  • This is what I was looking for - I really just wanted to see what it would look like. Thanks McKay, you clearly are a regex wizard. – Rion Williams Dec 27 '11 at 18:57
  • @ŠimeVidas I haven't actually tested it, but I notice that you aren't using two digit date and months: `console.log( validate( '02/01/1983' ) ); console.log( validate( '02/29/1983' ) ); console.log( validate( '02/29/1984' ) );` – McKay Dec 27 '11 at 18:58
  • @McKay Oh yes, I am impressed `:)` – Šime Vidas Dec 27 '11 at 19:02
  • Updated to shave off some length on the multiples of four. – McKay Dec 27 '11 at 19:03
  • Which brings it into the 4x length range. – McKay Dec 27 '11 at 19:04
  • 1
    There was an extra bracket in the "30 day months" portion so for example `11/30/2012` would not match, nor any 30th day date in a 30-day month. – ErikE Jan 02 '13 at 21:03
  • Why aren't you considering 1800 and 1900 as leap years? You should have something like: ((18|19|20)(00|04|08|[2468][048]|[13579][26])) – despot Aug 13 '13 at 14:53
  • Also if someone is interested, if you want to make the leading 0 optional you should convert the leading "0" to "0?". – despot Aug 13 '13 at 14:54
  • Also, if you want the regex to match only the whole string, you can place ^ before and $ after the regex. – despot Aug 13 '13 at 14:55
  • 2
    @despot - 1800 and 1900 are not leap years. Another reason it's not a good idea to reinvent the wheel, you may not understand the intricacies of the actual forces at play. – McKay Aug 14 '13 at 12:28
  • 1
    And even if you did want to consider 1800 and 1900 as leap years, the leap section could be simplified to `((18|19|20)([02468][048]|[13579][26]))` The tens and ones places have to be about twice as long as the tens and ones in this example, because I had to split out the zero in the tens place, because it has special rules because 1800 and 1900 are not leap years. – McKay Aug 14 '13 at 12:35
  • damn http://science.howstuffworks.com/science-vs-myth/everyday-myths/question50.htm true... – despot Aug 14 '13 at 14:20
  • still the fastest way is using a simpler regex and some custom java code to check for leap years and other cases: http://stackoverflow.com/questions/2149680/regex-date-format-validation-on-java/18252071#18252071 – despot Aug 15 '13 at 11:55
  • @despot Yes, this is not efficient. Don't use this. That's been quite clear in every answer to this question. Regex for date validation is bad. Also, this question is for client code. JavaScript is the language in the OPs case. – McKay Aug 15 '13 at 12:17
  • 1800, 1900 and 2100 are not leap years. The rule is, basically: when the year is divisible by 100, it must also be divisible by 400 to be considered a leap year. This is in order to fix the extra days added by the general rule, since the year has 365.2425 days. – Vinicius Nov 08 '19 at 13:17
  • @Vinicius Yes, that is correct (number of days in a year is not exactly 365.2425, but it does depend on how you calculate it tropically or sidereally, and is close enough) , however, there has already been extensive discussion about this in the comments, so I'm not sure what your comment is trying to achieve. Could you elaborate? – McKay Nov 09 '19 at 22:14
  • @McKay, I'm just clarifying what the rules would be to validate leap years programmatically. Although there really has been some discussion (allow me to disagree about the "extensive" adjective) about it above, the main (and more accurate) explanation is hidden behind an external link reference. Hence, I don't really see my comment being out of place here. – Vinicius Nov 11 '19 at 00:23
  • @Vinicius I see. Thanks for explaining. – McKay Nov 11 '19 at 05:49
  • Shorter version does not match `10/29/2020` and `10/30/2020`, but it should. – luke Mar 17 '21 at 08:35
  • Longer version might be at least 100x faster when you put non-capturing groups `(?:` instead of capturing groups `(`. For about 700 lines regex search time reduced from about `2 seconds` to about `15 milliseconds`. After modification the whole regex will look like this: `(?:(?:0[13578]|1[02])[\/.](?:0[1-9]|[12][0-9]|3[01])[\/.](?:18|19|20)[0-9]{2})|(?:(?:0[469]|11)[\/.](?:0[1-9]|[12][0-9]|30)[\/.](?:18|19|20)[0-9]{2})|(?:(?:02)[\/.](?:0[1-9]|1[0-9]|2[0-8])[\/.](?:18|19|20)[0-9]{2})|(?:(?:02)[\/.]29[\/.](?:(?:(?:18|19|20)(?:04|08|[2468][048]|[13579][26]))|2000))` – luke Mar 17 '21 at 13:20
  • There is explanation what is non-capturing group: https://stackoverflow.com/questions/3512471/what-is-a-non-capturing-group-in-regular-expressions – luke Mar 17 '21 at 13:34
13

I would suggest that you abandon the attempt to use regular expressions for this. You're much better off parsing the date into its constituent parts (month, day, year), and then using numerical comparisons to make sure it's in the proper range.

Better yet, see if the Javascript Date.parse function will do what you want.

Parsing dates with regular expressions is possible, but frustrating. It's hard to get right, the expression is difficult for non-regex wizards to understand (which means it's difficult to prove that the thing is correct), and it is slow compared to other options.

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
7

This is how I would do it:

function validate( input ) {
    var date = new Date( input );
    input = input.split( '/' );   
    return date.getMonth() + 1 === +input[0] && 
           date.getDate() === +input[1] && 
           date.getFullYear() === +input[2];
}

Usage:

validate( '2/1/1983' ) // true
validate( '2/29/1983' ) // false
validate( '2/29/1984' ) // true (1984 is a leap year)

Live demo: http://jsfiddle.net/9QNRx/

Šime Vidas
  • 182,163
  • 62
  • 281
  • 385
2

this regular expression for YYYY-MM-DD format

((18|19|20)[0-9]{2}[\-.](0[13578]|1[02])[\-.](0[1-9]|[12][0-9]|3[01]))|(18|19|20)[0-9]{2}[\-.](0[469]|11)[\-.](0[1-9]|[12][0-9]|30)|(18|19|20)[0-9]{2}[\-.](02)[\-.](0[1-9]|1[0-9]|2[0-8])|(((18|19|20)(04|08|[2468][048]|[13579][26]))|2000)[\-.](02)[\-.]29
Edward Newell
  • 17,203
  • 7
  • 34
  • 36
Kuldeep
  • 21
  • 1
2

Obviously regular expressions are not the ideal way to do this. Also, it's much safer to be working with YYYY-MM-DD (ISO 8601) format, not MM/DD/YYYY.

That said, here's going for the shortest fully-working regular expression for dates from 01/01/1800 to 12/31/2099:

^(((0[1-9]|1[012])\/(?!00|29)([012]\d)|(0[13-9]|1[012])\/(29|30)|(0[13578]|1[02])\/31)\/(18|19|20)\d{2}|02\/29\/((18|19|20)(0[48]|[2468][048]|[13579][26])|2000))$

Length: 162 characters.

Breakdown:

^ # start
  (
    ( # non-leap months & days
      (0[1-9]|1[012])/(?!00|29)([012]\\d) # all months, days 01-28, uses negative lookahead
    |
      (0[13-9]|1[012])/(29|30) # all months except feb, days 29,30
    |
      (0[13578]|1[02])/31 # all 31 day months, day 31 only
    )
    /
    (18|19|20)\\d{2} # all years
  |
    02/29 # leap day
    /
    (
      (18|19|20)(0[48]|[2468][048]|[13579][26]) # leap years not divisible by 100
    |
      2000 # leap years divisible by 100
    )
  )
$ # end

Here's a fiddle that tests all use cases from 00/00/1800 to 99/99/2099.

Also, for more fun, here's another fiddle that generates the lousiest possible regular expression that still works, 1205306 characters long. It looks something like this:

^(01/01/1800|01/02/1800|01/03/1800|...|12/29/2099|12/30/2099|12/31/2099)$
Andrew Macheret
  • 1,367
  • 13
  • 9
1

RegEx to check for valid dates following ISO 8601, SQL standard.

  • Has a range from 1000-9999
  • Checks for Invalid Dates
  • Checks for valid leap year dates
  • Format: YYYY-MM-DD HH:MM:SS
^([1-9]\d{3}[\-.](0[13578]|1[02])[\-.](0[1-9]|[12][0-9]|3[01]) ([01]\d|2[0123]):([012345]\d):([012345]\d))|([1-9]\d{3}[\-.](0[469]|11)[\-.](0[1-9]|[12][0-9]|30) ([01]\d|2[0123]):([012345]\d):([012345]\d))|([1-9]\d{3}[\-.](02)[\-.](0[1-9]|1[0-9]|2[0-8]) ([01]\d|2[0123]):([012345]\d):([012345]\d))|(((([1-9]\d)(0[48]|[2468][048]|[13579][26])|(([2468][048]|[13579][26])00)))[\-.](02)[\-.]29 ([01]\d|2[0123]):([012345]\d):([012345]\d))$
A K
  • 31
  • 4
0
^(((?:(?:1[6-9]|[2-9]\d)?\d{2})(-)(?:(?:(?:0?[13578]|1[02])(-)31)|(?:(?:0?[1,3-9]|1[0-2])(-)(?:29|30))))|(((?:(?:(?:1[6-9]|[2-9]\d)?(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])00))))(-)(?:0?2(-)29))|((?:(?:(?:1[6-9]|[2-9]\d)?\d{2})(-)(?:(?:0?[1-9])|(?:1[0-2]))(-)(?:0[1-9]|1\d|2[0-8]))))$

Please try the above Reg Expression. I tried multiple combinations and found to be working.

Please check if this works for you too.

Format Accepted : YYYY-MM-DD

Year accepted from 1600

-1

This is the RegEx I use for date validation on client-side. It has a range from 1000 to 2999, validates leap years and optionally the time part. Isn't it gorgeous :)

var r = /^(0[1-9]|1\d|2[0-8]|29(?=-\d\d-(?!1[01345789]00|2[1235679]00)\d\d(?:[02468][048]|[13579][26]))|30(?!-02)|31(?=-0[13578]|-1[02]))-(0[1-9]|1[0-2])-([12]\d{3})(\s([01]\d|2[0-3]):([0-5]\d):([0-5]\d))?$/gm;

r.test('20-02-2013 10:01:07'); // true
r.test('29-02-1700');          // false
r.test('29-02-1604 14:01:45'); // true
r.test('29-02-1900 20:10:50'); // false
r.test('31-12-2000');          // true
r.test('31-11-2008 05:05:05'); // false
r.test('29-02-2004 05:01:23'); // true
r.test('24-06-2014 24:10:05'); // false
hex494D49
  • 9,109
  • 3
  • 38
  • 47
-1

I was trying to validate YYYY-MM-DD, where YYYY can be two digit and MM and DD can be one. This is what I came up with. It treats all centuries as leap years.

((\d\d)?\d\d-((0?(1|3|5|7|8)|10|12)-(31|30|[21]\d|0?[1-9])|(0?(4|6|9)|11)-(31|30|[21]\d|0?[1-9])|0?2-((2[0-8]|1\d)|0?[1-9]))|(\d\d)?((0|2|4|6|8)(0|4|8)|(1|3|5|7|9)(2|6))-0?2-29)
Interrobang
  • 16,984
  • 3
  • 55
  • 63
-1

Adding my answer just for sport - otherwise I fully agree with @Jim.

This will match leap years, including the ones with digits fewer or more than 4.

^\d*((((^|0|[2468])[048])|[13579][26])00$)|((0[48]|(^0*|[2468])[048]|[13579][26]))$

A mini test case in Ruby (^ replaced with \A and $ with \Z, because Ruby):

r = /\A\d*((((\A|0|[2468])[048])|[13579][26])00\Z)|((0[48]|(\A0*|[2468])[048]|[13579][26]))\Z/
100000.times do |year|
  leap = year % 4 == 0 && ((year % 100 != 0) || (year % 400 == 0))
  leap_regex = !year.to_s[r].nil?
  if leap != leap_regex
    print 'Assertion broken:', year, leap, leap_regex, "\n"
  end
end
rr-
  • 14,303
  • 6
  • 45
  • 67
-1

Using moment (not regex) I've done the following:

Assuming you have an ISO date as a string value:

var isoDate = '2016-11-10';
var parsedIsoDate = moment(isoDate, ['YYYY-MM-DD'], true).format('YYYY-MM-DD');

if (parsedIsoDate !== isoDate) {
    // Invalid date.
}
mkaj
  • 3,421
  • 1
  • 31
  • 23
-1

Hello Find RegEx for your Requirement

  • Has a range from 1800
  • Now Performs proper date checking with leap years
  • DD/MM/YYYY Format
  • Invalid Date Checking

^(?:(?:31(/)(?:0[13578]|1[02]))\1|(?:(?:29|30)(/)(?:0[13-9]|1[0-2])\2))(?:(?:18|19|20)\d{2})$|^(?:29(/)02\3(?:(?:(?:(?:18|19|20))(?:0[48]|[2468][048]|[13579][26]))))$|^(?:0?[1-9]|1\d|2[0-8])(/)(?:(?:0[1-9])|(?:1[0-2]))\4(?:(?:18|19|20)\d{2})$

enter image description here

Image and debug RegEx At https://www.debuggex.com/

Testing:

  • DD/MM/YYYY
  • 01/12/190 Not Match
  • 29/02/1903 Not Match
  • 37/02/1903 Not Match
  • 09/03/1703 Not Match
  • 09/03/2103 Not Match
  • 09/31/2103 Not Match
  • 29/02/1904 - Match
  • 01/12/1988 - Match
MilapTank
  • 9,988
  • 7
  • 38
  • 53
  • What does this add that existing questions do not already consider? Also, this does not do proper leap year checking. (This also uses a different format than the OP requests) – McKay Nov 11 '19 at 05:45
-2

((0[13578]|1[02])[/.]31/.[0-9]{2})|((01|0[3-9]|1[1-2])/./.[0-9]{2})|((0[1-9]|1[0-2])/./.[0-9]{2})|((02)[/.]29/.)

The short version answer does not work for 10/29 and 10/30 any year the long version does work below is a simple java script program I wrote to test

import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

public class RegxDateTest {

public static void main(String[] args) {
    // String to be scanned to find the pattern.
      String line = "This order was placed for QT3000! OK?";
        String pattern ="((0[13578]|1[02])[\\/.]31[\\/.](18|19|20)[0-9]{2})|((01|0[3-9]|1[1-2])[\\/.](29|30)[\\/.](18|19|20)[0-9]{2})|((0[1-9]|1[0-2])[\\/.](0[1-9]|1[0-9]|2[0-8])[\\/.](18|19|20)[0-9]{2})|((02)[\\/.]29[\\/.](((18|19|20)(04|08|[2468][048]|[13579][26]))|2000))";
      // Create a Pattern object
      Pattern r = Pattern.compile(pattern);
      LocalDate startDate = new LocalDate("1950-01-01");
      LocalDate endDate = new LocalDate("2020-01-01");
      for (LocalDate date = startDate; date.isBefore(endDate); date = date.plusDays(1))
      {
          if (date.toString("MM/dd/yyyy").matches(pattern)) {
             // System.out.println("This date does  match:  " + date.toString("MM/dd/yyyy") );
            }else{
                  System.out.println("This date does not match:  " + date.toString("MM/dd/yyyy") );
            }

      }
      String baddate1="02/29/2016";
      if (baddate1.matches(pattern)) {
          System.out.println("This date does  match:  " + baddate1 );
      }else{
          System.out.println("This date does not match:  " + baddate1 );
      }
      System.out.println("alldone:  "  );

}

}

Joe ONeil
  • 130
  • 1
  • 6