Communities

Writing
Writing
Codidact Meta
Codidact Meta
The Great Outdoors
The Great Outdoors
Photography & Video
Photography & Video
Scientific Speculation
Scientific Speculation
Cooking
Cooking
Electrical Engineering
Electrical Engineering
Judaism
Judaism
Languages & Linguistics
Languages & Linguistics
Software Development
Software Development
Mathematics
Mathematics
Christianity
Christianity
Code Golf
Code Golf
Music
Music
Physics
Physics
Linux Systems
Linux Systems
Power Users
Power Users

Dashboard
Notifications
Mark all as read
Q&A

Why is this client code getting the wrong date for a few hours a day?

+6
−0

Our web site has a widget that displays some date-based information that we retrieve from some JSON-formatted data (one object per date). We get the current date from the environment, possibly adjust it (see below), look up the right entry in the JSON, and display it. Most of the time this works right, but at certain times of day we see an off-by-one error.

About that adjustment: For purposes of our widget, the day does not start at midnight but at 8PM local time. (The widget is for an alternate calendar where the day starts at sunset. 8PM is a compromise because we are not going to look up actual sunset times in the client locale.) We therefore get the local date and time, and if it's after 8PM we increment the date before looking up the entry.

The behavior I have been seeing is:

  • From midnight to about 8PM (my time), the information is correct.
  • At 8PM the day rolls over to "tomorrow" as expected.
  • At 9PM the day rolls over again, so it is now really wrong (one full day ahead of where it should be).
  • At midnight it rolls back and is correct.

According to the documentation, the Javascript Date class operates in local time so what we're doing should work fine. The objects in the JSON use YYYY-MM-DD date format and Date doesn't by default, so we use toISOString() to convert the local date into this format:

let now = new Date();
if (now.getHours() > 20) {
   now.setDate(now.getDate() + 1);
}

now = now.toISOString().substr(0, 10);
// use that to look up the value

Why is it incrementing twice, making the value wrong for a few hours a day?

Why does this post require moderator attention?
You might want to add some details to your flag.
Why should this post be closed?

2 comments

It took me an embarrassingly long time to figure this out, so I'm leaving a trail here for others or for future-me. Monica Cellio‭ 16 days ago

Not an expert on the matter, but coming from .NET development area, native JS date/time manipulation looks like a mess. I would definitely consider using a library, if possible: https://terodox.tech/migrating-away-from-momentjs-part1/ Alexei‭ 16 days ago

2 answers

+6
−0

First of all, we need to understand what a JavaScript Date actually is. And surprisingly, it's not exactly a date (at least not in terms of having unique values for day, month, year, hour, minute and second).

A JavaScript Date actually represents a timestamp. More precisely, according to the language specification, a Date's value is the number of milliseconds since Unix Epoch (which, in turn, corresponds to 1970-01-01T00:00:00Z - January 1st, 1970, at midnight in UTC).

Hence, a Date represents a specific instant: a point in the timeline. And this same instant corresponds to a different date and time, depending on the timezone you are.

Example: by calling new Date().valueOf(), you'll get the current timestamp. I've just called that and the result was 1622722404062, which means that more than 1.6 trillion miliseconds has elapsed since Unix Epoch. If you had ran that code in any computer in the world (assuming they're not misconfigured), at the same instant I did, you'd get this same value.

But that same timestamp value corresponds to a different date and time, depending on the timezone you use. So 1622722404062 corresponds to all of the dates/times below:

Where Corresponds to
São Paulo (Brazil) June 3rd 2021, 09:13:24 AM
Tokyo (Japan) June 3rd 2021, 09:13:24 PM
Apia (Samoa) June 4th 2021, 01:13:24 AM
UTC June 3rd 2021, 12:13:24 PM

Note that, depending on the timezone, the date and/or time can be completely different. That's a crucial thing to understand how Date works: all the date/times above correspond to the same timestamp (1622722404062). Hence, a Date object with such timestamp value actually represents all of them.

And that's the confusing part: when you use the getters (such as getDate(), getHours(), etc), the value returned considers the browser's timezone (whatever it's configured in it: some might get that config from the OS, some might override it - it doesn't matter how it's done, that config will be used in the end).

But some methods return the value in UTC: that's the case of toISOString(). There are also "UTC getters", such as getUTCDate() and getUTCHours, which return the values according to UTC.

Anyway, mixing UTC and non-UTC methods is what probably caused this confusion, specially if you're in that range of hours when your local time corresponds to the next - or previous - UTC day (based on the other answer, that seems to be the case).


Just for the sake of completeness, to check if it's after 08:00 PM, you should test now.getHours() >= 20 (not >). Hours are zero based, but zero means midnight, 1 means 01:00 AM, and so on (hence, 20 means 08:00 PM, not 09:00 PM as the other answer said).

And to get the date formatted, using local timezone values instead of UTC, you'll have to do it manually:

function pad(value) {
    return value.toString().padStart(2, '0');
}

let now = new Date();
if (now.getHours() >= 20) {
   now.setDate(now.getDate() + 1);
}

let formatted = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;

Don't forget to add 1 to the month, as months in JavaScript's Date are annoyingly zero-based (January is zero, February is 1, etc).


How to NOT do it

If you search enough in the internet, you'll certainly find someone suggesting to change the timestamp value, by adding or subtracting the respective timezone offset (the difference from UTC). Something like this:

// don't do this
function format(date) {
    let offset = date.getTimezoneOffset() / 60;
    date.setTime(date.getTime() - offset * 3600 * 1000);
    return date.toISOString().slice(0, 10);
}

This code changes the timestamp, by subtracting the offset from it, making toISOString return the "correct" string. Although it "works", this is not the best solution, because when you change the timestamp, you're changing the instant that the Date represents.

To make an analogy, now it's 10 AM in Brazil and and 2 PM in London (it's British Summer Time (BST)). Let's suppose that my computer is misconfigured with London's timezone, so it displays 2 PM. There are two ways I could fix it:

  1. by setting the timezone to Brazil (actually, to São Paulo, because the country has more than one timezone)
  2. by setting the clock 4 hours back

Both will make my computer's clock display 10 AM, but if I use option 2, I actually set my clock to an instant 4 hours in the past (10 AM in London). Not only this is wrong, it'll also lead to more problems when BST ends.

That's what happens when I change the timestamp. It might "work" at a first glance, but changing the Date to a different instant can affect other parts of your code that relies on that (such as code that thinks it's the current time).


Alternatives

If you don't mind using an external lib, there are plenty of options, such as Moment.js, date-fns, Luxon, etc, all with better timezone and formatting support, if compared to Date.

And in the (near?) future, JavaScript will have the native Temporal API. AFAICT, it's not available in any browser yet, but using the current version of the polyfill, we can see how it's gonna work:

// current date/time in browser's timezone
let now = Temporal.now.zonedDateTimeISO();
if (now.hour >= 20) {
    // add is a timezone aware, DST-safe operation
    now = now.add({ days: 1 });
}
// toString() by default returns the date in ISO 8601 format
let formatted = now.toPlainDate().toString();

Although it looks similar to Date, it internally deals with all the troubles of date arithmetic when timezones (and DST changeovers) are involved.

But this proposal is still in experimental stage. As soon as most major browsers implement it, I'll update this answer accordingly.

Why does this post require moderator attention?
You might want to add some details to your flag.

0 comments

+3
−0

There are two issues here. The first is that this code isn't rolling over at 8PM as expected:

if (now.getHours() > 20) {
   now.setDate(now.getDate() + 1);
}

This change happens at 9PM as explained in this answer. The second increment is the one that's coming from this code, not the first one.

This leaves the question of what's going on at 8PM. The answer is that while Date uses local time if you get individual elements like day or hour (or even toString()), the ISO standard isn't just about getting YYYY-MM-DD formatting. ISO 8601 uses the UTC date. It's shortly before 9PM local time as I write this, and this is what we get from various operations:

let now = new Date();
now.getDate();        // returns 2
now.getHours();      // returns 20
now.toISOString();   // returns 2021-06-03T00:50:44.053Z

My time zone is UTC-4. Over in UTC-0, aka "Z" time, it's no longer June 2 but has rolled over to June 3. This happens at 8PM. That's where the first rollover is coming from.

In order to get the local date in the YYYY-MM-DD format, you have to use getFullYear(), getMonth(), and getDate() and assemble the string yourself. If month or date is one digit you have to add the leading 0, and don't forget to increment the month (months are 0-based).

Done in by time zones...

Why does this post require moderator attention?
You might want to add some details to your flag.

3 comments

Actually, 20 is 8:00pm. 12:01am is 00:01 = Hour 0, Minute 1. The problem is that > 20 effectively means > 8:59pm because it is looking at only the hour. So the reason of the problem was incorrect, but the actual problem (and the implemented > 19 fix) were both correct. manassehkatz‭ 16 days ago

Oh I see. So we could have also made it >= 20 to get 8PM. I don't know why I didn't do that instead of changing 20 to 19 there. Monica Cellio‭ 16 days ago

Fenceposts. Always watch out for the fenceposts. manassehkatz‭ 15 days ago

Sign up to answer this question »