0

Let's say I have two different types of date strings - a mysql date stamp, and a mysql date+time stamp.

"2015-12-25"
"2015-12-25 00:00:00"

When I parse these in JavaScript, I get:

new Date("2015-12-25");
Thu Dec 24 2015 19:00:00 GMT-0500 (Eastern Standard Time)
new Date("2015-12-25 00:00:00");
Fri Dec 25 2015 00:00:00 GMT-0500 (Eastern Standard Time)

Or, in UTC:

new Date("2015-12-25").toUTCString();
"Fri, 25 Dec 2015 00:00:00 GMT"
new Date("2015-12-25 00:00:00").toUTCString();
"Fri, 25 Dec 2015 05:00:00 GMT"

Adding the time on the end somehow makes the date parse as being my-time-zone hours ahead. What I want is some kind of function to make these both parse as the same thing - GMT or no, I can adjust, as long as the outcome is known to be one or the other, no matter the date string passed in.

I got halfway through implementing a solution involving regexes and detecting date format before I figured that there is probably a better way to go about this.

I've found some solutions involving Date.prototype.getTimezoneOffset(), but for both of these dates that returns the same thing, since they are parsed as being in the same timezone.

Any ideas?

4
  • Can't you just trim the last chars from "2015-12-25 00:00:00" so that it just become "2015-12-25", if the input just has 2 cases? Commented Jun 27, 2015 at 12:50
  • It doesn't just have two cases - this is just an example. I would like to capture any time information, if it exists. I considered hacking extra 00:00:00s onto the end of strings, though. The trouble is determining the strings that you can tack this extra bit onto - otherwise, it returns Invalid Date. :( Commented Jun 27, 2015 at 13:04
  • Store time as an Int or in ISO 8601 Commented Jun 27, 2015 at 13:38
  • @PaulS. - For this particular problem, please assume that I have no control over date formatting in storage. Commented Jun 27, 2015 at 13:43

1 Answer 1

2

ECMAScript ed 5.1 (aka ES5) required that ISO 8601 date strings be parsed as UTC, however ISO required them to be parsed as local. Now the current ECMAScript standard (version 6) is consistent with ISO and requires ISO compliant strings with no timezone to be parsed as local.

<update> ECMAScript 2016 (ed 7) changed again so that YYYY-MM-DD format date strings are parsed as UTC, so no longer consistent with ISO 8601.</update>.

So whether you get one behaviour or the other depends on the browser (and if you try it with IE 8 you'll get NaN). Prior to ES5, parsing of all strings was implementation dependent.

"2015-12-25" is a valid ISO 8601 string with no timezone, so should be treated as UTC under ES5 rules and local by ed 6 rules.

"2015-12-25 00:00:00" is not a valid ISO string (missing the "T" between the date and time) so browsers can parse it however they like (parsing of non–ISO strings is implementation dependent in all versions of ECMAScript).

The bottom line is do not use the Date constructor to parse strings (or Date.parse). Parse them yourself.

To parse ISO-like strings, use something like the following. It treats strings with not offset as UTC (per ES5), however perhaps that should now be changed to be local:

/**
 * Parse an ISO string with or without an offset
 *   e.g. '2014-04-02T20:00:00-0600'
 *        '2014-04-02T20:00:00Z'
 *        '2014-02'
 *
 * Allows decimal seconds if supplied
 *   e.g. '2014-04-02T20:00:00.123-0600'
 *
 * If no offset is supplied (or it's Z), treat as UTC (per ECMA-262)
 *
 * If date only, e.g. '2014-04-02' or '2014-02', treat as UTC date (per ECMA-262)
 * All parts after year are optional
 * Don't allow two digit years to be converted to 20th century years
 * @param {string} s - ISO 860 date string
*/
function parseISOString(s) {
  var invalidDate = new Date(NaN);
  var t = s.split(/\D+/g);
  var hasOffset = /[-+]\d{4}$/.test(s);

  // Whether decimal seconds are present changes the offset field and ms value
  var hasDecimalSeconds = /[T ]\d{2}:\d{2}:\d{2}\.\d+/i.test(s);
  var offset = hasDecimalSeconds? t[7] : t[6];
  var offSign;
  var yr  = +t[0],
      mo  = t[1]? --t[1] : 0,
      da  = +t[2] || 1,
      hr  = +t[3] || 0,
      min = +t[4] || 0,
      sec = +t[5] || 0,
      ms  = hasDecimalSeconds? +t[6] : 0,
      offSign = hasOffset? /-\d{4}$/.test(s)? 1 : -1 : 0,
      offHr   = hasOffset? offset/100 | 0 : 0,
      offMin  = hasOffset? offset%100 : 0;

  // Ensure time values are in range, otherwise invalid date.
  // Values can't be -ve as splitting on non-digit character
  if (hr > 24 || min > 59 || sec > 59 || ms > 1000 || offHr > 24 || offMin > 59){
    return invalidDate;
  }

  // Create a date object from date parts, check for validity
  // Avoid two digit years being converted to 20th century
  var d = new Date();
  d.setUTCFullYear(yr, mo, da);

  // Check that date values are valid
  if (d.getUTCFullYear() != yr || d.getUTCDate() != da) {
    return invalidDate;
  }

  // If there's an offset, apply it to minutes to get a UTC time value
  min = hasOffset? +min + offSign * (offHr * 60 + offMin) : min;

  // Set UTC time values of d
  d.setUTCHours(hr, min, sec, ms);

  return d;
}

It can be reduced to less than half that, but it's written to be easy to read and maintain.

Sign up to request clarification or add additional context in comments.

2 Comments

Looks like my half-baked regex solution was closer than I thought. Also, what the hell, IE8? There's always more and it's always worse.
@pricklypear—IE 8 was released in 2009, two years before ES5 became the standard. Under ECMAScript ed 3, parsing of strings was entirely implementation dependent and most browsers didn't support ISO strings. Returning NaN was just their way of saying "I can't parse that string", per the standard, both then and now.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.