DEV Community

Kasey Speakman
Kasey Speakman

Posted on

Oh Javascript... πŸ™„

Just ran across this little gem.

new Date('2018-01-01')
<- Sun Dec 31 2017 18:00:00 GMT-0600 (Central Standard Time)

new Date('2018/01/01')
<- Mon Jan 01 2018 00:00:00 GMT-0600 (Central Standard Time) 
Enter fullscreen mode Exit fullscreen mode

The reason this happens (I think) is because JS parses the first date as ISO 8601 format. And since no timezone offset is specified it assumes UTC. However, JavaScript will only create Dates in local time. So after applying the local time zone, it is 6pm the previous day.

The second one uses a / separator, so it does not trigger the ISO parse branch. Instead it sensibly presumes I am interested in a date in the current time zone.

Top comments (22)

Collapse
 
variadicism profile image
Connor Davey

Firstly, thank you very much for making this post before it tripped me up at some point! This seems like something that I would spend way too much time debugging.

Now then...

This Date behavior is not just unintuitive; it is incorrect.

I suspect that your supposition is correct that a - causes the date to be parsed as ISO 8601. However, even if this is the case, using ISO 8601 does not imply that the date time is in UTC. In the absence of a time zone designation, ISO 8601 is supposed to assume local time zone, not UTC. If the date time is in UTC, ISO 8601 expects a standard time zone definition like "+0:00" or the special "Z" designation appended to the end.

Wait! It gets worse! If you add a time to this date, it no longer assumes UTC time!

new Date('1995-12-17')
> Sat Dec 16 1995 17:00:00 GMT-0700 (MST)
new Date('1995-12-17T00:00:00')
> Sun Dec 17 1995 00:00:00 GMT-0700 (MST)

In conclusion, a - separator does not in any way, shape, or form imply UTC time... except in JavaScript's Date library and only if no time is provided. This is bad behavior.

Collapse
 
squgeim profile image
Shreya Dahal

Here's a quote from the ECMAScript 5 specification:

All numbers must be base 10. If the MM or DD fields are absent β€œ01” is used as the value. If the HH, mm, or ss fields are absent β€œ00” is used as the value and the value of an absent sss field is β€œ000”. The value of an absent time zone offset is β€œZ”.

So a lack of timezone offset is treated as UTC.

Collapse
 
variadicism profile image
Connor Davey

Thanks for the reference! This explains the behavior described in the original post.

However, I then argue that this standard is bad. Though I'm having trouble finding any truly official documentation, everything I do find (e.g., Wikipedia and this document) says that according to ISO 8601, in the absence of a time zone, local time should be used, not UTC. So, the browser is behaving correctly with these bare dates, but based on what I argue is a flawed standard that conflicts with ISO 8601.

Even more bothersome, though, is that this still doesn't explain why adding an explicit time switches to local time instead of UTC! I looked at the document linked, not just the quote, but still as far as I can tell, the standard you linked seems to indicate that new Date('1995-12-17T00:00:00') should still be considered UTC and result in Sat Dec 16 1995 17:00:00 GMT-0700 (MST), but it is read in local time instead!

This makes me sad. I don't understand why this is.

Thread Thread
 
squgeim profile image
Shreya Dahal

What's more troublesome is that that is inconsistent across different browsers: new Date('1995-12-17T00:00:00') is parsed as UTC on Safari.

According to the specs, strings that do not confirm to ISO 8601 are open for interpretation by difference implementation (like April 13 is April 13 2001 on Chrome, but doesn't work on other browsers). So my first guess was that this string just did not register as ISO 8601 on Chrome and it fell back to its own Interpretation.

I went to the latest ES7 specs and it has changed:

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 we can have different results based on what version of the browser we are using.

Collapse
 
kspeakman profile image
Kasey Speakman

Wow, it is much worse than I thought! I think it is fair to categorize this in "the truly awful parts" of Javascript.

For the most part, I avoid working with dates in JS. In client-server apps I deal with them on the server and send dates back only as display strings for the client where possible. But in this case, I am doing a client-side-only app which needs to process dates and schedules. So I have to try to use the "least bad parts". I'm half-tempted to port moment.js or NodaTime to Elm.

Collapse
 
variadicism profile image
Connor Davey

At my company, we use moment.js. My colleagues recommend that.

Thread Thread
 
squgeim profile image
Shreya Dahal

Using moment does not solve this issue. Initializing date with a string that is not strictly ISO 8601 is inconsistent in the specification itself and depends on the browser.

On Safari:

> new Date('2018-3-14')
β‹– Invalid Date
> moment('2018-3-14')
β‹– Invalid Date

This works on Chrome, though. :D

More about the moment issue here: github.com/moment/moment/issues/1407

I have a more thorough write-up on this with a lot more caveats here: blog.lftechnology.com/date-ing-jav...

Thread Thread
 
kspeakman profile image
Kasey Speakman

Thanks for the link. That is a great article on many of the JS Date caveats.

Collapse
 
lioobayoyo profile image
lioobayoyo • Edited

As much as I hate native Date handling in Javascript, this one doesn't strike me as especially bad (at least not worse than the rest of Date).

Without specifying a time part, it assumes 0:00:00. Sounds fine.

Using your locale notation, it assume local client timezone.
Using ISO 8601 notation "yyyy-mm-dd", it assumes UTC timezone.

The basic problem is that you can't ignore timezones in JS when dealing with abstract dates which don't have time info with them (like a birthday).

In C# for instance you can deal with datetimes with "Unspecified" offset.

Collapse
 
kspeakman profile image
Kasey Speakman

You are right, it is a relatively minor tripping hazard. JS has lots of those. But it is one that I tripped over recently. :)

The unexpected behavior is that simply changing the separator character changes the result. Most of the time, users will consider common date separators like hyphen and slash interchangeable. Now I have to either write extra code to remediate that expectation or confuse users when it does not work as expected (bad UX, and a non-option).

Also, my locale does not use that date notation. I would be surprised if JS Date parsing took locale into consideration.

Collapse
 
tunaxor profile image
Angel Daniel Munoz Gonzalez

I've run across this issue several times over the last year, it was mostly because we were receiving the format that includes / from the client and the fact that our clients had a different timezone than our servers (which was our fault because that solution was supposed to be localized to the country) we end up changing to moment for anything that has to do with dates and converting any date that came with a /

Collapse
 
ben profile image
Ben Halpern

Do any JS linting libraries yell at you about these sorts of things? And if so, when do they yell at you, every time, or only in example 2?

Collapse
 
kspeakman profile image
Kasey Speakman • Edited

For this particular project, I haven't even written JS code yet. It has all been in Elm. However, Elm's Date.fromString string function calls this JS under the covers: new Date( string ). So it is important to know the behavior. I ran these bits from the browser console.

In this case, example 2 is the one I prefer. Because it is a way to store/transmit a date that will parse to the right day regardless of the current time zone. So hopefully it would not be a linting error.

In an ideal world I would just work with UTC dates, but JS Date can only be made in the local time zone. Working in non-local time zones must be done with something other than JS Date (e.g. unix epoch integer, moment.js, etc).

Collapse
 
guid75 profile image
Guid75

For UTC dates, is developer.mozilla.org/en-US/docs/W... not a possibility?

Thread Thread
 
kspeakman profile image
Kasey Speakman • Edited

Nope, it does not work. Date.UTC returns unix epoch time (milliseconds since UTC midnight on 1 Jan 1970) as an integer. When you use that to construct a new Date it will still be converted to the local time zone.

// zero-based months πŸ™„
new Date(Date.UTC(2018, 0 /* Jan */, 1))
<- Sun Dec 31 2017 18:00:00 GMT-0600 (Central Standard Time)

The only reason the linked example prints GMT time zone is because Date.toUTCString() is used. But when using the JS Date object, calculations will be off by a day behind in my local time zone.

Collapse
 
squgeim profile image
Shreya Dahal

The thing is initializing dates with string is very ambiguous and depends heavily on what browser you are using. The only format with any guarantee is IOS 8601.

I have a more detailed write up on this, with a lot more caveats: blog.lftechnology.com/date-ing-jav...

Collapse
 
kspeakman profile image
Kasey Speakman

Problem is that ISO 8601 does not work well for dates. If I save to local storage in that format, then the user closes their laptop and hops on an airplane going from London (UTC+1) to New York (UTC-4 currently). When they arrive and open the app again, I reload the ISO dates from local storage they will all be off by one day. This leads me to one of two conclusions. Abandon JS Date. OR The only safe way to initialize a date in local time across browsers is using the integer-based constructor with that 0-based month nonsense.

Collapse
 
squgeim profile image
Shreya Dahal

That is true. This was a major issue for us in the backend, because even if we stored a date value in the database as just date, it would come to javascript as a date object, and go to the frontend as ISO 8601 string.

We had to configure the postgres driver for node to not automatically parse dates and make sure we only initialize dates in the frontend with number arguments.

Collapse
 
yanlipnican profile image
Jan Lipničan

Just don't use Date. Use dayjs or momentjs. Preferably dayjs becouse momentjs is not immutable.

Collapse
 
gsmoffln profile image
gsmoffln • Edited

Why not read the spec at first?
YES we run over idiots over line all the time.

Collapse
 
kspeakman profile image
Kasey Speakman

Seems like you are in a bad mood. Sorry for that.

Collapse
 
gsmoffln profile image
gsmoffln

But really, we do learn American language (the seventh, for me), why don't they learn the basics of localisation?