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'
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'
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'
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.
-
addWeeks(new Date('2020-09-30'))
passed our Date object to addDays() - addDays() passed our Date object to toDate()
- 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
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)
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'
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'
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
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];
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
More Tools > Sensors > Change Location > Refreshing might be needed
Top comments (0)