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

addMonths is coming up 1 hour short #571

Open
harounb opened this issue Sep 26, 2017 · 31 comments
Open

addMonths is coming up 1 hour short #571

harounb opened this issue Sep 26, 2017 · 31 comments
Labels

Comments

@harounb
Copy link

harounb commented Sep 26, 2017

Version: ^1.28.5

I would expect
addMonths(new Date('2017-03-01T00:00:00.000Z'), 1).toISOString();
to equal
'2017-04-01T00:00:00.000Z'
Instead it's coming up as
'2017-03-31T23:00:00.000Z'

@kossnocorp
Copy link
Member

Thank you for reporting, I'll take a look!

@kossnocorp
Copy link
Member

Could you tell me what's your local time zone?

@harounb
Copy link
Author

harounb commented Sep 29, 2017

No problem.

My timezone is currently BST (UTC +1).

Should timezones affect the results if I'm comparing two UTC strings?

Edit:
I've been exploring past issues and I guess this is related to #556 and I should provide an offset?

Edit2: It would be difficult to know when to apply an offset. Some months are working correctly without an offset but March isn't

@kossnocorp
Copy link
Member

It may affect the result as date-fns (as well as JavaScript) converts dates to the local TZ. You can avoid it by doing a trick:

new Date('2017-03-01T00:00:00.000Z'.split('T')[0])

This way JS won't convert time zones and it will always product 1st of March regardless of the current time setup.

If that was the problem, please close the issue as a proper solution for it is coming with UTC release (no ETA at the moment): #376

If the problem is DST-related (unlikely, but I have to check it first), I'll fix it.

@harounb
Copy link
Author

harounb commented Sep 29, 2017

Ah unfortunately in that case I think it may be DST related.
Just tried
addMonths(new Date('2017-03-01T00:00:00.000Z'.split('T')[0]), 1)
And the result is
2017-03-31T23:00:00.000Z

@harounb
Copy link
Author

harounb commented Oct 5, 2017

Just done some more testing on this.
Definitely seems daylight savings related.

I made a range of dates using each day and you can see where the timezone changes from the offset.
Working around this should be easy now. Just have to adjust the dates by the computed offset.

Anyone reading this make sure you don't adjust it by the current offset. Needs to be the computed one at that date.

> const eachDay = require('date-fns/each_day')
undefined
> const start = new Date('2017-03-01')
undefined
> const end = new Date('2017-05-31')
undefined
> const dates = eachDay(start, end)
undefined
> const offsets = dates.map(date => date.getTimezoneOffset());
undefined
> console.log(offsets)
[ 0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  0,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60,
  -60 ]

@Dorphern
Copy link

Any updates on this issue?

@k0nserv
Copy link

k0nserv commented Jul 19, 2018

The same thing happens in Safari and older version of Chrome/Firefox with addDays. For example in Brasilia(BRT) the following code

addDays(new Date(2018, 9, 28), 7)

Returns Sat Nov 3 2018 23:00:00 GMT-0300 in Safari and Sun Nov 04 2018 01:00:00 GMT-0200 in Chrome and 2018-11-04T00:00:00.000Z in Firefox.

@tiagoengel
Copy link

tiagoengel commented Jul 24, 2018

This is definitely DST related but only when the DST transition happens during midnight, which is the case in Brazil. Screenshots bellow are from latest Safari with my system configured to use BRT timezone. November 4th is when Brazil transitions to DST.

screen shot 2018-07-24 at 06 49 51

screen shot 2018-07-24 at 06 51 51

Latest Chrome and Firefox both return Nov 4 2018 01:00:00 and IE 11 returns Nov 4 2018 00:00:00.

Any function using setHours(0, 0, 0, 0) internally will be affected by this. One example is eachDay.

function eachDay (dirtyStartDate, dirtyEndDate) {
  var startDate = parse(dirtyStartDate)
  var endDate = parse(dirtyEndDate)

  var endTime = endDate.getTime()

  if (startDate.getTime() > endTime) {
    throw new Error('The first date cannot be after the second date')
  }

  var dates = []

  var currentDate = startDate
  // this will cause any interval that goes through Nov 4 to return Nov 3 twice instead of Nov 4.
  // currentDate.setHours(0, 0, 0, 0) 
 
  currentDate.setHours(1, 0, 0, 0) // this works on any browser
  while (currentDate.getTime() <= endTime) {
    const toAdd = parse(currentDate)
    toAdd.setHours(0, 0, 0, 0) 
    dates.push(parse(toAdd))
    currentDate.setDate(currentDate.getDate() + 1)
  }

  return dates
}

Safari seems to be the only browser where this is still a problem.

@tiagoengel
Copy link

tiagoengel commented Jul 25, 2018

I did some more testing and setTime seems to work fine on every browser. That is what moment is doing to go around this problem. see: https://github.com/moment/moment/pull/4338/files.

Ex:

var date = new Date(2018, 10, 3)
date.setTime(date.getTime() + 8.64e+7) // Nov 4 2018 01:00:00

// startOfDay
var MS_PER_24_HOURS = 1000 * 60 * 60 * 24;
var date = new Date(2018, 10, 4, 5)
var startOfDay = Math.floor(date.getTime() / MS_PER_24_HOURS) * MS_PER_24_HOURS;
date.setTime(startOfDay);
date.setTime(date.getTime() + (date.getTimezoneOffset() * MS_PER_MINUTE)); // Nov 4 2018 01:00:00

@tiagoengel
Copy link

tiagoengel commented Jul 30, 2018

A couple more information, setTime is consistent in all browsers (even Safari).

Few examples:

var date = new Date(2018, 10, 3);
date.setTime(date.getTime() + 8.64e+7); // Sun Nov 04 2018 01:00:00 GMT-0200

date = new Date(2018, 1, 17);
date.setTime(date.getTime() + 8.64e+7); // Sat Feb 17 2018 23:00:00 GMT-0300
// This is actually correct since here the clock has to go back an hour.

The way we ended up fixing it internally was mostly not using date-fns functions, unfortunately, and using UTC most of the time. This is how we create a date at the start of the day atm.

function dateAtStartOfDay(year, month, day) {
  const date = new Date(Date.UTC(year, month, day));
  const tzOffset = date.getTimezoneOffset();

  // Convert to the "timezoned" date using `setTime` to avoid the Safari bug
  date.setTime(date.getTime() + tzOffset * ONE_MINUTE_IN_MS);

  // This happens during DST transitions at midnight and is correct, 
  // but we want the date to be at the start of the provided day.
  if (day !== date.getDate() && date.getHours() === 23) {
    date.setTime(date.getTime() + ONE_HOUR_IN_MS);
  }
  return date;
}

I have also filed a Webkit but about this: https://bugs.webkit.org/show_bug.cgi?id=188001

@GonchuB
Copy link

GonchuB commented Mar 8, 2019

On investigating this I wrote a small function that might solve this issue. Has not been tested thoroughly, so use with caution:

// Removed the dirtyDate / dirtyAmount just to test easier in isolation
function agnosticAddMonths(date, amount) {
  const originalTZO = date.getTimezoneOffset();
  const endDate = addMonths(date, amount);
  const endTZO = endDate.getTimezoneOffset();

  const dstDiff = originalTZO - endTZO;
  
  return dstDiff ? addMinutes(endDate, dstDiff) : endDate;
}

You can see the difference in the last console log, where the isoString is preserving the original UTC 00:00:00 time.

image

This could allow us to write TZ agnostic tests (passing UTC values, expecting UTC values) that can run in your local TZ (if set) or UTC.

The "issue" lies in https://github.com/date-fns/date-fns/blob/master/src/addMonths/index.js#L37, as that newly created date won't be in UTC even though you passed an UTC original date. And, as expected, 2019-01-01T00:00:00 might not be the same as 2019-01-01T00:00:00Z (depending where you are / your env)

@ksevksev
Copy link

The primary reason folks resort to a library to handle date math is because date math is not straight forward. It has many exceptions and irregularities. If it didn't no one would think to use a library when they already have math functions. There is also the issue of governments changing things up. Although, it is a finite list. Therefore, creating a library to tackle it makes sense. However, when the library doesn't fix key things like daylight savings time handling, they are voiding the primary reason you choose to use the library.

@dmclavel
Copy link

Ah unfortunately in that case I think it may be DST related.
Just tried
addMonths(new Date('2017-03-01T00:00:00.000Z'.split('T')[0]), 1)
And the result is
2017-03-31T23:00:00.000Z

This seemed to have worked, thanks. But I hope that this issue should be fixed soon.

@ovcOS
Copy link

ovcOS commented May 21, 2019

If you set your time zone to UTC in your environment you won't have to worry about DST.
Run export TZ=UTC in your console for development purposes, and set it on your server for production

@GonchuB
Copy link

GonchuB commented May 21, 2019

@ovcOS what if I don’t know the timezone and I don’t care about it? Which one should I set?

My questions are rhethorical. If you want to be timezone agnostic this does not help. If you want your code to work a fixed timezone per env, it will.

@ovcOS
Copy link

ovcOS commented May 21, 2019

@GonchuB as I understand, UTC is time zone-agnostic, therefore you don't need to know your TZ. With the command above you are disregarding any local TZs and setting your time to universal

@GonchuB
Copy link

GonchuB commented May 21, 2019

@ovcOS no, that is the whole point. Check the difference between addMonths and agnosticAddMonths #571 (comment)

@ovcOS
Copy link

ovcOS commented May 21, 2019

@GonchuB I've seen the difference. I believe you may be missing the point. UTC does not care about time zones or daylight savings. Therefore, setting your machine and server to UTC will disregard these issues. It works for me. Anyone else struggling with this should give it a try.

@GonchuB
Copy link

GonchuB commented May 21, 2019

@ovcOS what if we run in the browser, I cannot enforce UTC (or makes no sense for a test to run enforcing UTC). And the outcome of addMonths would be wrong, as it WILL take TZ into consideration for users

@tiagoengel
Copy link

I think there are two unrelated issues being discussed here. The original problem being discussed is DST related, is only timezone related because it only happens in timezones where DST change is at midnight. It will indeed not happen if one uses UTC but that is not the point, some people need to use "timezoned" dates. Also, converting back from UTC to a date with timezone will incur in the same problem.

The last thing to keep in mind is that this is a browser issue (https://bugs.webkit.org/show_bug.cgi?id=188001) and so a fix here would be just to get around that bug.

I've posted this before but moment had the same problem and you can see how they fixed it here https://github.com/moment/moment/pull/4338/files

@MarkPolivchuk
Copy link

@GonchuB Thank you for the workaround!

@bertho-zero
Copy link

bertho-zero commented Mar 23, 2020

Fixed agnosticAddDays with negative TZs:

function agnosticAddDays(date, amount) {
  const originalTZO = date.getTimezoneOffset();
  const endDate = dateFns.addDays(date, amount);
  const endTZO = endDate.getTimezoneOffset();

  const dstDiff = originalTZO - endTZO;

  return dstDiff >= 0
    ? dateFns.addMinutes(endDate, dstDiff)
    : dateFns.subMinutes(endDate, Math.abs(dstDiff));
}

@KamalAman
Copy link

KamalAman commented Jun 16, 2020

@bertho-zero thank you for your work around. It works like a charm.

It's sad that a bug that is so fundamental and critical can be left unresolved for so long. :(

It seems like Fix DST issues with add, addDays and addMonths (#1760) (closes #1682) is working in this area, however it didn't fix this issue.

@nullifiedtsk
Copy link

Also experiencing the problem here :)

So, this thing is not going to be fixed? (i probably understanding why since the Date is exactly the same but timezone changes)
So, this is desired behavior within this library?

First of all, browser timezone is +3 (Moscow) and ama using date-fns 2.16.1 using latest stable chrome.

I am trying to:

const staticDate = new Date('2000-01-01 00:00:00:000Z');
const result = addMonths(staticDate, 4);
expect(result.toIsoString()).toEqual('2000-05-01T00:00:00.000Z')

And getting an unexpected value

2000-04-30T23:00:00.000Z

instead of:

2000-05-01T00:00:00.000Z

p.s.
If i understood correct, everything i can do now is to wrap up every function and handle changes in timezone myself?

@saanderson1987
Copy link

saanderson1987 commented Jan 29, 2021

I just experienced the same issue as stated in the very first post, except with sub. For the record my local timezone is NOT Brazil, it is EST, and the browser I was using to test was Chrome. Look at the following return results:

  • sub(new Date("2021-05-01T00:00:00.000Z"), { months: 3 }).toISOString() returns "2021-01-31T00:00:00.000Z" (wrong)
  • sub(new Date("2021-02-01T00:00:00.000Z"), { months: 3 }).toISOString() returns "2021-10-31T23:00:00.000Z" (wrong)
  • sub(new Date("2021-10-01T00:00:00.000Z"), { months: 3 }).toISOString() returns "2021-07-01T00:00:00.000Z" (correct)

Solution:

Scrap date-fns and use vanilla JS:

const t = new Date("2021-02-01T00:00:00.000Z");
t.setUTCMonth(t.getUTCMonth() - 3);
t.toISOString() // "2020-11-01T00:00:00.000Z" CORRECT

Note that before you call setUTCMonth, the value of t.getUTCMonth() - 3 equals -2. Despite the mdn docs stating that the only valid value for the first parameter of setUTCMonth is "an integer between 0 and 11", it still yields the correct result!

LawnGnome added a commit to LawnGnome/crates.io that referenced this issue Mar 16, 2023
When falling back, a bug in date-fns[^0] results in the 23 hour day
being skipped when building the `dates` array in `toChartData` if the
current time is between 00:00 and 01:00 UTC. This results in that day's
data essentially going missing, resulting in the subtly broken charts
noted in rust-lang#5477.

This commit adds a workaround adapted from a comment on
date-fns/date-fns#571[^1] by @bertho-zero, which correctly adjusts the
new date based on the time zone offsets, and means that `dates` is built
as expected.

Fixes rust-lang#5477.

[^0]: date-fns/date-fns#571
[^1]: date-fns/date-fns#571 (comment)
Turbo87 pushed a commit to rust-lang/crates.io that referenced this issue Mar 19, 2023
When falling back, a bug in date-fns[^0] results in the 23 hour day
being skipped when building the `dates` array in `toChartData` if the
current time is between 00:00 and 01:00 UTC. This results in that day's
data essentially going missing, resulting in the subtly broken charts
noted in #5477.

This commit adds a workaround adapted from a comment on
date-fns/date-fns#571[^1] by @bertho-zero, which correctly adjusts the
new date based on the time zone offsets, and means that `dates` is built
as expected.

Fixes #5477.

[^0]: date-fns/date-fns#571
[^1]: date-fns/date-fns#571 (comment)
@ogmzh
Copy link

ogmzh commented Mar 27, 2023

I'm experiencing this for startOfMonth and endOfMonth methods. I'm in CET, and for a selectedDate with value of 2023-03-27T00:00:00.000Z, startOfMonth produces 2023-02-28T23:00:00.000Z (february!) and endOfMonth produces 2023-03-31T21:59:59.999Z. How do I make these methods local TZ agnostic?

@toniengelhardt
Copy link

toniengelhardt commented Apr 5, 2023

Same issue. For a date with day 01 and time 00:00:00, e.g. 2023-04-01T00:00:00, addMonths(date, 1) will return the original month (April) without adding anything (should be May) if the timezone offset is negative, aka. west of Greenwich. This leads to really nasty bugs because it works correctly if ahead of UTC and doesn't if behind.

@karlhorky
Copy link

karlhorky commented Aug 25, 2023

Fixed agnosticAddDays with negative TZs:

function agnosticAddDays(date, amount) {
  const originalTZO = date.getTimezoneOffset();
  const endDate = dateFns.addDays(date, amount);
  const endTZO = endDate.getTimezoneOffset();

  const dstDiff = originalTZO - endTZO;

  return dstDiff >= 0
    ? dateFns.addMinutes(endDate, dstDiff)
    : dateFns.subMinutes(endDate, Math.abs(dstDiff));
}

@kossnocorp @leshakoss would you accept PR(s) to allow DST-safe operations either by A) adding new functions or B) adding options to existing functions addDays, addWeeks, addMonths, etc?

@bertho-zero would you be open to creating such a PR?

A TypeScript version of addDaysDstSafe, addWeeksDstSafe, addMonthsDstSafe that's a bit more concise:

import { addDays, addMinutes, addMonths, addWeeks } from 'date-fns';

export function addDaysDstSafe(date: Date, amount: number) {
  const endDate = addDays(date, amount);
  return addMinutes(
    endDate,
    date.getTimezoneOffset() - endDate.getTimezoneOffset(),
  );
}

export function addWeeksDstSafe(date: Date, amount: number) {
  const endDate = addWeeks(date, amount);
  return addMinutes(
    endDate,
    date.getTimezoneOffset() - endDate.getTimezoneOffset(),
  );
}

export function addMonthsDstSafe(date: Date, amount: number) {
  const endDate = addMonths(date, amount);
  return addMinutes(
    endDate,
    date.getTimezoneOffset() - endDate.getTimezoneOffset(),
  );
}

@acrabb
Copy link

acrabb commented Feb 29, 2024

This should 100% be part of this library. I'm still running into this issue of DST when adding months.

2024-03-01 + one month should = 2024-04-01
not 2024-03-29 and 23 hours

@acrabb
Copy link

acrabb commented Feb 29, 2024

this worked for assuming UTC dates: in my dateMath function...for adding months...

if (months && months != 0) {
    newDate = new Date(newDate.getFullYear(), newDate.getUTCMonth() + months, newDate.getUTCDate())
    newDate = new Date(newDate.toISOString().split('T')[0])
  }

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

No branches or pull requests