NormB
NormB

Reputation: 41

Date.parse unexpected return string

I know there are lots of answers out there on this topic (too many to read all of them), but I am failing to understand why Date.parse is returning

'Sun Oct 21 2018 00:00:00 GMT+0100 (GMT Summer Time)'

from the string '2018-10-20T23:00:00Z'

All times return +1hr

Am I missing the obvious? Presumably it is something to do with the +0100 GMT, but what do I need to do to make sure that it parses correctly?

Thanks

Upvotes: 0

Views: 266

Answers (2)

Jason Ching
Jason Ching

Reputation: 2229

Date object represents a single moment of time, regardless of time zone.

Think about this, right at this moment, your clock in GMT+1 timezone is showing 15:00. The other person's clock living in GMT+0 is showing 14:00. The clock (representation of time) is different, but the moment of time (the value in the Date object) is the same.

This is what you are seeing in the code. 2018-10-20T23:00:00Z equals to 2018-10-21T00:00:00 GMT+1. By equal I mean they are the exact same moment of time, but just represent in different time zone.

Upvotes: 0

RobG
RobG

Reputation: 147363

Firstly, I would strongly recommend that you never use the Date constructor (or Date.parse, they are equivalent for parsing) to parse strings. Always use a small function or suitable library (suggestions with links below).

If parsed per ECMA-262, then '2018-10-20T23:00:00Z' will be parsed as UTC. If you send the resulting date to output using toString (or via a method that calls toString like console.log(new Date())), then generally the host timezone is used to compute "local" values.

The format of the string produced by toString is implementation dependent, so may not include the timezone, or might display it in an unexpected manner, and might be different in different hosts.

According to ECMA-262, if you want '2018-10-20T23:00:00Z' to be treated as local, remove the "Z":

var s = '2018-10-20T23:00:00Z';
var t = s.replace(/z$/i,'');
console.log(t);
console.log(new Date(t).toString());

However, following on from my first comment, Safari 10.0.3 seems to treat the string as UTC even if the Z is omitted, so your results may be incorrect depending on the host. Firefox seems to get it right.

I can't stress strongly enough that you should not rely on Date or Date.parse for parsing. Ever.

While writing your own parser for a particular format is easy, some feel much better using a library. Consider fecha.js (which is small and does parsing and formatting) or moment.js (which is not exactly petite but also helps with arithmetic and can include timezone functionality).

E.g. here is a small ISO extended format parser that tries to be as compatible as possible and use Date methods as little as possible:

/* Parse ISO date string in format yyyy-mm-ddThh:mm:ss.sss+hh:mm or Z
** @param (string} s - string to parse in ISO 8601 extended format
**                     yyyy-mm-ddThh:mm:ss.sss+/-hh:mm or z
**                     time zone can omit separator, so +05:30 or +0530
** @returns {Date}   - returns a Date object. If any value out of range,
**                     returns an invalid date.
*/
function parseISO(s) {
  // Create base Date object
  var date = new Date();
  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[1] > 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 member 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;
}

console.log('UTC  : ' + parseISO('2018-10-20T23:00:00Z').toString());
console.log('Local: ' + parseISO('2018-10-20T23:00:00').toString());

Upvotes: 1

Related Questions