# Beat Pure Regular Expressions at Validating ISO 8601 Dates

In ValiDate ISO 8601 by RX, the challenge was to use only standard regular expressions to validate standard date formats and values (the former is a common job for RX, the latter was unusual). The winning answer used 778 bytes. This challenge is to beat that using any language of your choosing, but without special date functions or classes.

# Challenge

Find the shortest code that

1. validates every possible date in the Proleptic Gregorian calendar (which also applies to all dates before its first adoption in 1582),
2. does not match any invalid date and
3. does not use any predefined functions, methods, classes, modules or similar for handling dates (and times), i.e. relies on string and numeric operations.

## Output

Output is truthy or falsey. It’s not necessary to output or convert the date.

## Input

Input is a single string 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)


# 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 (possibly negative) 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 …-W53-…, 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. 0001-01-01 and 2001-01-01 are Mondays. 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. Separating hyphens - are required in all cases. (These preconditions make it possible for leading + to be always optional.)

# Rules

This is code-golf. The shortest code in bytes wins. 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
+2016-02-29
200000-02-29
-200000-02-29
+200000-02-29
2016-366
2000-366
0000-366
-2000-366
-2016-366
+2016-366
2015-081
2015-W33-1
2015-W53-7
+2015-W53-7
+2015-W33-1
-2015-W33-1
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         # though a valid ISO format we require separators and day-precision
2015-08         # a valid ISO format, but we require day-precision
201508          # a valid but ambiguous ISO format
2015            # a valid ISO format, but we require day-precision


## Invalid dates

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

• off topic, but maybe useful - Stack Overflow: stackoverflow.com/questions/28020805/… (if I shouldn't post that, tell me) – Daniele D Apr 7 '16 at 11:20
• What if the programmer is a YEC (Young-Earth Creationist)? – Leaky Nun Apr 8 '16 at 9:44
• About -0000-08-10 what is exaclty the arbitrary decision? Not allowing the year as negative 0? – edc65 Apr 13 '16 at 10:18
• @edc65 Yes, +0000-08-10 and 0000-08-10 should be used instead. Note, though, that the accepted answer in the regular expression variant of this challenge fails this particular test case, so it’s not really a failing condition (i.e. a should, not a must). – Crissov Apr 13 '16 at 11:09
• @KennyLau Then the programmer is wrong. – Arcturus Apr 13 '16 at 12:54

# JavaScript (ES6), 236

236 bytes allowing negative 0 year (-0000). Returns true or false

s=>!!([,y,w,d]=s.match(/^([+-]?\d{4,})(-W?\d\d)?(-\d{1,3})$/)||[],n=y%100==0&y%400!=0|y%4!=0,l=((l=y-1)+8-~(l/4)+~(l/100)-~(l/400))%7,l=l==5|l==4&!n,+d&&(-w?d>0${2+n}0101001010[~w]-32:w?(w=w.slice(2),w>0&w<(53+l)&d>-8):d[3]&&d>n-367))


Adding the check for negative 0 cut 2 bytes but adds 13. Note that in javascript the numeric value -0 exists, and it is special cased to be equal to 0, but 1/-0 is -Infinity. This versione returns 0 or 1

s=>([,y,w,d]=s.match(/^([+-]?\d{4,})(-W?\d\d)?(-\d{1,3})$/)||[],n=y%100==0&y%400!=0|y%4!=0,l=((l=y-1)+8-~(l/4)+~(l/100)-~(l/400))%7,l=l==5|l==4&!n,+d&&(-w?d>0${2+n}0101001010[~w]-32:w?(w=w.slice(2),w>0&w<(53+l)&d>-8):d[3]&&d>n-367))&!(!+y&1/y<0)


Test

Check=
s=>!! // to obtain a true/false
(
// parse year in y, middle part in w, day in d
// day will be negative with 1 or 3 numeric digits and could be 0
// week will be '-W' + 2 digits
// month will be negative with2 digits and could be 0
// if the date is in format yyyy-ddd, then w is empty
[,y,w,d] = s.match(/^([+-]?\d{4,})(-W?\d\d)?(-\d{1,3})$/) || [], n = y%100==0 & y%400!=0 | y%4!=0, // n: not leap year l = ((l=y-1) + 8 -~(l/4) +~(l/100) -~(l/400)) % 7, l = l==5| l==4 & !n, // l: long year (see http://mathforum.org/library/drmath/view/55837.html) +d && ( // if d is not empty and not 0 -w // if w is numeric and not 0, then it's the month (negative) ? d > 0${2+n}0101001010[~w] - 32 // check month length (for leap year too)
: w // if w is not empty, then it's the week ('-Wnn')
? ( w = w.slice(2), w > 0 & w < (53+l) & d >- 8) // check long year too
: d[3] && d > n-367 // else d is the prog day, has to be 3 digits and < 367 o 366
)
)

console.log=x=>O.textContent += x +'\n'

OK=['1900-01-01','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','+2016-02-29','200000-02-29'
,'-200000-02-29','+200000-02-29','2016-366','2000-366','0000-366'
,'-2000-366','-2016-366','+2016-366','2015-081','2015-W33-1'
,'2015-W53-7','+2015-W53-7','+2015-W33-1','-2015-W33-1','2015-08-10']

KO=['-0000-08-10','15-08-10','2015-8-10','015-08-10','20150810','2015 08 10'
,'2015.08.10','2015–08–10','2015-0810','201508-10','2015 - 08 - 10','2015-w33-1'
,'2015W33-1','2015W331','2015-W331','2015-W33','2015W33','2015-08','201508'
,'2015','2015-00-10','2015-13-10','2015-08-00','2015-08-32','2015-04-31'
,'2015-02-30','2015-02-29','2100-02-29','-2100-02-29','2015-000'
,'2015-366','2016-367','2100-366','-2100-366','2015-W00-1'
,'2015-W54-1','2016-W53-1','2015-W33-0','2015-W33-8']

console.log('Valid')
OK.forEach(x=>console.log(Check(x)+' '+x))
console.log('Not valid')
KO.forEach(x=>console.log(Check(x)+' '+x))
<pre id=O></pre>