12

Run this jsfiddle: http://jsfiddle.net/E9gq9/7/ on Chrome, FF, and IE and you get:

Chrome:

Chrome http://images.devs-on.net/Image/vBTz86J0f4o8zlL3-Region.png

Firefox:

Firefox http://images.devs-on.net/Image/aNPxNPUpltyjVpSX-Region.png

IE:

IE http://images.devs-on.net/Image/WXLM5Ev1Viq4ecFq-Region.png

Safari:

Safari http://images.devs-on.net/Image/AEcyUouX04k2yIPo-Region.png

ISO 8601 does not appear to say how a string without a trailing Z should be interpreted.

Our server (ASP.NET MVC4) is pulling UTC times out of our database as DateTimes and simply stuffing them in JSON. As you can see because of this we are getting inconsistent results on the browser.

Should we just append Z to them on the server side?

tig
  • 3,424
  • 3
  • 32
  • 65

5 Answers5

15

Should we just append Z to them on the server side?

TL;DR Yes, you probably should.

Correct handling of dates and date/times without a timezone has, sadly, varied through the years — both in terms of specification and JavaScript engines adherence to the specification.

When this answer was originally written in 2013, the ES5 spec (the first to have a defined date/time format for JavaScript, which was meant to be a subset of ISO-8601) was clear: No timezone = UTC:

The value of an absent time zone offset is “Z”.

But that was at odds with ISO-8601, in which the absense of a timezone indicator means "local time." Some implementations never implemented ES5's meaning, sticking instead to ISO-8601.

In ES2015 (aka "ES6"), it was changed to match ISO-8601:

If the time zone offset is absent, the date-time is interpreted as a local time.

However, this caused incompatibility problems with existing code, particularly with date-only forms like 2018-07-01, so in ES2016 it was changed yet again:

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.

So new Date("2018-07-01") is parsed as UTC, but new Date("2018-07-01T00") is parsed as local time.

It's been consistent since, in ES2017 and in the upcoming ES2018; here's the link to the current editor's draft, which no longer has that exact text but still defines it the same way (though IMHO less clearly).

You can test your current browser here:

function test(val, expect) {
  var result = +val === +expect ? "Good" : "ERROR";
  console.log(val.toISOString(), expect.toISOString(), result);
}
test(new Date("2018-07-01"), new Date(Date.UTC(2018, 6, 1)));
test(new Date("2018-07-01T00:00:00"), new Date(2018, 6, 1));

Status as of September 2021:

  • Modern versions of Firefox, Chrome, and Safari get this right, including iOS browsers (which all currently use Apple's JavaScriptCore JavaScript engine since Apple won't let them use their own if they do JIT)
  • IE11 gets it right (interestingly)

Status as of April 2018:

  • IE11 gets it right (interestingly)
  • Firefox gets it right
  • Chrome 65 (desktop, Android) gets it right
  • Chrome 64 (iOS v11.3) gets the date/time form wrong (parses as UTC)
  • iOS Safari in v11.3 gets the date/time form wrong (parses as UTC)

Oddly, I can't find an issue in the v8 issues list that's been fixed between v6.4 (the v8 in Chrome 64) and v6.5 (the v8 in Chrome 65); I can only find this issue which is still open, but which appears to have been fixed.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • However, you should notice as well that [older IEs fail to parse ISO 8601 entirely](http://msdn.microsoft.com/en-us/library/ie/ff743760(v=vs.94).aspx). Nothing in JS date parsing is consistent :-( – Bergi Feb 17 '13 at 17:47
  • 5
    Wow. That's definitely what the ECMAScript spec says - but it's very broken, IMO - and directly against ISO-8601, which states: "The zone designator is empty if use is made of local time in accordance with 4.2.2.2 through 4.2.2.4." Sigh. Mind you, given that it's going to be parsed as just an instant in time, I'd argue the whole API is broken. Double sigh. – Jon Skeet Feb 17 '13 at 17:47
  • @Bergi: That's because this is new as of ECMAScript5. Amazingly, even ECMAScript3 didn't dictate **any** date/time string that an engine had to parse, other than to say that it had to parse whatever `Date#toString` spat out -- but it didn't dictate what that was. The good news is that there is no major browser that won't parse `yyyy/MM/dd` (has to be `/`, not `-`), even though it was never standardized. The bad news is that different browsers make different calls on whether that should be local or UTC. – T.J. Crowder Feb 17 '13 at 22:45
  • @JonSkeet: It **is** truly amazing that the committee doing ECMAScript5 based the date/time format *explicitly* on ISO-8601 (a "simplified" form, as ISO-8601 in full is ridiculously complex), and yet diverged in that regard. Astonishing, really. And yet, it would so appear... – T.J. Crowder Feb 17 '13 at 22:53
  • There have been some changes over the years: – Tom Apr 15 '18 at 11:37
  • ECMAScript 5: "The value of an absent time zone offset is “Z”." – Tom Apr 15 '18 at 11:37
  • ECMAScript 6: "If the time zone offset is absent, the date-time is interpreted as a local time." – Tom Apr 15 '18 at 11:38
  • ECMAScript 7 and 8: "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." – Tom Apr 15 '18 at 11:38
  • @Tom: Yeah, sadly. :-( – T.J. Crowder Apr 15 '18 at 11:47
  • @T.J.Crowder, very detailed answer. But what about Safari, It seems that the result is still wrong in it. Is there any bug report for Safari engine? – Yuri Beliakov Sep 15 '21 at 15:14
  • @YuriBeliakov - When I run the test code above in Safari on iOS (14.8), I get the right result. What version of Safari are you seeing where it's wrong? – T.J. Crowder Sep 15 '21 at 15:21
  • @T.J.Crowder Thanks for the quick response. I have tested in Safari 12.1.2. – Yuri Beliakov Sep 15 '21 at 16:57
  • @YuriBeliakov - Wow, they really took their time fixing it. :-D But at least it is fixed somewhere between that and 14.8. I hope it was in a release early enough that all devices can get it; my wife, for instance, is still happy with her iPhone 5 and it can't install the newer releases (just security updates). – T.J. Crowder Sep 15 '21 at 17:23
  • 1
    Well, it's good that it works as expected in the latest version of Safari. Thanks for your time! – Yuri Beliakov Sep 15 '21 at 17:31
6

At the end of the day, the problem I'm facing in this app can be fixed if my server always sends the client DateTime objects in a format that all browsers deal with correctly.

This means there must be that 'Z' on the end. Turns out the ASP.NET MVC4 Json serializer is based on Json.NET, but does not have Utc turned on by default. The default for DateTimeZoneHandling appears to be RoundtripKind and this does not output the values with Z on them, even if DateTime.Kind == Utc, which is quite annoying.

So the fix appears to be, set the way Json.NET handles timezones to DateTimeZoneHandling.Utc:

var json = GlobalConfiguration.Configuration.Formatters.JsonFormatter;
// Force Utc formatting ('Z' at end). 
json.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;

Now, everything coming down the wire from my server to the browser is formatted as ISO-8601 with a 'Z' at the end. And all browsers I've tested with do the right thing with this.

tig
  • 3,424
  • 3
  • 32
  • 65
2

I ran into this problem, and interpreting the date with the local timezone made a lot more sense than changing to "Z", at least for my application. I created this function to append the local timezone info when it is missing in the ISO date. This can be used in place of new Date(). Partially derived from this answer: How to ISO 8601 format a Date with Timezone Offset in JavaScript?

parseDate = function (/*String*/ d) {
    if (d.search(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/) == 0) {
        var pad = function (num) {
            norm = Math.abs(Math.floor(num));
            return (norm < 10 ? '0' : '') + norm;
        },
        tzo = -(new Date(d)).getTimezoneOffset(),
        sign = tzo >= 0 ? '+' : '-';
        return new Date(d + sign + pad(tzo / 60) + ':' + pad(tzo % 60));
    } else {
        return new Date(d);
    }
}
Community
  • 1
  • 1
David Hammond
  • 3,286
  • 1
  • 24
  • 18
0

David Hammond's answer is great but doesn't play all the tricks; so here is a modified version:

  • allows for fractional part in date/time string
  • allows for optional seconds in date/time string
  • considers crossing over daylight-saving time
appendTimezone = function (/*String*/ d) {
    // check for ISO 8601 date-time string (seconds and fractional part are optional)
    if (d.search(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1-3})?)?$/) == 0) {
        var pad = function (num) {
            norm = Math.abs(Math.floor(num));
            return (norm < 10 ? '0' : '') + norm;
        },
        tzo = -new Date(d).getTimezoneOffset(),
        sign = tzo >= 0 ? '+' : '-';

        var adjusted = d + sign + pad(tzo / 60) + ':' + pad(tzo % 60);

        // check whether timezone offsets are equal;
        // if not then the specified date is just within the hour when the clock
        // has been turned forward or back
        if (-new Date(adjusted).getTimezoneOffset() != tzo) {
            // re-adjust
            tzo -= 60;
            adjusted = d + sign + pad(tzo / 60) + ':' + pad(tzo % 60);
        }

        return adjusted;
    } else {
        return d;
    }
}

parseDate = function (/*String*/ d) {
    return new Date(appendTimezone(d));
}
Community
  • 1
  • 1
klaus triendl
  • 1,237
  • 14
  • 25
0

In addition to @tig's answer (which was exactly what I was looking for):

Here's the solution for .NetCore 1

services.AddMvc();
services.Configure<MvcJsonOptions>(o =>
{
    o.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
});

or

services.AddMvc().AddJsonOptions(o => o.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc);

For .NetCore 1.0.1

services
    .AddMvcCore()
    .AddJsonFormatters(o => o...);
themenace
  • 2,601
  • 2
  • 20
  • 33