DEV Community

zechdc
zechdc

Posted on • Edited on

Javascript Date() - DateOnly Format and off by 1 day when using date-fns

I'm working on an application that needs to deal with Dates only, times are irrelevant to me. I need to do some date math like add 4 weeks to my date. Javascript's Date object allow for this kind of math and there are popular packages like date-fns which gives you addWeeks(). But, I've been running into issues with dates being off by 1 day.

Use Case: Birthdays

If you were born on May 10, 2020 you likely want to celebrate your birthday on May 10th no matter what timezone you are in. You probably don't care about Daylight Saving either, we just want to save May 10, 2020 as our birthday.

Easy, lets store everything in UTC

new Date('2020-05-10').toISOString();
// '2020-05-10T00:00:00.000Z'
Enter fullscreen mode Exit fullscreen mode

Note: UTC is Coordinated Universal Time, which is a handy way to store dates and times and makes dealing with timezones "easier". toISOString() isn't giving you a UTC time but rather the ISO 8061 standard format, it could be UTC or a local datetime in the ISO format.

Use Case: Four weeks from today

Let's say today is Sept. 30, 2020 and we want to calculate 4 weeks from today which would be October 28, 2020.

const numWeeks = 4;
const today = new Date('2020-09-30');
today.setDate(today.getDate() + numWeeks * 7);
today.toISOString();
// '2020-10-28T00:00:00.000Z'
Enter fullscreen mode Exit fullscreen mode

Great that works like we expected!

So what's the problem?

If you're like most developers, when it comes to working with dates you might reach for a library like date-fns to make formatting easier and date math easier.

Add 4 weeks with date-fns

Using the code from before, let's update it to use the date-fns addWeeks() function.

const numWeeks = 4;
const today = new Date('2020-09-30');
const fourWeeksLater = addWeeks(today, numWeeks);
fourWeeksLater.toISOString();
// '2020-10-28T01:00:00.000Z'
Enter fullscreen mode Exit fullscreen mode

Wait what? Why is there an hour in our ISO string [...]01:00:00.000Z

This is caused by the way the JS Date() object parses strings.

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. Source

The time zone offset specified as "Z" (for UTC) or either "+" or "-" followed by a time expression HH:mm Source

Meaning, if we have new Date('2020-09-30') the date will be stored as a UTC time. But if you have new Date('2020-09-30T00:00:00.000') it will be stored as the local time. You can include the time and use the Z time zone offset such as new Date('2020-09-30T00:00:00.000Z'), but this still doesn't fix the issue once you pass this Date object to addWeeks().

When using addWeeks in date-fns (and other date-fns function), the date you pass in gets converted into a full datetime string, meaning it's no longer treated as a UTC date, but rather a local date.

Nitty Gritty Details

Let's look at the code for addWeeks() in date-fns to see where this gets converted from our UTC Date to a local date.

  1. addWeeks(new Date('2020-09-30')) passed our Date object to addDays()
  2. addDays() passed our Date object to toDate()
  3. which then hits this line of code in the date-fns library:
// Prevent the date to lose the milliseconds when passed to new Date() in IE10
return new (argument.constructor as GenericDateConstructor<DateType>)(
      +argument,
);

// Source: https://github.com/date-fns/date-fns/blob/5c1adb5369805ff552737bf8017dbe07f559b0c6/src/toDate/index.ts#L46
Enter fullscreen mode Exit fullscreen mode

Looks weird, but here is that same code rewritten on our today variable

const today = new Date('2020-09-30');
new (today.constructor)(+today);
// Wed Sep 30 2020 01:00:00 GMT+0100 (British Summer Time)
Enter fullscreen mode Exit fullscreen mode

By using date-fns addWeeks(), we converted our Date() object from a UTC date to a local date. Not really what we wanted, and this is where we start to get "off by 1 day" issues.

Can I still use date-fns? Should I?

If you really want to use date-fns though, you can add this date-fns package to handle UTC more predictably https://www.npmjs.com/package/@date-fns/utc

Note: If you are working with Date Only formats, I'm questioning if using date-fns is even needed. Would love to hear what others are doing for this kind of thing

import { UTCDate } from '@date-fns/utc;

const numWeeks = 4;
const today = new UTCDate(2020, 8, 30); // months are 0-index based, meaning January is 0, Feb 1... and September is 8 not 9. Seems strange but this mirrors Javascripts own Date() function: new Date(2020, 8, 30)

const fourWeeksLater = addWeeks(today, numWeeks);
fourWeeksLater.toISOString();
// '2020-10-28T00:00:00.000Z'
Enter fullscreen mode Exit fullscreen mode

Great! That sorts out the problem I was having at least. Hope it helps you too.

More Learnings:

Z as the time zone offset

Along the way, learned that you can make Date() use UTC even with a timestamp by including the timezone offset Z.

new Date('2020-05-10T00:00')
// Sun May 10 2020 00:00:00 GMT-0700 (Pacific Daylight Time)

new Date('2020-05-10T00:00').toISOString();
// '2020-05-10T07:00:00.000Z'

new Date('2020-05-10T00:00Z')
// Sat May 09 2020 17:00:00 GMT-0700 (Pacific Daylight Time)

new Date('2020-05-10T00:00Z').toISOString();
// '2020-05-10T00:00:00.000Z'
Enter fullscreen mode Exit fullscreen mode

More little experiments

new Date('2020-05-10T00:00').getHours()
// 0
new Date('2020-05-10T00:00').getUTCHours()
// 7

// Again, including `Z` will give you UTC hours
new Date('2020-05-10T00:00Z').getHours()
// 17
new Date('2020-05-10T00:00Z').getUTCHours()
// 0
Enter fullscreen mode Exit fullscreen mode

But, if you are using date-fns, as soon as you pass these UTC dates into addWeeks, addDays, etc it will get converted to a local time unless you use the UTCDate object

toISOString().split('T')[0]

I've seen a bunch of online advice saying to strip the time like this:

new Date(date).toISOString().split('T')[0];
Enter fullscreen mode Exit fullscreen mode

I haven't found this advice to be particularly useful and seems like it can easily lead down the wrong road. Let me know if you disagree, would love to understand a valid use case for it.

ProTip: Change Browser Timezone for testing

In Chrome you can change the browser timezone: Open Dev Tool > "Three dots button" at the top
Image description

More Tools > Sensors > Change Location > Refreshing might be needed
Image description

Sources

Top comments (0)