Challenge
Find the shortest regex that
- validates, i.e. matches, every possible date in the Proleptic Gregorian calendar (which also applies to all dates before its first adoption in 1582) and
- does not match any invalid date.
Output
Output is therefore truthy or falsey.
Input
Input is in any of 3 expanded ISO 8601 date formats – no times.
The first two are ±YYYY-MM-DD
(year, month, day) and ±YYYY-DDD
(year, day). Both need special-casing for the leap day. They are naively matched separately by these extended RXs:
(?<year>[+-]?\d{4,})-(?<month>\d\d)-(?<day>\d\d)
(?<year>[+-]?\d{4,})-(?<doy>\d{3})
The third input format is ±YYYY-wWW-D
(year, week, day). It is the complicated one because of the complex leap week pattern.
(?<year>-?\d{4,})-W(?<week>\d\d)-(?<dow>\d)
A basic, but insufficient validity check for all three combined would look something like this:
[+-]?\d{4,}-((0\d|1[0-2])-([0-2]\d|3[01]) ↩
|([0-2]\d\d|3[0-5]\d|36[0-6]) ↩
|(W([0-4]\d|5[0-3])-[1-7]))
Conditions
A leap year in the Proleptic Gregorian calendar contains the leap day …-02-29
and thus it is 366 days long, hence …-366
exists. This happens in any year whose ordinal number is divisible by 4, but not by 100 unless it’s also divisible by 400.
Year zero exists in this calendar and it is a leap year.
A long year in the ISO week calendar contains a 53rd week, which one could term a “leap week”. This happens in all years where 1 January is a Thursday and additionally in all leap years where it’s a Wednesday. It turns out to occur every 5 or 6 years usually, in a seemingly irregular pattern.
A year has at least 4 digits. Years with more than 10 digits do not have to be supported, because that’s close enough to the age of the universe (ca. 14 billion years). The leading plus sign is optional, although the actual standard suggests it should be required for years with more than 4 digits.
Partial or truncated dates, i.e. with less than day-precision, must not be accepted.
The parts of the date notation, e.g. the month, do not have to be matched by a group that could be referenced.
Rules
This is code-golf. The shortest regex without executed code wins. Update: You may use features like recursion and balanced groups, but will be fined by a factor of 10, which the character count then is multiplied with! This is now different from the rules in Hard code golf: Regex for divisibility by 7. Earlier answer wins a tie.
Test cases
Valid tests
2015-08-10
2015-10-08
12015-08-10
-2015-08-10
+2015-08-10
0015-08-10
1582-10-10
2015-02-28
2016-02-29
2000-02-29
0000-02-29
-2000-02-29
-2016-02-29
200000-02-29
2016-366
2000-366
0000-366
-2016-366
-2000-366
2015-081
2015-W33-1
2015-W53-7
2015-08-10
The last one is optionally valid, i.e. leading and trailing spaces in input strings may be trimmed.
Invalid formats
-0000-08-10 # that's an arbitrary decision
15-08-10 # year is at least 4 digits long
2015-8-10 # month (and day) is exactly two digits long, i.e. leading zero is required
015-08-10 # year is at least 4 digits long
20150810 # though a valid ISO format, we require separators; could also be interpreted as a 8-digit year
2015 08 10 # separator must be hyphen-minus
2015.08.10 # separator must be hyphen-minus
2015–08–10 # separator must be hyphen-minus
2015-0810
201508-10 # could be October in the year 201508
2015 - 08 - 10 # no internal spaces allowed
2015-w33-1 # letter ‘W’ must be uppercase
2015W33-1 # it would be unambiguous to omit the separator in front of a letter, but not in the standard
2015W331 # though a valid ISO format we require separators
2015-W331
2015-W33 # a valid ISO date, but we require day-precision
2015W33
Invalid dates
2015 # a valid ISO format, but we require day-precision
2015-08 # a valid ISO format, but we require day-precision
2015-00-10 # month range is 1–12
2015-13-10 # month range is 1–12
2015-08-00 # day range is 1–28 through 31
2015-08-32 # max. day range is 1–31
2015-04-31 # day range for April is 1–30
2015-02-30 # day range for February is 1–28 or 29
2015-02-29 # day range for common February is 1–28
2100-02-29 # most century years are non-leap
-2100-02-29 # most century years are non-leap
2015-000 # day range is 1–365 or 366
2015-366 # day range is 1–365 in common years
2016-367 # day range is 1–366 in leap years
2100-366 # most century years are non-leap
-2100-366 # most century years are non-leap
2015-W00-1 # week range is 1–52 or 53
2015-W54-1 # week range is 1–53 in long years
2016-W53-1 # week range is 1–52 in short years
2015-W33-0 # day range is 1–7
2015-W33-8 # day range is 1–7