Regular Expression | Leap Years and More

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.

Answers:

Answer

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])
Answer

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.

Answer

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)$
Answer

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
Answer

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)
Answer

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
Answer

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.
}
Answer

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
Answer

((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:  "  );

}

}

Answer

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/

Answer

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
Answer
^(((?:(?: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

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.