Javascript next month (or previous month) with snapping to the first/last day of a month
I’m working on some hotel booking reports and needed flexible selection of date ranges. Solution was flexible, user could select any date range he/she wanted, but this came with the cost of bad UI usability/efficiency. Users do require to be able to select some very specific date ranges for their reports but often they just wanted to select a complete month and then browse easily to previous/next one. JavaScript calendar did the job, but required too many clicks to do it. Solution was obvious, add “month browsing buttons” to UI.
The challenge
At first, adding +/- month buttons seemed trivial. I thought something like date.setMonth(date.getMonth()+1)
would do the job, but I was wrong. Not all months have equal number of days, it can be anywhere from 28 to 31. If you simply add a month to a month having more days than the next one - overflow happens:
let date = new Date('2016-1-31')
date.setMonth(date.getMonth()+1)
console.log(date.toDateString())
Above code outputs Wed Mar 02 2016
. There is a logic to it, but for financial/booking reports it is not what you want.
It would be easy to enable my users to browse only by complete month, but they sometimes need to browse by month, but not by complete month. I.e. they want only first 10 days of months. So, clicking “next month” on the date range 2016-1-1 to 2016-1-10
needs to bring them to 2016-2-1 to 2016-2-10
.
I wanted to minimize number of UI elements and clicks so I needed an intelligent solution that will guess what user wants without adding more UI elements. Base of that logic was:
- if any of date range boundaries (
from
orto
date) is the first or the last day of the month - add/substract a month but make sure resulting boundary remains the first/last day of the month. - if a date range boundary is not the first or the last day of the month - keep it that way but still take care of the overflow
The solution
I decided to extend standard JavaScript Date
. This is NOT a good practice (read this) but for this demonstration and my testing it was the simple thing to do.
Date.prototype.isLastDayOfMonth()
First I needed a helper to be used in nextMonth()
and prevMonth()
- any easy way to check if a date is the last day of the month:
Date.prototype.isLastDayOfMonth = function() {
let check = new Date(this.getTime())
check.setDate(check.getDate() + 1)
return check.getDate() === 1
}
Test is very simple. It adds a day to a date and checks if it became 1st in the month. Returns true
or false
.
Date.prototype.nextMonth()
Now to more difficult part:
Date.prototype.nextMonth = function() {
let oldDate = new Date(this)
let oldMonth = oldDate.getMonth()
let newDate = new Date(oldDate)
newDate.setMonth(oldMonth+1)
let newMonth = newDate.getMonth()
//prevent overflow
if (newMonth-oldMonth > 1) {
newDate.setDate(0)
} else {
//snap to last day of month (not needed if overflow was fixed)
if (oldDate.isLastDayOfMonth()) {
newDate = new Date(newDate.getFullYear(), newMonth + 1, 0)
}
}
return newDate
}
So, original date.setMonth(date.getMonth()+1)
idea is here extended with another check and that is overflow check. If adding a month resulted in month number difference larger than 1, we have an overflow!
Note that I ignored turn of the year because then, month 12 (which is 11 in JavaScript because months are zero-based) turns to month 1 (which is 0) and that makes a difference of -11 which makes newMonth-oldMonth > 1
return false and overflow will not be fixed. Thats is what we want because it is never needed (December and January always have 31 days so overflow cannot happen).
Overflow is simply fixed by newDate.setDate(0)
. Since dates are not zero-based in JavaScript (like months are), setting a date to 0 basically means 1 day before 1st in month, which is always the last day of previous month.
“Snapping” to the last day of the month
Another important check here is oldDate.isLastDayOfMonth()
. This is what makes it “intelligent”. This part tries to guess what user wants. So if original date was the last day of the month, we will make sure that result also is. Because most probably user does not expect to select ‘2016-2-29’, click next and get ‘2016-3-29’. We can bet he wants ‘2016-3-31’. This line fixes overflow: newDate = new Date(newDate.getFullYear(), newMonth + 1, 0)
There are two things we DON’T do here (for performance) and it might be important to notice them:
- When “snapping” to the last day of the month, we don’t check if date needs to be “snapped” at all. So, second part of check
oldDate.isLastDayOfMonth() && newDate.isLastDayOfMonth()
is not being done. This means we will “snap”newDate
even if it is already the last day of the month. That’s becauseisLastDayOfMonth()
is more expensive than snapping itself. At least it looks like that to me but I never tested this :) - We don’t do “snapping” if overflow was fixed before. That’s why I’ve put it in
else
within overflow check. Fact is, overflow fix sets the date to the last day of the month so we can be sure it’s not needed.
You can check finished nextMonth()
with quick demonstration:
cconsole.log('nextMonth')
console.log('2016-11-30 => ' + new Date('2016-11-30').nextMonth().toDateString())
console.log('2016-12-31 => ' + new Date('2016-12-31').nextMonth().toDateString())
console.log('2016-1-31 => ' + new Date('2016-1-31').nextMonth().toDateString())
console.log('2016-2-29 => ' + new Date('2016-2-29').nextMonth().toDateString())
console.log('2016-3-31 => ' + new Date('2016-3-31').nextMonth().toDateString())
console.log('2016-4-30 => ' + new Date('2016-4-30').nextMonth().toDateString())
console.log('----')
console.log('2016-4-29 => ' + new Date('2016-4-29').nextMonth().toDateString())
console.log('2016-1-30 => ' + new Date('2016-1-30').nextMonth().toDateString())
console.log('2016-2-15 => ' + new Date('2016-2-15').nextMonth().toDateString())
console.log('----')
console.log('2016-1-1 => ' + new Date('2016-1-1').nextMonth().toDateString())
console.log('2016-2-1 => ' + new Date('2016-2-1').nextMonth().toDateString())
console.log('2016-3-1 => ' + new Date('2016-3-1').nextMonth().toDateString())
console.log('2016-12-1 => ' + new Date('2016-12-1').nextMonth().toDateString())
Date.prototype.prevMonth()
Going to previous month is very similar:
Date.prototype.prevMonth = function() {
let oldDate = new Date(this)
let oldMonth = oldDate.getMonth()
let newDate = new Date(oldDate)
newDate.setMonth(oldMonth-1)
let newMonth = newDate.getMonth()
//prevent overflow
if (oldMonth-newMonth === 0) {
newDate.setDate(0)
} else {
//snap to last day of month (not needed if overflow fixed)
if (oldDate.isLastDayOfMonth()) {
newDate = new Date(newDate.getFullYear(), newMonth + 1, 0)
}
}
return newDate
}
In fact, only a few details are changed: setMonth
goes -1 when initialising newDate
and overflow condition is a bit different (if (oldMonth-newMonth === 0)
). This means you can easily merge prevMonth
and nextMonth
into single function if you want.
Some code to demonstrate prevMonth()
:
console.log('prevMonth')
console.log('2016-11-30 => ' + new Date('2016-11-30').prevMonth().toDateString())
console.log('2016-12-31 => ' + new Date('2016-12-31').prevMonth().toDateString())
console.log('2016-1-31 => ' + new Date('2016-1-31').prevMonth().toDateString())
console.log('2016-2-29 => ' + new Date('2016-2-29').prevMonth().toDateString())
console.log('2016-3-31 => ' + new Date('2016-3-31').prevMonth().toDateString())
console.log('2016-4-30 => ' + new Date('2016-4-30').prevMonth().toDateString())
console.log('----')
console.log('2016-4-29 => ' + new Date('2016-4-29').prevMonth().toDateString())
console.log('2016-1-30 => ' + new Date('2016-1-30').prevMonth().toDateString())
console.log('2016-2-15 => ' + new Date('2016-2-15').prevMonth().toDateString())
console.log('----')
console.log('2016-1-1 => ' + new Date('2016-1-1').prevMonth().toDateString())
console.log('2016-2-1 => ' + new Date('2016-2-1').prevMonth().toDateString())
console.log('2016-3-1 => ' + new Date('2016-3-1').prevMonth().toDateString())
console.log('2016-12-1 => ' + new Date('2016-12-1').prevMonth().toDateString())
And that’s it! I’m always looking to make things smaller/faster so please let me know if you have some better methods! With no external libraries of course :)
Leave a Comment