Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ Validator | Description
**isCreditCard(str [, options])** | check if the string is a credit card number.<br/><br/> `options` is an optional object that can be supplied with the following key(s): `provider` is an optional key whose value should be a string, and defines the company issuing the credit card. Valid values include `['amex', 'dinersclub', 'discover', 'jcb', 'mastercard', 'unionpay', 'visa']` or blank will check for any provider.
**isCurrency(str [, options])** | check if the string is a valid currency amount.<br/><br/>`options` is an object which defaults to `{ symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_decimal: true, require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false }`.<br/>**Note:** The array `digits_after_decimal` is filled with the exact number of digits allowed not a range, for example a range 1 to 3 will be given as [1, 2, 3].
**isDataURI(str)** | check if the string is a [data uri format][Data URI Format].
**isDate(str [, options])** | check if the string is a valid date. e.g. [`2002-07-15`, new Date()].<br/><br/> `options` is an object which can contain the keys `format`, `strictMode` and/or `delimiters`.<br/><br/>`format` is a string and defaults to `YYYY/MM/DD`.<br/><br/>`strictMode` is a boolean and defaults to `false`. If `strictMode` is set to true, the validator will reject strings different from `format`.<br/><br/> `delimiters` is an array of allowed date delimiters and defaults to `['/', '-']`.
**isDate(str [, options])** | check if the string is a valid date. e.g. [`2002-07-15`, `20020715`, new Date()].<br/><br/> `options` is an object which can contain the keys `format`, `strictMode`, `delimiters` and/or `allowDelimiterless`.<br/><br/>`format` is a string and defaults to `YYYY/MM/DD`. Supports delimiter-based formats (e.g., `YYYY/MM/DD`, `DD-MM-YYYY`) and ISO 8601 compliant delimiter-less formats (e.g., `YYYYMMDD`, `YYMMDD`) when `allowDelimiterless` is enabled.<br/><br/>`strictMode` is a boolean and defaults to `false`. If `strictMode` is set to true, the validator will reject strings different from `format`.<br/><br/> `delimiters` is an array of allowed date delimiters and defaults to `['/', '-']`. This option is ignored for delimiter-less formats.<br/><br/>`allowDelimiterless` is a boolean and defaults to `false`. Set to `true` to enable ISO 8601 compliant delimiter-less format validation (e.g., `YYYYMMDD`).
**isDecimal(str [, options])** | check if the string represents a decimal number, such as 0.1, .3, 1.1, 1.00003, 4.0, etc.<br/><br/>`options` is an object which defaults to `{force_decimal: false, decimal_digits: '1,', locale: 'en-US'}`.<br/><br/>`locale` determines the decimal separator and is one of `['ar', 'ar-AE', 'ar-BH', 'ar-DZ', 'ar-EG', 'ar-IQ', 'ar-JO', 'ar-KW', 'ar-LB', 'ar-LY', 'ar-MA', 'ar-QA', 'ar-QM', 'ar-SA', 'ar-SD', 'ar-SY', 'ar-TN', 'ar-YE', 'bg-BG', 'cs-CZ', 'da-DK', 'de-DE', 'el-GR', 'en-AU', 'en-GB', 'en-HK', 'en-IN', 'en-NZ', 'en-US', 'en-ZA', 'en-ZM', 'eo', 'es-ES', 'fa', 'fa-AF', 'fa-IR', 'fr-FR', 'fr-CA', 'hu-HU', 'id-ID', 'it-IT', 'ku-IQ', 'nb-NO', 'nl-NL', 'nn-NO', 'pl-PL', 'pl-Pl', 'pt-BR', 'pt-PT', 'ru-RU', 'sl-SI', 'sr-RS', 'sr-RS@latin', 'sv-SE', 'tr-TR', 'uk-UA', 'vi-VN']`.<br/>**Note:** `decimal_digits` is given as a range like '1,3', a specific value like '3' or min like '1,'.
**isDivisibleBy(str, number)** | check if the string is a number that is divisible by another.
**isEAN(str)** | check if the string is an [EAN (European Article Number)][European Article Number].
Expand Down
149 changes: 110 additions & 39 deletions src/lib/isDate.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,24 @@ const default_date_options = {
format: 'YYYY/MM/DD',
delimiters: ['/', '-'],
strictMode: false,
allowDelimiterless: false,
};

function isValidFormat(format) {
return /(^(y{4}|y{2})[.\/-](m{1,2})[.\/-](d{1,2})$)|(^(m{1,2})[.\/-](d{1,2})[.\/-]((y{4}|y{2})$))|(^(d{1,2})[.\/-](m{1,2})[.\/-]((y{4}|y{2})$))/gi.test(format);
function isValidFormat(format, allowDelimiterless) {
// Formats with delimiters
const withDelimiters = /(^(y{4}|y{2})[\.\/-](m{1,2})[\.\/-](d{1,2})$)|(^(m{1,2})[\.\/-](d{1,2})[\.\/-]((y{4}|y{2})$))|(^(d{1,2})[\.\/-](m{1,2})[\.\/-]((y{4}|y{2})$))/gi;

if (withDelimiters.test(format)) {
return true;
}

// Formats without delimiters (e.g., YYYYMMDD, YYMMDD)
if (allowDelimiterless) {
const withoutDelimiters = /^(y{4}|y{2})(m{2})(d{2})$/gi;
return withoutDelimiters.test(format);
}

return false;
}

function zip(date, format) {
Expand All @@ -21,14 +35,85 @@ function zip(date, format) {
return zippedArr;
}

function hasNoDelimiter(format, delimiters) {
return !delimiters.some(delimiter => format.indexOf(delimiter) !== -1);
}

function parseDelimiterlessFormat(format) {
// Extract component positions from format like
// YYYYMMDD, YYMMDD (ISO 8601 compliant)
const lowerFormat = format.toLowerCase();
const components = [];
let pos = 0;

while (pos < lowerFormat.length) {
const char = lowerFormat[pos];
let len = 0;

while (pos + len < lowerFormat.length && lowerFormat[pos + len] === char) {
len += 1;
}

components.push({
type: char,
start: pos,
length: len,
});

pos += len;
}

return components;
}

function extractDatePartsWithoutDelimiter(input, format) {
const components = parseDelimiterlessFormat(format);
const dateObj = {};

for (const comp of components) {
dateObj[comp.type] = input.substring(
comp.start,
comp.start + comp.length
);
}

return dateObj;
}

export default function isDate(input, options) {
if (typeof options === 'string') { // Allow backward compatibility for old format isDate(input [, format])
// Allow backward compatibility for old format isDate(input [, format])
if (typeof options === 'string') {
options = merge({ format: options }, default_date_options);
} else {
options = merge(options, default_date_options);
}
if (typeof input === 'string' && isValidFormat(options.format)) {
if (options.strictMode && input.length !== options.format.length) return false;

// Handle non-string input
if (typeof input !== 'string' || !isValidFormat(options.format, options.allowDelimiterless)) {
if (!options.strictMode) {
return (
Object.prototype.toString.call(input) === '[object Date]' &&
isFinite(input)
);
}
return false;
}

if (options.strictMode && input.length !== options.format.length) {
return false;
}

let dateObj;

// Check if format has no delimiters (e.g., YYYYMMDD)
if (hasNoDelimiter(options.format, options.delimiters)) {
// Delimiter-less format parsing
if (input.length !== options.format.length) {
return false;
}
dateObj = extractDatePartsWithoutDelimiter(input, options.format);
} else {
// Original delimiter-based parsing
const formatDelimiter = options.delimiters
.find(delimiter => options.format.indexOf(delimiter) !== -1);
const dateDelimiter = options.strictMode
Expand All @@ -38,7 +123,7 @@ export default function isDate(input, options) {
input.split(dateDelimiter),
options.format.toLowerCase().split(formatDelimiter)
);
const dateObj = {};
dateObj = {};

for (const [dateWord, formatWord] of dateAndFormat) {
if (!dateWord || !formatWord || dateWord.length !== formatWord.length) {
Expand All @@ -47,48 +132,34 @@ export default function isDate(input, options) {

dateObj[formatWord.charAt(0)] = dateWord;
}
}

let fullYear = dateObj.y;

// Check if the year starts with a hyphen
if (fullYear.startsWith('-')) {
return false; // Hyphen before year is not allowed
}

if (dateObj.y.length === 2) {
const parsedYear = parseInt(dateObj.y, 10);

if (isNaN(parsedYear)) {
return false;
}

const currentYearLastTwoDigits = new Date().getFullYear() % 100;
let fullYear = dateObj.y;

if (parsedYear < currentYearLastTwoDigits) {
fullYear = `20${dateObj.y}`;
} else {
fullYear = `19${dateObj.y}`;
}
}
// Check if the year starts with a hyphen
if (fullYear.startsWith('-')) {
return false; // Hyphen before year is not allowed
}

let month = dateObj.m;
if (dateObj.y.length === 2) {
const parsedYear = parseInt(dateObj.y, 10);

if (dateObj.m.length === 1) {
month = `0${dateObj.m}`;
if (isNaN(parsedYear)) {
return false;
}

let day = dateObj.d;
const currentYearLastTwoDigits = new Date().getFullYear() % 100;

if (dateObj.d.length === 1) {
day = `0${dateObj.d}`;
if (parsedYear < currentYearLastTwoDigits) {
fullYear = `20${dateObj.y}`;
} else {
fullYear = `19${dateObj.y}`;
}

return new Date(`${fullYear}-${month}-${day}T00:00:00.000Z`).getUTCDate() === +dateObj.d;
}

if (!options.strictMode) {
return Object.prototype.toString.call(input) === '[object Date]' && isFinite(input);
}
const month = dateObj.m.length === 1 ? `0${dateObj.m}` : dateObj.m;
const day = dateObj.d.length === 1 ? `0${dateObj.d}` : dateObj.d;

return false;
const dateString = `${fullYear}-${month}-${day}T00:00:00.000Z`;
return new Date(dateString).getUTCDate() === +dateObj.d;
}
58 changes: 58 additions & 0 deletions test/validators.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14467,6 +14467,64 @@ describe('Validators', () => {
'abc.04.05.2024',
],
});
// Test delimiter-less format YYYYMMDD
test({
validator: 'isDate',
args: [{ format: 'YYYYMMDD', allowDelimiterless: true }],
valid: [
'20200229', // leap year
'20200115',
'20140215',
'20140315',
'19991231',
'20000101',
],
invalid: [
'2020-02-29',
'2020/02/29',
'20200230', // invalid date
'20190229', // non-leap year
'20200431', // invalid date
'2020229', // too short
'202002290', // too long
'',
'abcdefgh',
'2020ab29',
'-20200229',
],
});
// Test delimiter-less format YYMMDD (two-digit year)
test({
validator: 'isDate',
args: [{ format: 'YYMMDD', allowDelimiterless: true }],
valid: [
'200229', // 2020 leap year
'140215',
'991231',
'000101',
],
invalid: [
'20200229',
'2020-02-29',
'20229', // too short
'2002290', // too long
'',
'abcdef',
'20ab29',
'-200229',
],
});
// Test that delimiter-less formats are rejected without allowDelimiterless option
test({
validator: 'isDate',
args: [{ format: 'YYYYMMDD' }],
valid: [],
invalid: [
'20200229',
'20020715',
'19991231',
],
});
// emulating Pacific time zone offset & time
// which could potentially result in UTC conversion issues
timezone_mock.register('US/Pacific');
Expand Down
Loading