Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add new isDate() validator #1270

Merged
Merged
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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ Sanitizer | Description
**trim(input [, chars])** | trim characters (whitespace by default) from both sides of the input.
**whitelist(input, chars)** | remove characters that do not appear in the whitelist. The characters are used in a RegExp and so you will need to escape some chars, e.g. `whitelist(input, '\\[\\]')`.
**isSlug** | Check if the string is of type slug. `Options` allow a single hyphen between string. e.g. [`cn-cn`, `cn-c-c`]
**isDate(input, [, format])** | Check if the input is a valid date. e.g. [`2002-07-15`, new Date()]
`format` is a string and defaults to 'YYYY/MM/DD'

### XSS Sanitization

Expand Down
4 changes: 3 additions & 1 deletion es/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import isMACAddress from './lib/isMACAddress';
import isIP from './lib/isIP';
import isIPRange from './lib/isIPRange';
import isFQDN from './lib/isFQDN';
import isDate from './lib/isDate';
import isBoolean from './lib/isBoolean';
import isLocale from './lib/isLocale';
import isAlpha, { locales as isAlphaLocales } from './lib/isAlpha';
Expand Down Expand Up @@ -176,6 +177,7 @@ var validator = {
isWhitelisted: isWhitelisted,
normalizeEmail: normalizeEmail,
toString: toString,
isSlug: isSlug
isSlug: isSlug,
isDate: isDate
};
export default validator;
63 changes: 63 additions & 0 deletions es/lib/isDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }

function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }

function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }

function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }

function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(n); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

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 zip(date, format) {
var zippedArr = [],
len = Math.min(date.length, format.length);

for (var i = 0; i < len; i++) {
zippedArr.push([date[i], format[i]]);
}

return zippedArr;
}

export default function isDate(input) {
var format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'YYYY/MM/DD';

if (typeof input === 'string' && isValidFormat(format)) {
var splitter = /[-/]/,
dateAndFormat = zip(input.split(splitter), format.toLowerCase().split(splitter)),
dateObj = {};

var _iterator = _createForOfIteratorHelper(dateAndFormat),
_step;

try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _step$value = _slicedToArray(_step.value, 2),
dateWord = _step$value[0],
formatWord = _step$value[1];

if (dateWord.length !== formatWord.length) {
return false;
}

dateObj[formatWord.charAt(0)] = dateWord;
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}

return new Date("".concat(dateObj.m, "/").concat(dateObj.d, "/").concat(dateObj.y)).getDate() === +dateObj.d;
}

return Object.prototype.toString.call(input) === '[object Date]' && isFinite(input);
}
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var _isIPRange = _interopRequireDefault(require("./lib/isIPRange"));

var _isFQDN = _interopRequireDefault(require("./lib/isFQDN"));

var _isDate = _interopRequireDefault(require("./lib/isDate"));

var _isBoolean = _interopRequireDefault(require("./lib/isBoolean"));

var _isLocale = _interopRequireDefault(require("./lib/isLocale"));
Expand Down Expand Up @@ -276,7 +278,8 @@ var validator = {
isWhitelisted: _isWhitelisted.default,
normalizeEmail: _normalizeEmail.default,
toString: toString,
isSlug: _isSlug.default
isSlug: _isSlug.default,
isDate: _isDate.default
};
var _default = validator;
exports.default = _default;
Expand Down
73 changes: 73 additions & 0 deletions lib/isDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use strict";

Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = isDate;

function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); }

function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }

function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return; var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }

function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }

function _createForOfIteratorHelper(o) { if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (o = _unsupportedIterableToArray(o))) { var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var it, normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it.return != null) it.return(); } finally { if (didErr) throw err; } } }; }

function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(n); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }

function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }

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 zip(date, format) {
var zippedArr = [],
len = Math.min(date.length, format.length);

for (var i = 0; i < len; i++) {
zippedArr.push([date[i], format[i]]);
}

return zippedArr;
}

function isDate(input) {
var format = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'YYYY/MM/DD';

if (typeof input === 'string' && isValidFormat(format)) {
var splitter = /[-/]/,
dateAndFormat = zip(input.split(splitter), format.toLowerCase().split(splitter)),
dateObj = {};

var _iterator = _createForOfIteratorHelper(dateAndFormat),
_step;

try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _step$value = _slicedToArray(_step.value, 2),
dateWord = _step$value[0],
formatWord = _step$value[1];

if (dateWord.length !== formatWord.length) {
return false;
}

dateObj[formatWord.charAt(0)] = dateWord;
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}

return new Date("".concat(dateObj.m, "/").concat(dateObj.d, "/").concat(dateObj.y)).getDate() === +dateObj.d;
}

return Object.prototype.toString.call(input) === '[object Date]' && isFinite(input);
}

module.exports = exports.default;
module.exports.default = exports.default;
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import isMACAddress from './lib/isMACAddress';
import isIP from './lib/isIP';
import isIPRange from './lib/isIPRange';
import isFQDN from './lib/isFQDN';
import isDate from './lib/isDate';

import isBoolean from './lib/isBoolean';
import isLocale from './lib/isLocale';
Expand Down Expand Up @@ -206,6 +207,7 @@ const validator = {
normalizeEmail,
toString,
isSlug,
isDate,
};

export default validator;
34 changes: 34 additions & 0 deletions src/lib/isDate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd thought we'll come up with multiple regex depending with the format provided, starting with YYYY/MM/DD as a default.

We said we'll keep it simple as just date string only, nothing else.

}

function zip(date, format) {
const zippedArr = [],
len = Math.min(date.length, format.length);

for (let i = 0; i < len; i++) {
zippedArr.push([date[i], format[i]]);
}

return zippedArr;
}

export default function isDate(input, format = 'YYYY/MM/DD') {
if (typeof input === 'string' && isValidFormat(format)) {
const splitter = /[-/]/,
dateAndFormat = zip(input.split(splitter), format.toLowerCase().split(splitter)),
dateObj = {};

for (const [dateWord, formatWord] of dateAndFormat) {
if (dateWord.length !== formatWord.length) {
return false;
}

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

return new Date(`${dateObj.m}/${dateObj.d}/${dateObj.y}`).getDate() === +dateObj.d;
}

return Object.prototype.toString.call(input) === '[object Date]' && isFinite(input);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What this last call for? The assumption is that input will always be a string. You need to call assertString() at the top of the function...

}
63 changes: 63 additions & 0 deletions test/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -8025,4 +8025,67 @@ describe('Validators', () => {
],
});
});

it('should validate date', () => {
test({
validator: 'isDate',
valid: [
new Date(),
new Date([2014, 2, 15]),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means we allow users to send created dates using the new Date() method?
I thought, we just need to pass a string. But, it is a good idea.

new Date('2014-03-15'),
'2020/02/29',
],
invalid: [
'',
Copy link
Member

@profnandaa profnandaa Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try add the following test cases:

2020-02-30, // invalid date
2019-02-29, // non-leap year
2020-04-31, // invalid date

I presume all those will pass as true (or fail as false). We will need to handle these cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since:

> new Date('2020-02-31')
2020-03-02T00:00:00.000Z
> new Date('2020-04-31')
2020-05-01T00:00:00.000Z

Copy link
Contributor Author

@mum-never-proud mum-never-proud Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice catch!

a simple validation would be

> new Date('2020-02-30').getUTCDate()
1
> new Date('2019-02-29').getUTCDate()
1
> new Date('2020-02-29').getUTCDate()
29

we can compare it with the date supplied

the problem here is with the date format some may give as YYYY-MM-DD or MM-DD-YYYY

we can do that! my bad

any suggestion?

any better way ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd not thought of that way, I think I like it! 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm looks like there are many edge cases to be considered in this case,

what if a user supplies

MM-DD-YYYY
YYYY-MM-DD
YY-MM-DD
MM-DD-YY

we need to keep things simple and general

any suggestion?

Copy link
Member

@profnandaa profnandaa Mar 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, for now let's just support those that are supported by Date(), and strictly YYYY. Need to add that note on the README.

However, I noticed something else, look at this?

> new Date('02-02-2019')
2019-02-01T21:00:00.000Z // .getUTCDate() = 1
> new Date('2019-02-02')
2019-02-02T00:00:00.000Z // .getUTCDate() = 2

Perhaps this are some of the intricacies that led to deprecating the first isDate() from the library. 🤔

What if we allowed the user to supply the format from a list of predefined formats?

MM-DD-YYYY
DD-MM-YYYY
YYYY-MM-DD

Then transform this to a YYYY-MM-DD format before running it through new Date()

Copy link
Contributor Author

@mum-never-proud mum-never-proud Mar 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check out the fiddle

https://jsfiddle.net/hvrwp07e/1/

I have done some tweaking, I have some stuff to do so right now I don't have time to think about edge cases

feel free to introduce some edge case or enhance the code

let me know if you are aware of any edge cases

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking forward to that we can do more addition on edge cases in the future and update the readme accordingly.

'15072002',
null,
undefined,
{ year: 2002, month: 7, day: 15 },
42,
{ toString() { return '[object Date]'; } }, // faking
'2020-02-30', // invalid date
'2019-02-29', // non-leap year
'2020-04-31', // invalid date
],
});
test({
validator: 'isDate',
args: ['DD/MM/YYYY'],
valid: [
'15-07-2002',
'15/07/2002',
],
invalid: [
'15/7/2002',
'15-7-2002',
'15/7/02',
'15-7-02',
],
});
test({
validator: 'isDate',
args: ['DD/MM/YY'],
valid: [
'15-07-02',
'15/07/02',
],
invalid: [
'15/7/2002',
'15-7-2002',
],
});
test({
validator: 'isDate',
args: ['D/M/YY'],
valid: [
'5-7-02',
'5/7/02',
],
invalid: [
'5/07/02',
'15/7/02',
'15-7-02',
],
});
});
});
Loading