1

I have an API that provides date in the following format:

yyyy-MM-dd'T'HH:mm:ssZ, which is something like, for instance: 2017-04-18T11:18:05-0300. Chrome can build a date from such a string, but IE can't. Is there a way I can force IE to accept this format as well, what looks the most easy way to fix this problem?

I tried something like the snippet below but it didn't work

(function() {

var originalDateFn = Date;

var month_names_short =  ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

Date = function(date) {
   var splitted = splitIfOnApiFormat(date);
   if(splitted !== null) {
     return new Date( month_names_short[splitted[2]] + ' ' +  splitted[3] + ' ' +  splitted[1] + ' ' + splitted[4] +' GMT'+ splitted[5]);
   }
   return originalDateFn(date);
};

function splitIfOnApiFormat(date) {
  return typeof date === 'string' && date.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}\:\d{2}\:\d{2})([-+]\d{4})$/);
}



})();
Leonardo Lana
  • 590
  • 1
  • 4
  • 13
  • When working with dates in JS I suggest using moment.js. http://momentjs.com/ – gforce301 Apr 18 '17 at 14:22
  • Possible duplicate of [Why does Date.parse give incorrect results?](http://stackoverflow.com/questions/2587345/why-does-date-parse-give-incorrect-results) – CBroe Apr 18 '17 at 14:24
  • Actually I do use momentjs but date input component uses Date itself, so in my case making it work on IE too would be a needed workaround – Leonardo Lana Apr 18 '17 at 14:25
  • If all platforms implemented all standards properly, then libraries like moment.js would most likely never even get started. Just saying. What "should not be necessary" and what is necessary are generally a long way apart. – gforce301 Apr 18 '17 at 17:16
  • @gforce301—but there are many browsers in use that were created before the current standard required ISO 8601 strings to be parsed correctly. – RobG Apr 18 '17 at 23:16
  • @gforce301 But IE **does** implement the standard properly. The problem is that `2017-04-18T11:18:05-0300` does not conform to the standard, which requires `03:00`. –  Apr 19 '17 at 04:26
  • @torazaburo—well, it doesn't conform to ECMA-262, but it does conform with ISO 8601 (which allows ±hhmm, ±hh:mm and ±hh), which is another variance between the two. ;-) I fail to understand why EMCA-262 doesn't just conform with ISO 8601. – RobG Apr 20 '17 at 03:34
  • @RobG Right, I was referring to ECMA-262 which is the relevant standard here. I'm sure there is some story behind why `Date.parse` is not defined to fully parse ISO 8601 dates, but I don't know what it is. –  Apr 20 '17 at 04:58
  • @torazaburo—see [*Date Time String Format: default time zone difference from ES5 not web-compatible*](https://github.com/tc39/ecma262/issues/87) for why date only forms are treated as UTC. It seems the commercial interests of a single influential member were put before common sense. – RobG Apr 20 '17 at 06:14

3 Answers3

1

The problem in this case is the missing : between hours and minutes in the offset part at the end. In this regard, IE is within its rights to report this as an invalid date, since it does not conform with the definition of date in the spec, which is YYYY-MM-DDTHH:mm:ss.sssZ or YYYY-MM-DDTHH:mm:ss.sss±hh:mm (with a colon).

You can fix this with:

const date = "2017-04-18T11:18:05-0300";

function fixDate(date) {
  return date.replace(/(\d\d)(\d\d)$/, 
    function(_, hh, mm) { return hh + ':' + mm; });
}

console.log(new Date(fixDate(date)));
  • Thank you so much buddy, much simpler and more correct than my answer below, actually I will change the API to provide dates in this format. Perfect as you also referred to the spec which I didn't know – Leonardo Lana Apr 19 '17 at 11:27
0

The golden rule is to never parse strings with the built-in parser (i.e. using either the Date constructor or Date.parse). You have already broken the string into parts, so just pass them to the Date constructor and adjust for the time zone.

The following will parse an ISO 8601 extended format string as specified in ECMA-262, hopefully the comments are sufficient.

/* Parse string in ISO 8601 extended format: YYYY-MM-DDTHH:mm:ss.sssZ.
 ** Date only strings are treated as local (per ISO 8601)
 **
 ** @param {string} s - string to parse. Can be just a date, date and time,
 **                     or date, time and timezone.
 **                     Separator can be T or " " (space) 
 **                     Date string must be at least year and month: YYYY-MM 
 **                     All time parts are optional, if time provided then
 **                     date must have year, month and day.
 **                     All time parts are optional.
 **                     Timezone, if present, must be Z or z or +/-HH:mm or +/-HHmm
 **
 ** @returns {Date}   - if s is a invalid date, contains invalid values,
 **                     or is an invalid format, returns an invalid Date
 */
function parseISO(s) {

  // Create base Date object
  var date = new Date();
  date.setHours(12, 0, 0, 0);
  var invalidDate = new Date(NaN);

  // Set some defaults
  var sign = -1,
    tzMins = 0;
  var tzHr, tzMin;

  // Trim leading and trailing whitespace
  s = s.replace(/^\s*|\s*$/g, '').toUpperCase();

  // Get parts of string and split into numbers
  var d = (s.match(/^\d+(-\d+){0,2}/) || [''])[0].split(/\D/);
  var t = (s.match(/[\sT]\d+(:\d+){0,2}(\.\d+)?/) || [''])[0].split(/\D/);
  var tz = (s.match(/Z|[+\-]\d\d:?\d\d$/) || [''])[0];

  // Resolve timezone to minutes, may be Z, +hh:mm or +hhmm
  // substr is old school but more compatible than slice
  // Don't need to split into parts but makes validation easier
  if (tz) {
    sign = /^-/.test(tz) ? 1 : -1;
    tzHr = tz == 'Z' ? 0 : tz.substr(1, 2);
    tzMin = tz == 'Z' ? 0 : tz.substr(tz.length - 2, 2) * 1;
    tzMins = sign * (tzHr * 60 + tzMin);
  }

  // Validation
  function isLeap(year) {
    return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0);
  }

  // Check number of date parts and month is valid
  if (d.length > 3 || d[1] < 1 || d[1] > 12) return invalidDate;

  // Test day is valid
  var monthDays = [, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  var monthMax = isLeap(d[0]) && d[1] == 2 ? 29 : monthDays[+d[1]];
  if (d[2] < 1 || d[2] > monthMax) return invalidDate;

  // Test time parts
  if (t.length > 5 || t[1] > 23 || t[2] > 59 || t[3] > 59 || t[4] > 999) return invalidDate;

  // Test tz within bounds
  if (tzHr > 12 || tzMin > 59) return invalidDate;

  // If there's a timezone, use UTC methods, otherwise local
  var method = tz ? 'UTC' : '';

  // Set date values
  date['set' + method + 'FullYear'](d[0], (d[1] ? d[1] - 1 : 0), d[2] || 1);
  // Set time values - first memeber is '' from separator \s or T
  date['set' + method + 'Hours'](t[1] || 0, (+t[2] || 0) + tzMins, t[3] || 0, t[4] || 0);

  return date;
}

// Some tests
['2017-04-18T11:18:05-0300',  // No colon in timezone
  '2017-04-18 11:18:05-0300', // Space separator instead of T
  '2016-04-12T04:31+05:30',   // Colon in timezone
  '2016-02-29T04:31+05:30',   // Colon, leap year
  '2016-04-12T04:31:56.004Z', // GMT
  '2000-02-29T04:31+05:30',   // Colon, leap year
  '1900-02-29T04:31+05:30',   // Colon, not leap year (invalid date)
  '2016-04-12T04:31:56.004',  // Local
  '2016-04-12',               // Date only (local)
  '2016-04'                   // Minimal date (local)
].forEach(function(d) {
  console.log(d + ': ' + parseISO(d).toString());
});

Alternatively, use a library, there are plenty of good ones to choose from.

RobG
  • 142,382
  • 31
  • 172
  • 209
  • The exception is if you are sure the input is in `YYYY-MM-DDThh:mm:ss.sssZ` or `YYYY-MM-DDThh:mm:ss.sss±hh:mm` form, you can safely use `Date.parse` or `new Date`. –  Apr 19 '17 at 04:07
  • @torazaburo—I still don't trust it. There are lots of quirks with built-in parsers, so much better to not use them at all—IMHO of course. ;-) – RobG Apr 19 '17 at 05:30
-1

OK, got it For anyone else who may need, the following code fixed it:

(function() {

var originalDateFn = Date.prototype.constructor;

var month_names_short =  ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];

Date = function(date) {
   var splitted = splitIfOnApiFormat(date);
   if(splitted !== null) {
     return new originalDateFn( month_names_short[(Number(splitted[2])) -1] + ' ' +  splitted[3] + ' ' +  splitted[1] + ' ' + splitted[4] +' GMT'+ splitted[5]);
   }
   return new originalDateFn(date);
};

function splitIfOnApiFormat(date) {
  return typeof date === 'string' ? date.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}\:\d{2}\:\d{2})([-+]\d{4})$/) : null;
}



})();
Leonardo Lana
  • 590
  • 1
  • 4
  • 13
  • No!! Once you have parsed the string, give the parts **directly** to the constructor, don't make another string that must then be parsed. See [*Why does Date.parse give incorrect results?*](http://stackoverflow.com/questions/2587345/why-does-date-parse-give-incorrect-results/20463521?s=11|0.4677#20463521) – RobG Apr 18 '17 at 20:33
  • Your regexp will fail if there are fractional seconds. –  Apr 19 '17 at 04:27