简体   繁体   English

如何可靠地实施'下一个'/'前一个月'

[英]How to reliably implement 'next' / 'previous' month

This has been asked (badly) before - I don't think the answer in that post really addressed the issue, and then it went stale. 之前已经(严重) 问了这个问题 - 我认为那篇文章的答案并没有真正解决这个问题,然后就变得陈旧了。 I'm going to attempt to ask it again with a clearer demonstration of the issue. 我将尝试再次提出问题,更明确地证明这个问题。

The implementation of Javascript Date.setMonth() appears not to follow the principle of least surprise. Javascript Date.setMonth()似乎不遵循最小惊喜的原则。 Try this in a browser console: 在浏览器控制台中尝试:

d = new Date('2017-08-31')  // Set to last day of August
d.getMonth()  // 7 - months are zero-based
d.setMonth(8)  // Try to set the month to 8 (September)
d.getMonth()  // 9 - October. WTF Javascript?

Similarly: 同理:

d = new Date('2017-10-31')
d.getMonth()  // 9
d.setMonth(8)
d.getMonth() // 9 (still?)

Firefox on Linux appears even worse - sometimes returning a date in October, and a result from getMonth() which doesn't match that month! Linux上的Firefox看起来更糟糕 - 有时在十月份返回一个日期,而getMonth()的结果与那个月不匹配!

My question (and I think that of the OP from that linked question) is how to consistently implement a 'next' / 'prev' month function in, eg a datepicker? 我的问题(我认为来自该链接问题的OP的问题)是如何在例如日期选择器中持续实现“下一个”/“上一个”月份函数? Is there a well known way of doing this which doesn't surprise the user by, for example, skipping September when they start on August 31st and click 'next'? 有没有一种众所周知的做法,这并不会让用户感到惊讶,例如,在8月31日开始时跳过9月并点击“下一步”? Going from January 31st is even more unpredictable currently - you will end up on either March 2nd or March 3rd, depending on whether it's a leap year or not! 从1月31日开始,目前更难以预测 - 你将在3月2日或3月3日结束,这取决于它是否是闰年!

My personal view is that the least surprise would be to move to the last day of the next / previous month. 我个人认为,最不惊讶的是搬到下个月/上个月的最后一天 But that requires the setMonth() implementation to care about the number of days in the months in question, not just add / subtract a fixed duration. 但这需要setMonth()实现来关注有问题的月份中的天数,而不仅仅是添加/减去固定的持续时间。 According to this thread, the moment.js approach is to add / subtract the number of milliseconds in 30 days, which suggests that library would be prone to the same inconsistencies. 根据这个线程, moment.js方法是在30天内添加/减去毫秒数,这表明库容易出现相同的不一致。

It's all simple and logic. 这一切都很简单和逻辑。 Lets take your example and go see what id does. 让我们举个例子,看看id做了什么。

So the first line 所以第一行

 d = new Date('2017-08-31') // Set to last day of August console.log(d); // "2017-08-31T00:00:00.000Z" console.log(d.getMonth()); // 7 - months are zero-based 

So all good so far. 到目前为止一切都很好。 Next step: Your comment says it: // Try to set the month to 8 (September) So it's not done with trying. 下一步:您的评论说: // Try to set the month to 8 (September)所以没有尝试。 You either set it to september or you don't. 您可以将其设置为9月,也可以不设置。 In your example you set it to October. 在您的示例中,您将其设置为十月。 Explanation further down. 进一步说明。

 d = new Date('2017-08-31') // Set to last day of August console.log(d); // "2017-08-31T00:00:00.000Z" console.log(d.getMonth()); // 7 - months are zero-based d.setMonth(8) // Try to set the month to 8 (September) console.log(d); // but now I see I was wrong it is (October) 

So the good question is WHY? 所以好问题是为什么? From MDN 来自MDN

Note: Where Date is called as a constructor with more than one argument, if values are greater than their logical range (eg 13 is provided as the month value or 70 for the minute value), the adjacent value will be adjusted. 注意:如果将Date作为具有多个参数的构造函数调用,如果值大于其逻辑范围(例如,13表示月份值或70表示分钟值), 则将调整相邻值。 Eg new Date(2013, 13, 1) is equivalent to new Date(2014, 1, 1) , both create a date for 2014-02-01 (note that the month is 0-based). 例如,新日期(2013,13,1)相当于新日期(2014,1,1) ,两者都创建了2014-02-01的日期(请注意月份为0)。 Similarly for other values: new Date(2013, 2, 1, 0, 70) is equivalent to new Date(2013, 2, 1, 1, 10) which both create a date for 2013-03-01T01:10:00. 类似地,对于其他值:新日期(2013,2,1,0,70)等同于新日期(2013,2,1,1,10),它们都创建2013-03-01T01:10:00的日期。

So that sayd September has only 30 Days but the Date Object has 31. This is why it gives you October and not September. 所以说9月只有30天但是Date对象有31天。这就是为什么它给你10月而不是9月。

The simplest will be to take the date you have and set it to first day of month. 最简单的方法是将日期设置为月份的第一天。 Something like so: 像这样的东西:

 var d = new Date('2017-08-31') // Set to last day of August // simplest fix take the date you have and set it to first day of month d = new Date(d.getFullYear(), d.getMonth(), 1); console.log(d); // "2017-08-31T00:00:00.000Z" console.log(d.getMonth()); // 7 - months are zero-based d.setMonth(8) // Set the month to 8 (September) console.log(d.getMonth()); // get 8 it is (September) 

Since getMonth() returns an integer number, you can simply implement a generator over the date object, that sets the month + 1 or - 1 so long as your not at month 11 or month 0 respectively. 由于getMonth()返回一个整数,你可以简单地在日期对象上实现一个生成器,它设置月份+ 1或-1,只要你不是分别在11月或0月。

function nextMonth(dateObj) {
  var month = dateObj.getMonth();
  if(month != 11) dateObj.setMonth(month + 1);
  return dateObj;
}

function prevMonth(dateObj) {
  var month = dateObj.getMonth();
  if(month != 0) dateObj.setMonth(month - 1);
  return dateObj;
}

If you want to match the days in the previous month you can use an object lookup table. 如果要匹配上个月的日期,可以使用对象查找表。

Now, for your last day of the month problem: 现在,对于你这个月的最后一天问题:

function getLastDayofMonth(month) {
  var lookUp = {
    0:31,
    1:28,
    2:30,
    3:31
  };
  return lookUp[month];
}

//and then a revised version

function nextMonth(dateObj) {
  var month = dateObj.getMonth();
  var day = dateObj.getDate();
  if(month != 12) dateObj.setMonth(month + 1);
  if(getLastDayofMonth(month)<day)dateObj.setDate(getLastDayofMonth(month));
  return dateObj;
}

This should work for incrementing the month, you can use a similar strategy to decrement. 这应该适用于递增月份,您可以使用类似的策略来减少。

// isLeapYear :: Number -> Boolean
const isLeapYear = ((err) => {
  return yr => {
    // check for the special years, see https://www.wwu.edu/skywise/leapyear.html
    if (yr === 0) {
      throw err;
    }
    // after 8 AD, follows 'normal' leap year rules
    let passed = true;
    // not technically true as there were 13 LY BCE, but hey.
    if (yr === 4 || yr < 0 || (yr % 4)) {
      passed = false;
    } else {
      if (yr % 400) {
        if (!(yr % 100)) {
          passed = false;
        }
      }
    }
    return passed;
  };
})(new Error('Year zero does not exist, refers to 1 BCE'));

const daysInMonth = [
  31,
  28,
  31,
  30,
  31,
  30,
  31,
  31,
  30,
  31,
  30,
  31
];

// isLastDay :: Number, Number -> Boolean
const isLastDay = (d, m, y) => {
  let dm = isLeapYear(y) && m === 1 ? 29 : daysInMonth(m);
  return dm === d;
};

// getLastDay :: Number, Number -> Number
const getLastDay = (m, y) => isLeapYear(y) && m === 1 ? 29 : daysInMonth[m];

// incMonth :: Date -> Date
const incMonth = d => {
  let dd = new Date(d.getTime());
  let day = dd.getDate();
  let month = dd.getMonth() + 1;
  dd.setDate(5);  // should avoid edge-case shenanigans
  dd.setMonth(month);
  let year = dd.getFullYear();
  if (isLastDay(day, month, year)) day = getLastDay(month, year);
  dd.setDate(day);
  return dd;
};

This was the solution I came up with, which seems small and reliable as far as I can tell. 这是我提出的解决方案,就我所知,它似乎小而可靠。 It doesn't need any extra data structures, and relies on setDate(0) to select the last day of the month in the edge cases. 它不需要任何额外的数据结构,并依赖于setDate(0)来选择边缘情况下的月份的最后一天。 Otherwise it leaves the date alone, which is the behaviour I wanted. 否则它只留下日期,这是我想要的行为。 It also handles wrapping round from one year to the next (in either direction): 它还处理从一年到下一年(在任一方向)的包装:

function reallySetMonth(dateObj, targetMonth) {
  const newDate = new Date(dateObj.setMonth(targetMonth))
  if (newDate.getMonth() !== ((targetMonth % 12) + 12) % 12) {  // Get the target month modulo 12 (see https://stackoverflow.com/a/4467559/1454454 for details about modulo in Javascript)
    newDate.setDate(0)
  }
  return newDate
}

Note I've only tested this with targetMonth being either one higher or lower than the current month, since I'm using it with 'next' / 'back' buttons. 注意我只测试了这个,其中targetMonth比当前月份更高或更低,因为我正在使用'next'/'back'按钮。 It would need testing further user with arbitrary months. 它需要在任意月份测试更多用户。

If setMonth is used when adding and subtracting months, then if the date of the start month doesn't exist in the end month, the extra days cause the date to "roll over" to the next month, so 31 March minus 1 month gives 2 or 3 March. 如果在添加和减去月份时使用setMonth ,那么如果在月末不存在开始月份的日期,则额外天数会导致日期“翻转”到下个月,因此3月31日减1个月给出3月2日或3日。

A simple algorithm is to test the start date and end date and if they differ, set the end date to 0 so it goes to the last day of the previous month. 一个简单的算法是测试开始日期和结束日期,如果它们不同,则将结束日期设置为0,使其进入上个月的最后一天。

One issue with this is that subtracting 1 month twice may not give the same result as subtracting 2 months once. 与此相关的一个问题是,两次减去1个月可能不会产生与减去2个月一次相同的结果。 31 March 2017 minus one month gives 28 Feb, minus another month gives 28 Jan. Subtract 2 months from 31 March and you get 31 Jan. 2017年3月31日减1个月给2月28日,减去另一个月给1月28日。从3月31日减去2个月,你将获得1月31日。

C'est la vie. 这就是生活。

 function addMonths(date, num) { var d = date.getDate(); date.setMonth(date.getMonth() + num); if (date.getDate() != d) date.setDate(0); return date; } // Subtract one month from 31 March var a = new Date(2017,2,31); console.log(addMonths(a, -1).toString()); // 28 Feb // Add one month to 31 January var b = new Date(2017,0,31); console.log(addMonths(b, 1).toString()); // 28 Feb // 29 Feb plus 12 months var c = new Date(2016,1,29) console.log(addMonths(c, 12).toString()); // 28 Feb // 29 Feb minus 12 months var c = new Date(2016,1,29) console.log(addMonths(c, -12).toString()); // 28 Feb // 31 Jul minus 1 month var d = new Date(2016,6,31) console.log(addMonths(d, -1).toString()); // 30 Jun 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 如何可靠地从 js Date 获取上个月? - How to reliably get previous month from js Date? 如何使用javascript在html上获取上个月和下个月的结果 - how to get previous month and next month results on html using javascript 如何在Bootstrap日期选择器中获得下个月上个月而不是下一个上一个图标? - How to get next month Previous month in Bootstrap date picker instead of next previous icon? 如何用next和Previous在UIWebView中实现查找? - How to implement finding in UIWebView with next and Previous? 如何在 React 中实现上一个和下一个按钮? - How to implement previous and next Buttons in React? 如何在 Next JS 博客中实现上一篇和下一篇? - How to implement Previous Post & Next Post in a Next JS Blog? 如何在日期选择器中显示下一个和上一个月的名称,而不只是箭头&lt;,&gt; - How to show next and previous month names in datepicker instead of just arrows <,> 如何从fullcalender的年视图中隐藏上个月和下个月的日期和事件? - How to hide previous and next month dates and events from yearview of fullcalender? 我将如何在 JavaScript 中实现下一个和上一个按钮? - How would I implement a next and previous button in JavaScript? 如何在mvc中使用ajax实现上一个和下一个按钮? - How to implement previous and next button using ajax in mvc?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM