What is the source of this off-by-one error in a JavaScript date?

一笑奈何 提交于 2020-03-23 08:19:49

问题


I am trying to write a function that will take a string like 07/2020 and then return whether it is more than three months away.

I have written a function isMoreThan3MonthsHence that I am reasonably sure works correctly:

const isMoreThan3MonthsHence = ({ utcYear, utcMonth }, 
                                now = new Date, 
                                target = new Date(Date.UTC(utcYear, utcMonth)), 
                                threeMonthsAway = new Date(now.valueOf()).setUTCMonth(now.getUTCMonth() + 3)) => 
    (target > threeMonthsAway)


console.log(isMoreThan3MonthsHence({ utcYear: 2020, utcMonth: 7 })) // true (correct!)

The problem comes when I try to construct a Date object to use to populate the arguments for isMoreThan3MonthsHence.

const validate = (str, 
                  [localMonth, localYear] = str.split('/'), 
                  date = new Date(+localYear, (+localMonth)-1)) => 
    isMoreThan3MonthsHence({ utcYear: date.getUTCFullYear(), utcMonth: date.getUTCMonth() })

// Note: input is one-based months
console.log(validate('07/2020')) // false (but should be true!)

I think the reason is that new-ing up a Date in validate without specifying the timezone will use the local timezone in effect at the supplied date, which will be BST (UTC+1).

Wed Jul 01 2020 00:00:00 GMT+0100 (British Summer Time)

This time is actually 2300hrs on June 30th in UTC. So the month is actually 5 in zero-based terms. But I don't want this behavior. I want it so specifying July actually means July in UTC.

How can I fix this?


回答1:


Other than mixing UTC and local dates, the way you're adding 3 months will cause an incorrect response for dates like 31 March, where adding 3 months simply by incrementing the month number results in a date for 1 July. See Adding months to a Date in JavaScript.

So validate('07,2020') will return false if run on 31 March.

To fix that, when adding months, check that the updated date is still on the same day in the month, otherwise it's rolled over so set it to the last day of the previous month.

function validate(s) {
  let testDate = addMonths(new Date(), 3);
  let [m, y] = s.split(/\D/);
  return testDate < new Date(y, m-1);
};

function addMonths(date, months) {
  let d = date.getDate();
  date.setMonth(date.getMonth() + +months);
  // If rolled over to next month, set to last day of previous month
  if (date.getDate() != d) {
    date.setDate(0);
  }
  return date;
}

// Sample
console.log('On ' + new Date().toDateString() + ':');
['07/2020', '04/2020'].forEach(
  s => console.log(s + ' - ' + validate(s))
);



回答2:


It looks like you're mixing the usage of Date.UTC and not when instantiating dates. For example, if you use the following for your validate function:

const validate = (str, 
                  [month, year] = str.split('/'), 
                  date = new Date(Date.UTC(+year, (+month)-1))) => 
    isMoreThan3MonthsHence({ utcYear: date.getUTCFullYear(), utcMonth: date.getUTCMonth() })

// Note: input is one-based months
console.log(validate('07/2020')) // Now true

It works as expected: JSFiddle

Removing the usage of Date.UTC altogether would perform the calculation in the user's local timezone, with any applicable daylight saving adjustment included. This could be seen as a valid approach, however would result in the behaviour you have described.


Note I've renamed the local prefixed variables based on feedback from Bergi. Using Date.UTC implies you're passing in UTC arguments.



来源:https://stackoverflow.com/questions/60511658/what-is-the-source-of-this-off-by-one-error-in-a-javascript-date

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!