diff --git a/README.md b/README.md index 366036d43..19beef7f9 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Validator | Description **isISIN(str)** | check if the string is an [ISIN][ISIN] (stock/security identifier). **isISO6346(str)** | check if the string is a valid [ISO 6346](https://en.wikipedia.org/wiki/ISO_6346) shipping container identification. **isISO6391(str)** | check if the string is a valid [ISO 639-1][ISO 639-1] language code. -**isISO8601(str [, options])** | check if the string is a valid [ISO 8601][ISO 8601] date.
`options` is an object which defaults to `{ strict: false, strictSeparator: false }`. If `strict` is true, date strings with invalid dates like `2009-02-29` will be invalid. If `strictSeparator` is true, date strings with date and time separated by anything other than a T will be invalid. +**isISO8601(str)** | check if the string is a valid [ISO 8601][ISO 8601] date. **isISO15924(str)** | check if the string is a valid [ISO 15924][ISO 15924] officially assigned script code. **isISO31661Alpha2(str)** | check if the string is a valid [ISO 3166-1 alpha-2][ISO 3166-1 alpha-2] officially assigned country code. **isISO31661Alpha3(str)** | check if the string is a valid [ISO 3166-1 alpha-3][ISO 3166-1 alpha-3] officially assigned country code. diff --git a/src/lib/isISO8601.js b/src/lib/isISO8601.js index 1f797347d..5593d5b4a 100644 --- a/src/lib/isISO8601.js +++ b/src/lib/isISO8601.js @@ -2,43 +2,80 @@ import assertString from './util/assertString'; /* eslint-disable max-len */ // from http://goo.gl/0ejHHW -const iso8601 = /^([\+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([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)?)?)?)?$/; -// 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-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-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 */ -const isValidDate = (str) => { - // str must have passed the ISO8601 check - // this check is meant to catch invalid dates - // like 2009-02-31 - // first check for ordinal dates - const ordinalMatch = str.match(/^(\d{4})-?(\d{3})([ T]{1}\.*|$)/); +function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +} + +function has53Week(year) { + const jan1 = new Date(Date.UTC(year, 0, 1)).getUTCDay(); + return (jan1 === 4) || (isLeapYear(year) && jan1 === 3); +} + +const isValidIso8601 = (str) => { + const iso8601 = /^(?:\d{4}(?:-\d{2}(?:-\d{2})?|-\d{3}|-W\d{2}(?:-\d)?)?)(?:T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+\-]\d{2}:\d{2}|[+\-]\d{4}|[+\-]\d{2})?)?$/; + // this above regex check iso8601 format along with correct milisecond and second variation and T if time exist + // it rejects basic format(20251231) , only accepts date with extended format(2025-12-31). + // future Bug: it doesnot validate astronomical year, change the regex and fix its working if it's needed + const checkIso8601Format = iso8601.test(str); + if (!checkIso8601Format) { + return false; + } + + // Not checking time for ordinary dates and dates with weeks as its uncommon, but can be added in future as it is valid format. + // need to check for ordinary dates + const ordinalMatch = str.match(/^(\d{4})-?(\d{3})([T]{1}\.*|$)/); if (ordinalMatch) { const oYear = Number(ordinalMatch[1]); const oDay = Number(ordinalMatch[2]); + if (oDay < 1) return false; // if is leap year - if ((oYear % 4 === 0 && oYear % 100 !== 0) || oYear % 400 === 0) return oDay <= 366; + if (isLeapYear(oYear)) return oDay <= 366; return oDay <= 365; } - const match = str.match(/(\d{4})-?(\d{0,2})-?(\d*)/).map(Number); - const year = match[1]; - const month = match[2]; - const day = match[3]; - const monthString = month ? `0${month}`.slice(-2) : month; - const dayString = day ? `0${day}`.slice(-2) : day; - - // create a date object and compare - const d = new Date(`${year}-${monthString || '01'}-${dayString || '01'}`); + + // need to check for dates with week, dates with week and day + // only week match - issue if dates are with time weekmatch cannot reject it it will check only week part and return it, which i need to solve, same case for ordinal dates. + const WeekMatch = str.match(/^(\d{4})-W(\d{2})(?:-(\d))?$/); + if (WeekMatch) { + const [, yearStr, weekStr, dayStr] = WeekMatch; + const year = Number(yearStr); + const week = parseInt(weekStr, 10); + const day = dayStr ? parseInt(dayStr, 10) : null; + // check if week is in correct range + if (week < 1 || week > (has53Week(year) ? 53 : 52)) return false; + // check if week is last week of year it means 53 or 52, does it ends in between the last day + // check if day exist if it does it is in correct range + if (day != null) { + if (day < 1 || day > 7) return false; + } + return true; + } + + // check for correct values in iso8601 format + if (isNaN(Date.parse(str))) { + return false; + } + + // Final date check for correct range of date if it has month and day + // for edge cases like feb 30 is parsed in date.parse so it should be checked manually so date.parse check for out of 31 days and rest is passed through it + const dateMatch = str.match(/(\d{4})-?(\d{0,2})-?(\d*)/).map(Number); + const year = dateMatch[1]; + const month = dateMatch[2]; + const day = dateMatch[3]; + // check for valid month and day if (month && day) { - return d.getUTCFullYear() === year - && (d.getUTCMonth() + 1) === month - && d.getUTCDate() === day; + const d = new Date(Date.UTC(year, month - 1, day)); + return ( + d.getUTCFullYear() === year && + d.getUTCMonth() + 1 === month && + d.getUTCDate() === day + ); } + return true; }; -export default function isISO8601(str, options = {}) { +export default function isISO8601(str) { assertString(str); - const check = options.strictSeparator ? iso8601StrictSeparator.test(str) : iso8601.test(str); - if (check && options.strict) return isValidDate(str); - return check; + return isValidIso8601(str); } diff --git a/test/validators.test.js b/test/validators.test.js index 299af27d8..356e7aedc 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -11838,82 +11838,89 @@ describe('Validators', () => { }); const validISO8601 = [ - '2009-12T12:34', - '2009', - '2009-05-19', - '2009-05-19', - '20090519', - '2009123', - '2009-05', - '2009-123', - '2009-222', - '2009-001', - '2009-W01-1', - '2009-W51-1', - '2009-W511', - '2009-W33', - '2009W511', - '2009-05-19', - '2009-05-19 00:00', - '2009-05-19 14', - '2009-05-19 14:31', - '2009-05-19 14:39:22', - '2009-05-19T14:39Z', - '2009-W21-2', - '2009-W21-2T01:22', - '2009-139', - '2009-05-19 14:39:22-06:00', - '2009-05-19 14:39:22+0600', - '2009-05-19 14:39:22-01', - '20090621T0545Z', - '2007-04-06T00:00', - '2007-04-05T24:00', - '2010-02-18T16:23:48.5', - '2010-02-18T16:23:48,444', - '2010-02-18T16:23:48,3-06:00', - '2010-02-18T16:23.4', - '2010-02-18T16:23,25', - '2010-02-18T16:23.33+0600', - '2010-02-18T16.23334444', - '2010-02-18T16,2283', - '2009-05-19 143922.500', - '2009-05-19 1439,55', - '2009-10-10', - '2020-366', - '2000-366', + '2020-W53-4', + '2024-02-29', + '2023-12-31', + '2023-365', + '2024-366', + '2023-03-15T12:30:45.123+05:30', + '2023-03-15T12:30:45Z', + '2023-03-15T12:30:45+0000', + '2023-03-15T12:30:45+00:00', + '2023-W01-1', + '2023-W52-7', + '2020-W01', + '2024-001', + '2023-03-15', + '2023-03', + '2023', + '2024-02-29', + '2023-02-28', + '2023-04-30', + '2023-01-31', + '2023-03', + '2023', + '2020-001', + '2023-W01', + '2023-03-15T12:30Z', + '2023-03-15T12:30:45.1Z', + '2023-03-15T12:30:45.12Z', + '2023-03-15T12:30:45.123456Z', + '2020-W53-4', + '2009-W53-4', ]; const invalidISO8601 = [ - '200905', - '2009367', - '2009-', - '2007-04-05T24:50', - '2009-000', - '2009-M511', - '2009M511', - '2009-05-19T14a39r', - '2009-05-19T14:3924', - '2009-0519', - '2009-05-1914:39', - '2009-05-19 14:', - '2009-05-19r14:39', - '2009-05-19 14a39a22', - '200912-01', - '2009-05-19 14:39:22+06a00', - '2009-05-19 146922.500', - '2010-02-18T16.5:23.35:48', - '2010-02-18T16:23.35:48', - '2010-02-18T16:23.35:48.45', - '2009-05-19 14.5.44', - '2010-02-18T16:23.33.600', - '2010-02-18T16,25:23:48,444', - '2010-13-1', + '2019-W53-2', + '2020-W52-8', + '2012-W53-4', + '2019-366', + '2023-13-01', + '2023-02-30', + '2023-W00-2', + 'T12:00:00Z', + '2023-01-01T10:00:00+25:00', + '2023-01-01T10:00:00Zabc', + '2023-52-1', + '2020-W10-0', + '2020-W53-8', + '2020-W54-1', + '2020-W01-00', + '2024-367', + 'abcd-ef-gh', + '', + '123', + '2023-03-15T25:00:00Z', + '2023-03-15T12:60:00Z', + '2023-03-15T12:00:60Z', 'nonsense2021-01-01T00:00:00Z', '2021-01-01T00:00:00Znonsense', + '2023-02-29', + '2023-02-30', + '2023-04-31', + '2023-01-32', + '2023-13', + '20A3', + '+001980-13-25', + '0000-01-01', + '2023-0032', + '2023-W01-8', + '2023-W01-0', + '2023-W01-01', + '2023-03-15T', + '2023-03-15T12:', + '2023-03-15T12:30:Z', + '2023-03-15T12:30:45.', + '2023-03-15T12:30:45.12+2400', + '2023-03-15T12:30:45+2400', + '2023-03-15T12:30:45+2360', + '2023-W1', + '2023-W01-', + '2023-03-15T12:30:45.123+01', + '2000-000', ]; it('should validate ISO 8601 dates', () => { - // from http://www.pelagodesign.com/blog/2009/05/20/iso-8601-date-validation-that-doesnt-suck/ test({ validator: 'isISO8601', valid: validISO8601, @@ -11921,146 +11928,6 @@ describe('Validators', () => { }); }); - it('should validate ISO 8601 dates, with strict = true (regression)', () => { - test({ - validator: 'isISO8601', - args: [ - { strict: true }, - ], - valid: validISO8601, - invalid: invalidISO8601, - }); - }); - - it('should validate ISO 8601 dates, with strict = true', () => { - test({ - validator: 'isISO8601', - args: [ - { strict: true }, - ], - valid: [ - '2000-02-29', - '2009-123', - '2009-222', - '2020-366', - '2400-366', - ], - invalid: [ - '2010-02-30', - '2009-02-29', - '2009-366', - '2019-02-31', - ], - }); - }); - - it('should validate ISO 8601 dates, with strictSeparator = true', () => { - test({ - validator: 'isISO8601', - args: [ - { strictSeparator: true }, - ], - valid: [ - '2009-12T12:34', - '2009', - '2009-05-19', - '2009-05-19', - '20090519', - '2009123', - '2009-05', - '2009-123', - '2009-222', - '2009-001', - '2009-W01-1', - '2009-W51-1', - '2009-W511', - '2009-W33', - '2009W511', - '2009-05-19', - '2009-05-19T14:39Z', - '2009-W21-2', - '2009-W21-2T01:22', - '2009-139', - '20090621T0545Z', - '2007-04-06T00:00', - '2007-04-05T24:00', - '2010-02-18T16:23:48.5', - '2010-02-18T16:23:48,444', - '2010-02-18T16:23:48,3-06:00', - '2010-02-18T16:23.4', - '2010-02-18T16:23,25', - '2010-02-18T16:23.33+0600', - '2010-02-18T16.23334444', - '2010-02-18T16,2283', - '2009-10-10', - '2020-366', - '2000-366', - ], - invalid: [ - '200905', - '2009367', - '2009-', - '2007-04-05T24:50', - '2009-000', - '2009-M511', - '2009M511', - '2009-05-19T14a39r', - '2009-05-19T14:3924', - '2009-0519', - '2009-05-1914:39', - '2009-05-19 14:', - '2009-05-19r14:39', - '2009-05-19 14a39a22', - '200912-01', - '2009-05-19 14:39:22+06a00', - '2009-05-19 146922.500', - '2010-02-18T16.5:23.35:48', - '2010-02-18T16:23.35:48', - '2010-02-18T16:23.35:48.45', - '2009-05-19 14.5.44', - '2010-02-18T16:23.33.600', - '2010-02-18T16,25:23:48,444', - '2010-13-1', - '2009-05-19 00:00', - // Previously valid cases - '2009-05-19 14', - '2009-05-19 14:31', - '2009-05-19 14:39:22', - '2009-05-19 14:39:22-06:00', - '2009-05-19 14:39:22+0600', - '2009-05-19 14:39:22-01', - ], - }); - }); - - it('should validate ISO 8601 dates, with strict = true and strictSeparator = true (regression)', () => { - test({ - validator: 'isISO8601', - args: [ - { strict: true, strictSeparator: true }, - ], - valid: [ - '2000-02-29', - '2009-123', - '2009-222', - '2020-366', - '2400-366', - ], - invalid: [ - '2010-02-30', - '2009-02-29', - '2009-366', - '2019-02-31', - '2009-05-19 14', - '2009-05-19 14:31', - '2009-05-19 14:39:22', - '2009-05-19 14:39:22-06:00', - '2009-05-19 14:39:22+0600', - '2009-05-19 14:39:22-01', - ], - }); - }); - it('should validate ISO 15924 script codes', () => { test({ validator: 'isISO15924',