diff --git a/src/lib/isISO8601.js b/src/lib/isISO8601.js index 6eea9ae27..d0e92d047 100644 --- a/src/lib/isISO8601.js +++ b/src/lib/isISO8601.js @@ -6,6 +6,20 @@ const iso8601 = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9] // same as above, except with a strict 'T' separator between date and time const iso8601StrictSeparator = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W(0[1-9]|[1-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[0-6])))([T]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([\.,]\d+(?!:))?)?(\17[0-5]\d([\.,]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; /* eslint-enable max-len */ +// Returns true iff ISO week-numbering year `year` has a 53rd week, i.e. 31 +// December of that year falls within ISO week 53 rather than week 1 of the +// following year. Only week 53 is year-dependent -- weeks 01-52 always exist +// -- and the main iso8601 regex already constrains the week field to 01-53 +// syntactically, so this is the only additional check strict mode needs. +const hasISOWeek53 = (year) => { + const dec31 = new Date(Date.UTC(year, 11, 31)); + const dayNum = dec31.getUTCDay() || 7; // Sunday (0) -> 7, so Mon=1..Sun=7 + dec31.setUTCDate(dec31.getUTCDate() + (4 - dayNum)); // Thursday of the same ISO week + const yearStart = new Date(Date.UTC(dec31.getUTCFullYear(), 0, 1)); + const week = Math.ceil((((dec31 - yearStart) / 86400000) + 1) / 7); + return week === 53; +}; + const isValidDate = (str) => { // str must have passed the ISO8601 check // this check is meant to catch invalid dates @@ -19,6 +33,13 @@ const isValidDate = (str) => { if ((oYear % 4 === 0 && oYear % 100 !== 0) || oYear % 400 === 0) return oDay <= 366; return oDay <= 365; } + // then check for week dates (YYYY-Www or YYYY-Www-D, basic or extended) + const weekMatch = str.match(/^([\+-]?\d{4})-?W(\d{2})/); + if (weekMatch) { + const wYear = Number(weekMatch[1]); + const week = Number(weekMatch[2]); + return week <= 52 || hasISOWeek53(wYear); + } const match = str.match(/(\d{4})-?(\d{0,2})-?(\d*)/).map(Number); const year = match[1]; const month = match[2]; diff --git a/test/validators.test.js b/test/validators.test.js index 98d2a12ff..a6f54ec5e 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -12443,12 +12443,30 @@ describe('Validators', () => { '2009-222', '2020-366', '2400-366', + // week 53 exists in these ISO week-numbering years (long years) + '2015-W53-1', + '2015-W53', + '2020-W53-7', + '2026-W53', + // weeks 01-52 exist in every ISO week-numbering year, long or short + '2020-W10', + '2020-W10-3', ], invalid: [ '2010-02-30', '2009-02-29', '2009-366', '2019-02-31', + // week 53 does not exist in these ISO week-numbering years (short + // years) -- regression test for #2859: previously strict mode never + // validated week dates at all, so these were wrongly accepted. + '2019-W53-1', + '2019-W53', + '2021-W53-7', + '2022-W53', + // 31 December 2017 is a Sunday, so the ISO remap of getUTCDay() 0 -> 7 + // runs here; 2017 is still a short year, so week 53 does not exist. + '2017-W53', ], }); });