# For Userscript Developers - Functions to Validate and Parse Datetime

**URL:** <https://community.wanikani.com/t/for-userscript-developers-functions-to-validate-and-parse-datetime/46288>\
**Category:** API And Third-Party Apps\
**Created:** [October 5, 2020, 9:23pm UTC](https://community.wanikani.com/t/for-userscript-developers-functions-to-validate-and-parse-datetime/46288 "2020-10-05T21:23:02Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![prouleau](https://sea1.discourse-cdn.com/wanikanicommunity/user_avatar/community.wanikani.com/prouleau/32/376127_2.png) [@prouleau](https://community.wanikani.com/u/prouleau)\
**Post date:** [October 5, 2020, 9:23pm UTC](https://community.wanikani.com/t/for-userscript-developers-functions-to-validate-and-parse-datetime/46288/1 "2020-10-05T21:23:02Z")

</div>

I have written Javascript datetime validation and parsing functions for use with wkof. They are interpreting time in the user local time zone and they properly handle leap years. Some of these functions support relative time.

The validation functions comply with the wkof requirements for a validate callback in a settings dialog. They accept a text input and return either true (if the datetime is valid) or an error text string that will be displayed to the user. If the user messed the datetime format they are presented with the format that is expected from them. If the format is correct but the numbers are invalid for a date they are presented with a ‘Number out of range’ message.

The functions provided:

- `validateDateTime`

Parameter: a string representing the date to validate.  
Returns: true if the date is valid. A string for the error message if the date is invalid.

This function validates datetime according to the `YYYY-MM-DD HH:MM` format. It accepts both 12 hours and 24 hours formats. The time (`HH:MM`) is optional. Seconds and milliseconds are not supported.

- `validateDateTimeRelative`

Parameter: a string representing the date to validate.  
Returns: true if the date is valid. A string for the error message if the date is invalid.

Same as `validateDateTime` with added support for relative time in the format ±24d10h30m. Days, hours and minutes are optional as long as one of them is present.

- `validateDateTimeFull`

Parameter: a string representing the date to validate.  
Returns: true if the date is valid. A string for the error message if the date is invalid.

This function validates datetime according to the `YYYY-MM-DD HH:MM:SS.mmm` format. Seconds and milliseconds are optional. For use when support for seconds and/or millisecond is wanted.

- `validateDateTimeFullRelative`

Parameter: a string representing the date to validate.  
Returns: true if the date is valid. A string for the error message if the date is invalid.

Same as `validateDateTimeFull` with added support for relative time in the format ±24d10h30m15s. Days, hours, minutes and seconds are optional as long as one of them is present.

- `validateDate`

Parameter: a string representing the date to validate.  
Returns: true if the date is valid. A string for the error message if the date is invalid.

This function validates a date in the `YYYY-MM-DD` format.

- `validateDateRelative`

Parameter: a string representing the date to validate.  
Returns: true if the date is valid. A string for the error message if the date is invalid.

Same as `validateDate` with added support for relative time in the format ±24d.

- `parseDateTime`

Parameter: a string representing the date to validate.  
Returns: A Date object for the date, interpreted in the end user local timezone. An invalid date Date object is returned in case of an error.

This functions parses a date that has been validated with one of the six previous functions.

- It accepts all three formats supported by the three validation functions as well as relative time…
- It interprets the date in the local time zone and returns the corresponding Date object.
- Conversion from 12 hours to 24 hours is performed when needed.
- Relative time is interpreted as an offset from when the function is called.

The data should be validated. It will return an invalid Date object if an error occurs but the input is not otherwise validated. If you have doubts about data quality validate first.

> **The code**
>
> Permission is granted to everyone to use this code.
> 
> ```auto
> //=======================================
> // Date Validation and Parsing Functions
> //=======================================
> 
> //=======================================
> // All time validation functions and the parsing function accept
> // YYYY-MM-DD 24:00 to mean next day at 00:00
> // According to wikipedia this is part of the 24 hours time comvention
> //=======================================
> 
> //=======================================
> // This group of functions nails the format to YYYY-MM-DD something
> //=======================================
> // Error messages
> const errorWrongDateTimeFormat = 'Use YYYY-MM-DD HH:MM [24h, 12h]';
> const errorWrongDateTimeRelativeFormat = 'Use YYYY-MM-DD HH:MM [24h, 12h]<br>Or +10d3h45m or -4h12h30m<br>+- needed, rest may be omitted';
> const errorWrongDateTimeFullFormat = 'Use YYYY-MM-DD HH:MM:SS.mmm<br>Seconds and milliseconds optional';
> const errorWrongDateTimeFullRelativeFormat = 'Use YYYY-MM-DD HH:MM:SS.mmm<br>Seconds and milliseconds optional<br>Or +10d3h45m12s -4h12h30m10s<br>+- needed, rest may be omitted';
> const errorWrongDateFormat = 'Invalid date - Use YYYY-MM-DD';
> const errorWrongDateRelativeFormat = 'Invalid date - Use YYYY-MM-DD<br>Or +10d or -2d';
> const errorOutOfRange = 'Number out of range';
> 
> //=======================================
> // Validates datetime in YYYY-MM-DD HH:MM format
> // Accepts both 24h and 12h formats (am pm)
> // Accepts YYYY-MM-DD (HH:MM omitted)
> // Bissextile years are properly processed
> // Suitable for use as validate callback in a text component of a setting
> function validateDateTime(dateString, config){
> dateString = dateString.trim();
> if (dateString.length > 18){
> return errorWrongDateTimeFormat;
> } else {
> let result = validateDate(dateString.slice(0,10), config);
> if (result === errorOutOfRange) return errorOutOfRange;
> if (result !== true) return errorWrongDateTimeFormat;
> if (dateString.length === 10) return true; //Valid YYY-MM-DD and nothing else
> result = validateTime(dateString.slice(0,16));
> if (result === errorOutOfRange) return errorOutOfRange;
> if (result !== true) return errorWrongDateTimeFormat;
> if (dateString.length === 16){
> return true
> } else {
> if (dateString.length === 18){
> let suffix = dateString.slice(16)
> if (suffix === 'am' || suffix === 'pm'){
> let hh = Number(dateString.slice(11, 13))
> if (hh < 1 || hh > 12){return errorOutOfRange}
> return true
> } else {
> return errorWrongDateTimeFormat;
> }
> }
> return errorWrongDateTimeFormat;
> };
> };
> return errorWrongDateTimeFormat;
> };
> 
> //=======================================
> // Validates datetime in YYYY-MM-DD HH:MM format or relative time format
> // Accepts both 24h and 12h formats (am pm)
> // Accepts YYYY-MM-DD (HH:MM omitted)
> // Bissextile years are properly processed
> // Suitable for use as validate callback in a text component of a setting
> function validateDateTimeRelative(dateString, config){
> dateString = dateString.trim();
> if (dateString.match(/^([+-])(?:(\d+)[dD])?(?:(\d+)[hH])?(?:(\d+)[mM])?$/) !== null){
> if (dateString === '+' || dateString === '-') return errorWrongDateTimeRelativeFormat
> return true;
> } else {
> let result = validateDateTime(dateString, config)
> if (result === true || result === errorOutOfRange) return result;
> return errorWrongDateTimeRelativeFormat;
> }
> };
> 
> //=======================================
> // Validate datetime in YYYY-MM-DD HH:MM:SS.mmm format
> // Seconds and milliseconds are optional
> // Bissextile years are properly processed
> // Suitable for use as validate callback in a text component of a setting
> function validateDateTimeFull(dateString, config){
> dateString = dateString.trim();
> let result = validateDateTime(dateString.slice(0, 16), config);
> if (result === errorOutOfRange){
> return errorOutOfRange;
> } else if (result !== true){
> return errorWrongDateTimeFullFormat;
> } else if (dateString.length <= 16){
> return true // seconds and milliseconds omitted
> } else {
> var regEx = /^:(\d{2}|\d{2}\.\d{3})$/;
> if(!dateString.slice(16).match(regEx)) return errorWrongDateTimeFullFormat; // Invalid format
> let d = new Date(dateString);
> let dNum = d.getTime();
> if(!dNum && dNum !== 0) return errorOutOfRange; // NaN value, Invalid date
> return true
> }
> }
> 
> //=======================================
> // Validate datetime in YYYY-MM-DD HH:MM:SS.mmm format or relative format
> // Seconds and milliseconds are optional
> // Bissextile years are properly processed
> // Suitable for use as validate callback in a text component of a setting
> function validateDateTimeFullRelative(dateString, config){
> dateString = dateString.trim();
> if (dateString.match(/^([+-])(?:(\d+)[dD])?(?:(\d+)[hH])?(?:(\d+)[mM])?(?:(\d+)[sS])?$/) !== null){
> if (dateString === '+' || dateString === '-') return errorWrongDateTimeFullRelativeFormat
> return true;
> } else {
> let result = validateDateTimeFull(dateString, config)
> if (result === true || result === errorOutOfRange) return result;
> return errorWrongDateTimeFullRelativeFormat;
> }
> };
> 
> //=======================================
> // Validates dates in YYYY-MM-DD format
> // Bissextile years are properly processed
> // Suitable for use as validate callback in a text component of a setting
> function validateDate(dateString, config, keyword) {
> dateString = dateString.trim();
> let regEx = /^\d{4}-\d{2}-\d{2}$/;
> if(!dateString.match(regEx)) return errorWrongDateFormat; // Invalid format
> let d = new Date(dateString);
> let dNum = d.getTime();
> if(!dNum && dNum !== 0) return errorOutOfRange; // NaN value, Invalid date
> let r = d.toISOString().slice(0,10) === dateString;
> if (r) {
> return true
> } else {
> return errorOutOfRange
> };
> }
> 
> //=======================================
> // Validates dates in YYYY-MM-DD format or relative format
> // Bissextile years are properly processed
> // Suitable for use as validate callback in a text component of a setting
> function validateDateRelative(dateString, config){
> dateString = dateString.trim();
> if (dateString.match(/^([+-])(?:(\d+)[dD])?$/) !== null){
> if (dateString === '+' || dateString === '-') return errorWrongDateRelativeFormat
> return true;
> } else {
> let result = validateDate(dateString, config)
> if (result === true || result === errorOutOfRange) return result;
> return errorWrongDateRelativeFormat;
> }
> };
> 
> //=======================================
> // Helper function to validate time in HH:MM format
> // It should not be publicly exposed
> function validateTime(timeString) {
> let regEx = /^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}$/;
> if(!timeString.match(regEx)) return 'No match'; // Invalid format
> let d = new Date(timeString);
> let dNum = d.getTime();
> if(!dNum && dNum !== 0) return errorOutOfRange; // NaN value, Invalid date
> return true
> }
> 
> //=======================================
> // Parses a validated date in YYYY-MM-DD format
> // Also parse a validated datetime in YYYY-MM-DD HH:MM format
> // Parses datetime in both 12h and 24h formats
> // Parses optional seconds and milliseconds
> // Returns the corresponding date object for this date/datetime in the local time zone
> // May return an invalid date if presented with empty or invalid data - but not always
> // If there is doubt about the quality of the data, validate first
> // Suitable to parse a validated date from a text component in a setting
> function parseDateTime(dateString) {
> dateString = dateString.trim(); // validation allows leading and trailing blanks
> try {
> if (dateString === '') return new Date('###'); // returns an invalid date
> let match = dateString.match(/^([+-])(?:(\d+)[dD])?(?:(\d+)[hH])?(?:(\d+)[mM])?(?:(\d+)[sS])?$/);
> if (match !== null){
> if (dateString === '+' || dateString === '-') return new Date('###'); // returns an invalid date
> let date = Date.now();
> let sign = (match[1] === '+' ? 1 : -1);
> let days = (match[2] || 0) * 86400000;
> let hrs = (match[3] || 0) * 3600000;
> let min = (match[4] || 0) * 60000;
> let sec = (match[5] || 0) * 1000;
> return new Date(date + sign * (days + hrs + min + sec));
> }
> // new Date() uses local time zone when the parameters are separated
> let YY = Number(dateString.substring(0, 4));
> let MM = Number(dateString.substring(5, 7))-1;
> let DD = Number(dateString.substring(8, 10));
> let hh = (dateString.length >= 13) ? Number(dateString.substring(11, 13)) : 0;
> let mm = (dateString.length >= 16) ? Number(dateString.substring(14, 16)) : 0;
> let ss = (dateString.length >= 19) ? Number(dateString.substring(17, 19)) : 0;
> let ml = (dateString.length === 23) ? Number(dateString.substring(20, 23)) : 0;
> 
> let suffix = (dateString.length === 18) ? dateString.substring(16, 18) : ''
> if (suffix === 'am' || suffix === 'pm'){ // if 12 hours format, convert to 24 hours
> if (hh === 12) hh = 0;
> if (suffix === 'pm') hh += 12;
> }
> return new Date(YY, MM, DD, hh, mm, ss, ml);
> } catch (e) {
> return new Date('###'); // returns an invalid date in case of error
> }
> }
> 
> ```

**Date.parse()**

In discussions with @Kumirei and @rfindley the question was raised of why not use a plain `Date.parse()` or its equivalent `new Date()`. It was argued that `Date.parse()` is convenient, simple to use, suffice to most needs and supports locale datetime formats. Well, in my opinion we can’t use this for these reasons.

- The validation functions are meant to be used as validate called back in WKOF settings dialog. We need a wrapper around `Date.parse()` to comply with the framework requirements.
- A wrapper is also needed when we want to support relative time.
- `Date.parse()` accepts integers as valid dates and makes dates out of them. This data type is not a date. (as tested on Chrome)
- `Date.parse()` makes a date out of `2/2/2020` although it is not clear whether the day or the month comes first.
- `Date.parse()` makes a date out of `septejdhdf,!!! 2:;.. 1995?? 9` without reporting an error. (Tested on Chrome)
- `Mozilla.org` [recommends against it](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse).

> It is not recommended to use `Date.parse` as until ES5, parsing of strings was entirely implementation dependent. There are still many differences in how different hosts parse date strings, therefore date strings should be manually parsed (a library can help if many different formats are to be accommodated).

For me the real killers are `septejdhdf,!!! 2:;.. 1995?? 9` and the ambiguity around 2/2/2020. This is an issue of data quality. God only knows what other kind of nonsense `Date.parse()` will accept as a date. I can’t trust Date.parse() to produce the date the user intended to type without further validation.

**More Evidence**

I looked further into the locale datetime support of `Date.parse()`. Mozilla reports the following. (same link as above)

> However, invalid values in date strings not recognized as simplified ISO format as defined by ECMA-262 may or may not result in [`NaN`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN), depending on the browser and values provided, e.g.:

```auto
// Non-ISO string with invalid date values
new Date('23/25/2014');

```

> will be treated as a local date of 25 November, 2015 in Firefox 30 and an invalid date in Safari 7.

This means we can’t trust a NaN test of `Date.parse()` to provide the correct result in every browser. There is more.

> The string " `10 06 2014` " is an example of a non-conforming ISO format and thus falls back to a custom routine. See also this [rough outline](https://bugzilla.mozilla.org/show_bug.cgi?id=1023155#c6) on how the parsing works.

```auto
new Date('10 06 2014');

```

> will be treated as a local date of 6 October, 2014, and not 10 June, 2014.

This is a problem. In some locale the month comes first and others puts the day first. You can’t use a one format fits all approach and get correct results. For example in Canada the English put the month first and the French put the day first. The browser should use the computer settings to determine what a locally valid date is. Chrome does not. I am set up for YYYY-MM-DD and `Date.parse()` accepted `2/2/2020`. For a long date my computer is set up for `2 septembre 1995` and Chrome accepted `septejdhdf,!!! 2:;.. 1995?? 9` which is a weid typo for `september 2 1995`.

And then, still according to Mozilla, there is this.

> given a simplification of the ISO 8601 calendar date extended format such as " `2014-03-07` ", it will assume a time zone of UTC (ES5 and ECMAScript 2015).

This is a problem. In Canada the locale format for a date is YYYY-MM-DD and it will always be interpreted as UTC by `Date.parse()`.

For all these reasons, I conclude that the locale support of `Date.parse()` is broken.

**Acknowledgement**

@rfindley kindly provided reference code for the relative datetime format.

---

<div class="post-metadata">

**Author:** ![Kumirei](https://sea1.discourse-cdn.com/wanikanicommunity/user_avatar/community.wanikani.com/kumirei/32/480522_2.png) [@Kumirei](https://community.wanikani.com/u/Kumirei)\
**Post date:** [October 5, 2020, 10:09pm UTC](https://community.wanikani.com/t/for-userscript-developers-functions-to-validate-and-parse-datetime/46288/2 "2020-10-05T22:09:59Z")

</div>

I think for most cases something like `!isNaN(Date.parse(date))` is good enough for me, but I’ll keep this in mind

---

<div class="post-metadata">

**Author:** ![prouleau](https://sea1.discourse-cdn.com/wanikanicommunity/user_avatar/community.wanikani.com/prouleau/32/376127_2.png) [@prouleau](https://community.wanikani.com/u/prouleau)\
**Post date:** [October 5, 2020, 10:34pm UTC](https://community.wanikani.com/t/for-userscript-developers-functions-to-validate-and-parse-datetime/46288/3 "2020-10-05T22:34:13Z")

</div>

We must keep in mind that ‘Date.parse(date)’ makes a date out of many sorts of strings. I got it to parse `02/02/2020` even though I don’t know what that date means. Does the month comes first? Or is it the day? There is a user interface issue if the user doesn’t understand the date the same as the software. My functions enforce the format exactly and inform the user of what is expected of them.

---

<div class="post-metadata">

**Author:** ![Kumirei](https://sea1.discourse-cdn.com/wanikanicommunity/user_avatar/community.wanikani.com/kumirei/32/480522_2.png) [@Kumirei](https://community.wanikani.com/u/Kumirei)\
**Post date:** [October 5, 2020, 10:38pm UTC](https://community.wanikani.com/t/for-userscript-developers-functions-to-validate-and-parse-datetime/46288/4 "2020-10-05T22:38:02Z")

</div>

Yeah, that’s why I say in most cases

---

<div class="post-metadata">

**Author:** ![prouleau](https://sea1.discourse-cdn.com/wanikanicommunity/user_avatar/community.wanikani.com/prouleau/32/376127_2.png) [@prouleau](https://community.wanikani.com/u/prouleau)\
**Post date:** [October 7, 2020, 2:09am UTC](https://community.wanikani.com/t/for-userscript-developers-functions-to-validate-and-parse-datetime/46288/5 "2020-10-07T02:09:52Z")

</div>

Following on a suggestion of @rfindley I have added the support of relative time to my functions. Thanks to @rfindley for providing some reference code.

I have updated the top post accordingly.
