Regular Expressions 101

Community Patterns

Strong Matcher for ISO 8601 / RFC 3339 Date Times; rejects bad TZ offsets, illegal times (Museum of Bad Data)

1

Regular Expression
ECMAScript (JavaScript)

/
(?=(?:^(?:\d\d\d\d-(?:0[1-9]|10|11|12)-(?:0[1-9]|1[0-9]|2[0-8])|\d\d\d\d-(?:0[13-9]|10|11|12)-(?:29|30)|\d\d\d\d-(?:0[13578]|10|12)-31|(?:\d\d[2468][048]|\d\d0[48]|\d\d[13579][26])-02-29|(?:[02468][048]00|[13579][26]00)-02-29)T(?:(?:0[0-9]|1[0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9]))(?:\.\d\d\d)?(?:Z|[\+\-](?:0[0-9]|1[012]):00|\+0[34569]:30|\+10:30|-0[39]:30|\+1[34]:00|\+0[58]:45|\+12:45)$)|^(?:1972|198[1235]|199[2347]|2012|2015)-06-30T23:59:60Z$|^(?:197[2-9]|1987|1989|199[058]|2005|2008|2016)-12-31T23:59:60Z$)(?!.*-00:00$)^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.(\d\d\d))?((Z)|([\+\-])(\d\d):(\d\d))$
/
gm

Description

Handles many nuanced cases around time zone offsets, leap seconds and leap days.

References:

Pattern

(?=
  (?:^
    (?: # All non-leap-second YYYY-MM-DD parts:
      \d\d\d\d-(?:0[1-9]  |10|11|12)-(?:0[1-9]|1[0-9]|2[0-8])   # Days 01-28
    | \d\d\d\d-(?:0[13-9] |10|11|12)-(?:29|30)                  # Days 29+30
    | \d\d\d\d-(?:0[13578]|10   |12)-31                         # Day 31
    | (?:\d\d[2468][048]|\d\d0[48]|\d\d[13579][26])-02-29       # leap years not divisible by 100
    | (?:[02468][048]00|[13579][26]00)-02-29                    # leap years divisible by 400
    )
    T
    (?:(?:0[0-9]|1[0-9]|2[0-3]):(?:[0-5][0-9]):(?:[0-5][0-9]))  # time part
    (?:\.\d\d\d)?                                               # ms part (optional)
    (?:Z|[\+\-](?:0[0-9]|1[012]):00|\+0[34569]:30|\+10:30|-0[39]:30|\+1[34]:00|\+0[58]:45|\+12:45)
  $)
  |^(?:1972    |198[1235]|199[2347]|2012|2015     )-06-30T23:59:60Z$ # all june leapsecs
  |^(?:197[2-9]|1987|1989|199[058] |2005|2008|2016)-12-31T23:59:60Z$ # all december leapsecs
)
(?!.*-00:00$) # if given a -00:00 offset reject unconditionally, psychhhh
# OK! Since only valid times are now possible, we can use a loose pattern match to parse.
^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)(?:\.(\d\d\d))?((Z)|([\+\-])(\d\d):(\d\d))$

Test cases

//	accept: Exemplars
2008-02-03T04:05:06.007Z
2008-02-03T04:05:06Z
0000-01-01T00:00:00.000Z
9999-12-31T59:59:59.999Z
9999-12-31T59:59:59Z
2008-02-03T04:05:06.007+12:45
2008-02-03T04:05:06+03:30
0000-01-01T00:00:00.007-09:30
0000-02-29T04:05:06.007+14:00
9999-12-31T23:59:59.999+12:00
9999-12-31T23:59:59-12:00
//	accept: Leap day, year is multiple of four
0004-02-29T04:05:06Z
0096-02-29T04:05:06Z
1560-02-29T04:05:06Z
2004-02-29T04:05:06Z
2020-02-29T04:05:06Z
2032-02-29T04:05:06Z
9996-02-29T04:05:06Z
//	accept: Leap day, year is Multiple of 400
0000-02-29T04:05:06Z
1200-02-29T04:05:06Z
1600-02-29T04:05:06Z
2000-02-29T04:05:06Z
3600-02-29T04:05:06Z
8000-02-29T04:05:06Z
9600-02-29T04:05:06Z
//	accept: Day in range for month
2008-01-30T04:05:06Z
2008-03-30T04:05:06Z
2008-04-30T04:05:06Z
2008-05-30T04:05:06Z
2008-06-30T04:05:06Z
2008-07-30T04:05:06Z
2008-08-30T04:05:06Z
2008-09-30T04:05:06Z
2008-10-30T04:05:06Z
2008-11-30T04:05:06Z
2008-12-30T04:05:06Z
2008-01-31T04:05:06Z
2008-02-31T04:05:06Z
2008-03-31T04:05:06Z
2008-05-31T04:05:06Z
2008-07-31T04:05:06Z
2008-08-31T04:05:06Z
2008-10-31T04:05:06Z
2008-12-31T04:05:06Z
//	accept: leap second
1972-06-30T23:59:60Z
1981-06-30T23:59:60Z
1982-06-30T23:59:60Z
1983-06-30T23:59:60Z
1985-06-30T23:59:60Z
1992-06-30T23:59:60Z
1993-06-30T23:59:60Z
1994-06-30T23:59:60Z
1997-06-30T23:59:60Z
2012-06-30T23:59:60Z
2015-06-30T23:59:60Z
1972-12-31T23:59:60Z
1973-12-31T23:59:60Z
1974-12-31T23:59:60Z
1975-12-31T23:59:60Z
1976-12-31T23:59:60Z
1977-12-31T23:59:60Z
1978-12-31T23:59:60Z
1979-12-31T23:59:60Z
1987-12-31T23:59:60Z
1989-12-31T23:59:60Z
1990-12-31T23:59:60Z
1995-12-31T23:59:60Z
1998-12-31T23:59:60Z
2005-12-31T23:59:60Z
2008-12-31T23:59:60Z
2016-12-31T23:59:60Z
//	REJECT: Out of range
10000-02-29T04:05:06Z
2008-00-30T04:05:06Z
2008-13-30T04:05:06Z
2008-02-30T04:05:06Z
2008-04-31T04:05:06Z
2008-06-31T04:05:06Z
2008-09-31T04:05:06Z
2008-11-31T04:05:06Z
2008-12-32T04:05:06Z
2008-12-99T04:05:06Z
2008-12-00T04:05:06Z
//	REJECT: Hour/min/sec out of range
2008-12-08T60:05:06Z
2008-12-08T04:60:06Z
2008-12-08T04:99:06Z
9999-12-31T59:59:61.999Z
2008-02-03T04:05:61.999Z
2008-02-03T04:05:61Z
//	REJECT: Negative dates not accepted
-2000-02-29T04:05:06Z
//	REJECT: Malformed
2008-02-0304:05:06Z
20080203T040506Z
2008-02-03T04:05:06007Z
2008-02-03T04:05:06.7Z
2008-02-03T04:05:06.07Z
2008-02-03T04:05:06.0007Z
2008-02-03T04:05:06
2008-02-03T04:05:06.Z
//	REJECT: no leap second
1978-06-30T23:59:60Z
//	REJECT: assumes no future leap seconds
2024-12-31T23:59:60Z
2099-12-31T23:59:60Z
9999-12-31T23:59:60Z
//	REJECT: leap seconds only in UTC format
2005-12-31T23:59:60+00:00
2008-12-31T23:59:60+00:00
//	REJECT: Not a leap day, not a multiple of four
2003-02-29T04:05:06Z
2037-02-29T04:05:06Z
2038-02-29T04:05:06Z
2039-02-29T04:05:06Z
9997-02-29T04:05:06Z
9998-02-29T04:05:06Z
9999-02-29T04:05:06Z
//	REJECT: Not a leap day, mult of 100
0100-02-29T04:05:06Z
1000-02-29T04:05:06Z
2100-02-29T04:05:06Z
2200-02-29T04:05:06Z
3500-02-29T04:05:06Z
3700-02-29T04:05:06Z
Submitted by Philip Flip Kromer (@mrflip) - 10 months ago