Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When on the last day of month, addMonth and subMonths doesn't return the expected date #3506

Open
scopsy opened this issue Aug 31, 2023 · 7 comments

Comments

@scopsy
Copy link

scopsy commented Aug 31, 2023

Reproduction

import subMonths from "date-fns/subMonths";
import addMonths from "date-fns/addMonths";

const startDate = new Date(2023, 8, 31); // YYYY-MM-DD
const oneMonthAhead = addMonths(startDate, 1);

const result = subMonths(oneMonthAhead, 1);
console.log(result);  // prints 31st of August

The expected results would be the 31st of August, while the printed result is the 30th of August.

I'm assuming that the subtraction should be in calendar months, hence subtracting should result in the last day of August.

Also, when doing this in February, this works as expected:

import subMonths from "date-fns/subMonths";
import addMonths from "date-fns/addMonths";

const startDate = new Date(2023, 2, 28);
const oneMonthAhead = addMonths(startDate, 1);

const result = subMonths(oneMonthAhead, 1);
console.log(result); // prints 28th of February
@llllvvuu
Copy link

llllvvuu commented Sep 1, 2023

This is tricky because if you want subMonths of September 30 to be August 31, then should subMonths of September 29 be August 30? How about subMonths of September 29, 28, ... 1?

Then is addMonths of August 31 becoming September 30 wrong? October 1 wouldn't work either.

But you are right, having subMonth(addMonth(x)) != x feels very strange. And it feels strange for addMonths of August 30 to be the same as addMonths of August 31.

@scopsy
Copy link
Author

scopsy commented Sep 3, 2023

@llllvvuu yep that's true, I had a second thought of this. I'm just curious if we should handle last-of-month subtraction with special consideration. But seems like it's not necessarily the case...

@lemming
Copy link

lemming commented Sep 4, 2023

@scopsy, be wary when creating dates like this: new Date("2023-08-31"). The resulting date will be set to 00:00:00 of that day in UTC

new Date("2023-08-31").toUTCString()
//=> 'Thu, 31 Aug 2023 00:00:00 GMT'

But printing them with console.log will display your date in local time zone which may lead to unexpected results in different time zones:

// in time zone ahead of UTC, e.g. 'Europe/Amsterdam'
new Date("2023-08-31")
// console.log will print:
Thu Aug 31 2023 02:00:00 GMT+0200 (Central European Summer Time)

// in time zone behind UTC, e.g. 'America/New_York'
new Date("2023-08-31")
// console.log will print:
Wed Aug 30 2023 20:00:00 GMT-0400 (Eastern Daylight Time)

date-fns operates on local time zone, so new Date(2023, 7, 31) would be more appropriate way to create date when working with them.

d = new Date(2023, 7, 31).toUTCString()
'Thu, 31 Aug 2023 04:00:00 GMT'
console.log(d)
Thu, 31 Aug 2023 04:00:00 GMT

@scopsy
Copy link
Author

scopsy commented Sep 4, 2023

@lemming thank you for the clarification! Yes, that was just used for the example here. Happens also when specified in the suggested format. Updated the original format

@lemming
Copy link

lemming commented Sep 4, 2023

I think date-fns works correctly here, but the docs should make it clear that these operations are not always reversible and highlight edge cases when it might bite you.

Something like this:

Description

Add the specified number of calendar months to the given date.

Warning! Due to the nature of calendar math this operation is lossy. E.g. subMonths(addMonths(date, x), x) is not always equal to date when operating on dates close to the end of the month. Say, we added 1 month to August 31. The expected result is September 30, since September has only 30 days compared to August. But the same date corresponds to exactly 1 month after August 30. Subtracting 1 month from September 30 will return August 30, thus starting from August 31 we will land on the different date. Note that this is not the case if we would added two months, since both of August and October has 31 days.

Please see the examples below.

Examples

// Note that due to differences in the lengths of calendar months this operation is irreversible

// Add 1 month to 28 January 2023:
addMonths(new Date(2023, 0, 28), 1)
//=> Tue Feb 28 2023 00:00:00

// Add 1 month to 29 January 2023:
addMonths(new Date(2023, 0, 29), 1)
//=> Tue Feb 28 2023 00:00:00

// Add 1 month to 30 January 2023:
addMonths(new Date(2023, 0, 30), 1)
//=> Tue Feb 28 2023 00:00:00

// Add 1 month to 31 January 2023:
addMonths(new Date(2023, 0, 31), 1)
//=> Tue Feb 28 2023 00:00:00
// For the same reason adding months to the same date in conjunction and
// successively  may lead to different results

// Add 1 month to October 31 2023
addMonths(new Date(2023, 9, 31), 1)
//=> Thu Nov 30 2023 00:00:00

// Add 2 month to October 31 2023
addMonths(new Date(2023, 9, 31), 2)
//=> Sun Dec 31 2023 00:00:00

addMonths(addMonths(new Date(2023, 9, 31), 1), 1)
//=> Sat Dec 30 2023 00:00:00

@lemming
Copy link

lemming commented Sep 4, 2023

@lemming thank you for the clarification! Yes, that was just used for the example here. Happens also when specified in the suggested format. Updated the original format

Sorry for nitpicking, but for anyone to easily reproduce the issue I would suggest one minor update. When using Date constructor from string months are indexed from 1, and when using individual date values, months are indexed from 0:

new Date(2023, 7, 31) // August 31 2023
new Date(2023, 8, 31) // October 01 2023

@p-fernandez
Copy link

but the docs should make it clear that these operations are not always reversible and highlight edge cases when it might bite you.

Agreed regarding the documentation update point. It was our case and being a time based bug that only happens 3-4 times per year made harder to notice it.
I'd suggest to mention in the documentation that for cases with more accuracy to use addDays/subDays even when needing to add or substract months. It was the route we chose to follow to fix our problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants