1

Let's say we're in London at midnight on 2020-01-01 and make an entry into an app that stores the datetime as an ISO-8601 string like this.
2020-01-01T00:00:00-00:00

Later, I am in Los Angeles and want to view this date on a chart that requires a javascript date object.

Getting the localized date object is easy.

const iso8601Date = '2020-01-01T00:00:00+00:00';
const theDate = new Date(iso8601Date);

console.log(typeOf(theDate)); // date
console.log(theDate);        // Tue Dec 31 2019 16:00:00 GMT-0800 (PST)

But, sometimes we want to "ignore" the timezone offset and analyze the data as if it happened in the current timezone.

This is the result I'm looking for but don't know how to accomplish.

const iso8601Date = '2020-01-01T00:00:00+00:00';
const theRepositionedDate = someMagic(iso8601Date);

console.log(typeOf(theRepositionedDate)); // date
console.log(theRepositionedDate);         // Wed Jan 01 2020 00:00:00 GMT-0800 (PST)

How do you reposition the date and return a date object?



/* Helper function

Returns the object type
https://stackoverflow.com/a/28475133/25197
    typeOf(); //undefined
    typeOf(null); //null
    typeOf(NaN); //number
    typeOf(5); //number
    typeOf({}); //object
    typeOf([]); //array
    typeOf(''); //string
    typeOf(function () {}); //function
    typeOf(/a/) //regexp
    typeOf(new Date()) //date
*/

function typeOf(obj) {
  return {}.toString
    .call(obj)
    .split(' ')[1]
    .slice(0, -1)
    .toLowerCase();
}
Community
  • 1
  • 1
GollyJer
  • 23,857
  • 16
  • 106
  • 174

4 Answers4

3

This is really a duplicate of Why does Date.parse give incorrect results?, but that may not seem apparent at first glance.

The first rule of parsing timestamps is "do not use the built–in parser", even for the 2 or 3 formats supported by ECMA-262.

To reliably parse a timestamp, you must know the format. Built–in parsers try and work it out, so there are differences between them that may well produce unexpected results. It just happens that '2020-01-01T00:00:00+00:00' is probably the only supported format that is actually reliably parsed. But it does differ slightly from strict ISO 8601, and different browsers differ in how strictly they apply the ECMAScript parsing rules so again, very easy to get wrong.

You can convert it to a "local" timestamp by just trimming the offset information, i.e. '2020-01-01T00:00:00', however Safari at least gets it wrong and treats it as UTC anyway. ECMAScrip itself is inconsistent with ISO 8601 by treating date–only forms of ISO 8601 as UTC (i.e. '2020-01-01' as UTC when ISO 8601 says to treat it as local).

So just write your own parser or use a library, there are plenty to choose from. If you're only looking for parsing and formatting, there are some that are less than 2k minified (and there are examples on SO).

Writing your own is not that challenging if you just want to support straight forward ISO 8601 like formats, e.g.

// Parse ISO 8601 timestamps in YYYY-MM-DDTHH:mm:ss±HH:mm format
// Optional "T" date time separator and
// Optional ":" offset hour minute separator
function parseIso(s, local) {
  let offset = (s.match(/[+-]\d\d:?\d\d$/) || [])[0];
  let b = s.split(/\D/g);
  // By default create a "local" date
  let d = new Date(
    b[0],
    b[1]-1,
    b[2] || 1,
    b[3] || 0,
    b[4] || 0,
    b[5] || 0
  );
  // Use offset if present and not told to ignore it
  if (offset && !local){
    let sign = /^\+/.test(offset)? 1 : -1;
    let [h, m] = offset.match(/\d\d/g);
    d.setMinutes(d.getMinutes() - sign * (h*60 + m*1) - d.getTimezoneOffset());
  }
  return d;
}

// Samples
['2020-01-01T00:00:00+00:00', // UTC, ISO 8601 standard
 '2020-01-01 00:00:00+05:30', // IST, missing T
 '2020-01-01T00:00:00-0400',  // US EST, missing T and :
 '2020-01-01 00:00:00',       // No timezone, local always
 '2020-01-01'                 // Date-only as local (differs from ECMA-262)
].forEach(s => {
  console.log(s);
  console.log('Using offset\n' + parseIso(s).toString());
  console.log('Ignoring offset\n' + parseIso(s, true).toString());
});
RobG
  • 142,382
  • 31
  • 172
  • 209
  • Hey @RobG. I worked all day on this with no success. I'm VERY grateful for your response. It's working perfectly. – GollyJer Apr 16 '20 at 06:46
  • Thanks again for this. I played around it some more today looking to speed up as much as possible. Posted on [codereview.stackoverflow](https://codereview.stackexchange.com/q/240906/30306) if you're interested in the results. – GollyJer Apr 21 '20 at 01:14
1

Building off of @RobG's answer I was able to speed this one up a little by using a single regex. Posting here for posterity.

const isoToDate = (iso8601, ignoreTimezone = false) => {
  // Differences from default `new Date()` are...
  // - Returns a local datetime for all without-timezone inputs, including date-only strings.
  // - ignoreTimezone processes datetimes-with-timezones as if they are without-timezones.
  // - Accurate across all mobile browsers.  https://stackoverflow.com/a/61242262/25197

  const dateTimeParts = iso8601.match(
    /(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2}):(\d{2})(?:\.(\d{0,7}))?(?:([+-])(\d{2}):(\d{2}))?)?/,
  );

  // Create a "localized" Date by always specifying a time. If you create a date without specifying
  // time a date set at midnight in UTC Zulu is returned.  https://www.diigo.com/0hc3eb
  const date = new Date(
    dateTimeParts[1], // year
    dateTimeParts[2] - 1, // month (0-indexed)
    dateTimeParts[3] || 1, // day
    dateTimeParts[4] || 0, // hours
    dateTimeParts[5] || 0, // minutes
    dateTimeParts[6] || 0, // seconds
    dateTimeParts[7] || 0, // milliseconds
  );

  const sign = dateTimeParts[8];
  if (sign && ignoreTimezone === false) {
    const direction = sign === '+' ? 1 : -1;
    const hoursOffset = dateTimeParts[9] || 0;
    const minutesOffset = dateTimeParts[10] || 0;
    const offset = direction * (hoursOffset * 60 + minutesOffset * 1);
    date.setMinutes(date.getMinutes() - offset - date.getTimezoneOffset());
  }

  return date;
};

The key difference is a single regex that returns all the matching groups at once.

Regular expression visualization

Here's a regex101 with some examples of it matching/grouping.


It's about double the speed of the @RobG's awesome accepted answer and 4-6x faster than moment.js and date-fns packages.

GollyJer
  • 23,857
  • 16
  • 106
  • 174
0

const createDate = (isoDate) => {
  isoDate = new Date(isoDate)

  return new Date(Date.UTC(
    isoDate.getUTCFullYear(),
    isoDate.getUTCMonth(),
    isoDate.getUTCDate(),
    isoDate.getUTCMinutes(),
    isoDate.getUTCSeconds(),
    isoDate.getUTCMilliseconds()
  ));
}

const iso8601Date = '2020-01-01T00:00:00+00:00';
const theRepositionedDate = createDate(iso8601Date);

console.log(theRepositionedDate instanceof Date); // true
console.log(theRepositionedDate);
aagamezl
  • 49
  • 3
  • Thanks... but that will return a `string`. I need a `date`. – GollyJer Apr 16 '20 at 02:34
  • You can use UTC methods such as Date.prototype.getUTCDate() to get the values ​​you want. – aagamezl Apr 16 '20 at 02:51
  • Thanks for the help @aagamezl. I've tried many many things including a bunch of things from other questions here on stackoverflow but I'm unable to get the result I'm looking for. An answer with working code would be super helpful. – GollyJer Apr 16 '20 at 02:55
  • @GollyJer can you validate if my solution works for you? – aagamezl Apr 16 '20 at 03:20
  • 1
    Problem here is it assumes the input offset will always be zero (UTC). The OP said the data could be from London. The UK uses +00:00 in the winter, but +01:00 in the summer. – Matt Johnson-Pint Apr 16 '20 at 03:28
  • This doesn't work, e.g. "2020-01-01T00:00:00+10:00" is returned as "2019-12-31T00:00:00.000Z". Code without explanation is not particularly helpful as you haven't said what you think the code actually does or how to use it. Even if parsing fails, it will still return a valid Date instance, however the time value will be set to NaN so *instanceof* is pointless. – RobG Apr 16 '20 at 04:10
  • 1
    @aagamezl Please don't get discouraged because of this first attempt. Keep helping! – GollyJer Apr 16 '20 at 16:53
0

But, sometimes we want to "ignore" the timezone offset and analyze the data as if it happened in the current timezone.

Ok, then ignore it.

const iso8601Date = '2020-01-01T00:00:00+00:00';
const theDate = new Date(iso8601Date.substring(0, 19));

This works because you're creating a Date object from 2020-01-01T00:00:00 - an ISO 8601 date-time without offset.

ECMAScript section 20.3.1.15 - Date Time String Format says:

When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as a local time.

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • Yeah, but Safari still gets it wrong and treats 2020-01-01T00:00:00 as UTC. Pity about [*Date Time String Format: default time zone difference from ES5 not web-compatible*](https://github.com/tc39/ecma262/issues/87). :-( – RobG Apr 16 '20 at 03:36
  • @RobG - Arrrggghhh... You're right. Safari latest (13) on Mac has not implemented this correctly. ([Screenshot](https://i.stack.imgur.com/u7Ail.png)) – Matt Johnson-Pint Apr 16 '20 at 15:19
  • FYI - I reported the bug to Apple using their Feedback Assistant tool. (which doesn't give a public url to link to here, sorry) – Matt Johnson-Pint Apr 16 '20 at 15:43