Prevent moment.js accepting an integer as a valid ISO8601 date

Viewed 311

I have some API's for creating dashboard widgets. Those API's return basic name/value data pairs that are passed to Google Charts. Moment.js checks whether the value is an ISO8601 date, and if so passes to Google Charts as a date instance.

However, the ISO_8601 isValid check is currently returning true if the date is a simple integer, e.g. 1234:

var myInt = 1234;
if (moment(myInt, moment.ISO_8601, true).isValid()) {
    console.log("Valid!");
}

I couldn't locate the necessary functionality to force a date format in the moment.js code, so this brutal hack works for now:

var myInt = 1234;
if (JSON.stringify(myInt).includes("T") && moment(myInt, moment.ISO_8601, true).isValid()) {
    console.log("Valid!");
}

Is there a correct way to use moment.js to configure the isValid() check?

The date format from my API is yyyy-mm-ddThh:mm:ss (without Z on the end).

4 Answers

According to THIS answer, when using strict parsing (last parameter set to true) you should also specify parse format, to avoid situations like you discribed. As many users notice, specyfying string format instead of using moment.ISO_8601 works as expected.

alert(isISODateValid(123)); //false
alert(isISODateValid("2011-10-10T14:48:00")); //true
alert(isISODateValid("2011-10-10T14:48:00Z")); //true

function isISODateValid(date) {

  return moment(date.toString().replaceAll("Z",""), "YYYY-MM-DDTHH:mm:ss", true).isValid();

}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>

EDITS: Updated snippet - if date contains "Z" suffix, remove it before parsing validating source date format

Since you stated that: "The date format from my API is yyyy-mm-ddThh:mm:ss (without Z on the end)", the best way to parse it is explictly pass the format you are expecting to moment, using the right moment format tokens instead of using moment.ISO_8601.

So, in your case, simply use moment(myInt, "YYYY-MM-DDTHH:mm:ss", true), as shown in the snipppet:

function checkValid(input) {
  if (moment(input, "YYYY-MM-DDTHH:mm:ss", true).isValid()) {
    console.log(input + " is valid!");
  }
}

checkValid(1234);
checkValid("2021-04-27T20:40:15");
checkValid("2021-04-27T20:40:15Z");
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>


Please note that the Z at the end stands for the timezone offset UTC+0, if you have it moment takes it into account, while without it, moment parses the input as local time (see Local vs UTC vs Offset guide)

As a side note, moment.ISO_8601 works as you were expecting in moment versions prior to 2.25.0:

function checkValid(input) {
  if (moment(input, moment.ISO_8601, true).isValid()) {
    console.log(input + " is valid!");
  }
}

checkValid(1234);
checkValid("2021-04-27T20:40:15");
checkValid("2021-04-27T20:40:15Z");
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>

You could testify the sting before you pass it to the moment.? I have taken an example from this post

/**
 * RegExp to test a string for a full ISO 8601 Date
 * Does not do any sort of date validation, only checks if the string is according to the ISO 8601 spec.
 *  YYYY-MM-DDThh:mm:ss
 *  YYYY-MM-DDThh:mm:ssTZD
 *  YYYY-MM-DDThh:mm:ss.sTZD
 * @see: https://www.w3.org/TR/NOTE-datetime
 * @type {RegExp}
 */
var ISO_8601_FULL = /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+)?(([+-]\d\d:\d\d)|Z)?$/i


// Usage:

ISO_8601_FULL.test( "2016-05-24T15:54:14.876Z" )  // true
ISO_8601_FULL.test( "2002-12-31T23:00:00+01:00" ) // true
ISO_8601_FULL.test( "2016-02-01" )                // false
ISO_8601_FULL.test( "2016" )                      // false

if (ISO_8601_FULL.test(myDate) && moment(myDate, moment.ISO_8601, true).isValid()) {
    console.log("Valid!");
}

I suppose the date should not be an integer.

Interesting is the difference between string and number. If it's a number, it is interpreted as the number of milliseconds since epoc, which is quite helpful in computer languages, but obviously not always what was requested and also not obvious to every developer. This can easily avoided with a type check (typeof input != 'string').

The other variant is more confusing: "12345" is not valid. good. But "1234" is interpreted as a year, and at the same time "34" seems to be interpreted as a time offset in minutes (Sun Jan 01 1234 00:00:00 GMT+0034). To me, this clearly looks like a bug in the library, since it's quite useless to parse the same digits multiple times for different purposes. But also after this is fixed, "1234" stays a valid date (year only) as defined in the standard ISO 8601

https://en.wikipedia.org/wiki/ISO_8601

For reduced precision,[17] any number of values may be dropped from any of the date and time representations, but in the order from the least to the most significant. For example, "2004-05" is a valid ISO 8601 date, which indicates May (the fifth month) 2004. This format will never represent the 5th day of an unspecified month in 2004, nor will it represent a time-span extending from 2004 into 2005.

Btw: "543210" is also valid and means "5432-10", or October of the year 5432

function checkValid(input) {
  m = moment(input, true);
  console.log(input + "  type: " + typeof input + "  valid: " + m.isValid() + "  result:" + m.toString());
}

checkValid(1234);
checkValid("1234");
checkValid(12345);
checkValid("12345");
checkValid("2021-04-27T20:40:15");
checkValid("2021-04-27T20:40:15Z");
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>

Related