I'm building a project with express and I have a scheduling calendar. I want to give to my users next available day. Format YYYY-MM-DD.
Rules:
The next available day is usually tomorrow unless:
- After 4pm the next available day is two days from now (i.e. Monday afternoon they can book Wednesday);
- Friday after 4pm the next available day is Monday;
- For Saturday it's Monday;
- For Sunday it's Tuesday;
I also have an array of public holidays, which are also unavailable. If the next day is a public holiday, the app should return the day after.
When there is a public holiday my app goes into a loop and it runs the whole loop. I don't know how to fix this. I thought it would skip the loop when it runs the second time.
const publicHolidays = ['2018-09-28', '2018-12-25']
const availableDay = (nextDay) => {
const d = new Date();
const utc = d.getTime() + (d.getTimezoneOffset() * 60000);
const nd = new Date(utc + (3600000 * 8));
if (nextDay === undefined) {
nextDay = 1;
}
if (nd.getDay() === 5 && nd.getHours() > 15) {
nextDay = 3;
} else if ([0, 6].includes(nd.getDay()) || nd.getHours() > 15) {
nextDay = 2;
}
const day = new Date();
const tomorrow = new Date(day);
tomorrow.setDate(tomorrow.getDate() + nextDay);
const yy = tomorrow.getFullYear();
let mm = tomorrow.getMonth() + 1;
if (mm < 10) {
mm = `0${mm}`;
}
let dd = tomorrow.getDate();
if (dd < 10) {
dd = `0${dd}`;
}
const available = `${yy}-${mm}-${dd}`;
if (publicHolidays.includes(available)) {
const nextDay = 7;
for (let i = 2; i < nextDay; i += 1) {
availableDay(i);
}
} else {
console.log('returning available', available);
return(available);
}
}
availableDay()
I think this logic will work - I've created a function to do the "date string - yyyy-mm-dd" thing because it's used in two places now
I also check for weekends by tomorrow.getDay() % 6 === 0 - you can of course use [0, 6].includes(tomorrow.getDay()) if you prefer
const publicHolidays = ['2018-09-28', '2018-12-25']
const availableDay = () => {
let nextDay = 1; // since we are not recursive any more
const d = new Date();
const utc = d.getTime() + (d.getTimezoneOffset() * 60000);
const nd = new Date(utc + (3600000 * 8));
if (nd.getDay() === 5 && nd.getHours() > 15) {
nextDay = 3;
} else if ([0, 6].includes(nd.getDay()) || nd.getHours() > 15) {
nextDay = 2;
}
const day = new Date();
const tomorrow = new Date(day);
tomorrow.setDate(tomorrow.getDate() + nextDay);
// changes start here
const dateString = d => `${.getFullYear()}-${('0' + (d.getMonth() + 1)).toString(-2)}-${('0' + d.getDate()).toString(-2)}`;
let available = dateString(tomorrow);
while (publicHolidays.includes(available) || (tomorrow.getDay() === 0)) {
tomorrow.setDate(tomorrow.getDate() + 1);
available = dateString(tomorrow);
}
console.log('returning available', available);
return(available);
}
availableDay()
There's probably more you can do to streamline the code - but this should fix the problem at least
I think you should always + 1 to nextDay. so if today is public holiday, try get the next day. the cycle repeat until it is not public holiday.
if (publicHolidays.includes(available)) {
availableDay(nextDay +1 );
} else {
console.log('returning available', available);
return(available);
}
Here is a more generic solution that might be applicable for people searching for something similar:
/**
* #summary Finds the next available date between a range, excluding a list of unavailable dates
* #param {Date} startDate The beginning of the date range.
* #param {Date} endDate The beginning of the date range.
* #param {Array of Date} excludeDates Dates that are not available.
*/
export const findNextAvailableDate = (startDate, endDate, excludeDates) => {
const excludeDatesStrArr = excludeDates.map(date => {
// Make sure dates are in a consistent string format so we can check for equality
excludeDate.setUTCHours(0, 0, 0, 0)
return excludeDate.toISOString()
})
let possibleDate = startDate
possibleDate.setUTCHours(0, 0, 0, 0)
let possibleDateStr = possibleDate.toISOString()
while (possibleDateStr !== endDate) {
if (!excludeDatesStrArr.includes(possibleDateStr)) {
// Date is not in exclude array, return available date
return possibleDate
} else {
// Date is included in exclude array, iterate to the next day
const newDate = possibleDate.setDate(possibleDate.getDate() + 1)
possibleDate = new Date(newDate)
possibleDate.setUTCHours(0, 0, 0, 0)
possibleDateStr = possibleDate.toISOString()
}
}
// Did not find next available date
return false
}
Related
The line let dateInPlay = zzGetLastDateOfTheMonth(new Date(year, month + 1, 1)) is duplicated and I could not think of how to avoid that.
Thanks in advance for any improvements!
/**
* Gets number of full months between dates
*
* #author me
* #version 1.0
* #since 2022/10/24
* #param {Date} start date
* #param {Date} end date
* #return {number} number of months between dates
*/
function zzFullMonthsBetween(_startDate, _endDate) {
let firstDate = zzGetFirstDateOfTheMonth(_startDate)
let lastDate = zzGetLastDateOfTheMonth(_endDate)
if (lastDate > firstDate) {
let numberOfMonths = 1
if (firstDate.getFullYear() == lastDate.getFullYear() && firstDate.getMonth() == lastDate.getMonth())
return numberOfMonths
let year = firstDate.getFullYear()
let month = firstDate.getMonth()
if (month == 12)
{
year++
month = 1
}
let dateInPlay = zzGetLastDateOfTheMonth(new Date(year, month + 1, 1))
numberOfMonths++
do {
if (lastDate.getTime() == dateInPlay.getTime())
return numberOfMonths
else {
numberOfMonths++
let year = dateInPlay.getFullYear()
let month = dateInPlay.getMonth()
if (month == 12)
{
year++
month = 1
}
dateInPlay = zzGetLastDateOfTheMonth(new Date(year, month + 1, 1))
}
}
while (lastDate !== dateInPlay);
}
return undefined
}
// function TESTzzFullMonthsBetween()
// {
// let today = new Date()
// let dates = [new Date('1/1/2022'), new Date('10/24/2022'), new Date('11/27/2022'), new Date('12/5/2022'), new Date('1/2/2023'), new Date('2/1/2023'), new Date('2/2/2024'), ]
// for (let z = 0; z < dates.length; z++)
// Logger.log(`${dates[z]}: ${zzFullMonthsBetween(today, dates[z])}`)
// }
function zzGetFirstDateOfTheMonth(_date)
{
return new Date(_date.getFullYear(), _date.getMonth(), 1)
}
function zzGetLastDateOfTheMonth(_date)
{
let year = _date.getFullYear()
let month = _date.getMonth()
if (month == 12)
{
year++
month = 1
}
return zzAddOnDate(new Date(year, month + 1, 1), -1)
}
function zzAddOnDate(_date, _daysToAdd)
{
var newDate = new Date(_date);
newDate.setDate(newDate.getDate() + _daysToAdd);
return newDate;
}
perhaps someone can give me some good ideas of how to accomplish this.
I would like to get the weekdays for either the first two weeks or last two weeks of a month based on the current date.
So if we are using the following to get the date today (2022-07-06)
const current = new Date();
const date = `${current.getFullYear()}-${current.getMonth()+1}-${current.getDate()}`;
The results I would be looking for are
const firstHalfWeekdates = ['2022-07-04', '2022-07-05', '2022-07-06', '2022-07-07', '2022-07-08', '2022-07-11', '2022-07-12', '2022-07-13', '2022-07-14', '2022-07-15']
and if the date fell on 2022-07-18 it would return
const secondHalfWeekdates = ['2022-07-18', '2022-07-19', '2022-07-20', '2022-07-21', '2022-07-22', '2022-07-25', '2022-07-26', '2022-07-27', '2022-07-28', '2022-07-29']
Also happy to use a library
Maybe this can give you a start. It returns the weekdays, divided by week.
I was trying to do everything you asked, but I ran into some problems, for example you want to divide the month into 4 weeks, 2 in the first half and 2 in the second half, but for example this month now July/2022, has a week that has only one weekday (July 1º), but in your expected results you ignored this week, whats the logic to ignore weeks? It has to be a complete week with 5 weekdays?
What about last month Jun/2022, there were not 4 complete weeks, there were only 3 complete weeks, the other 2 has 3 and 4 days respectively, which week would you ignore in this case?
function isWeekDay(day) {
return day != 0 && day != 6;
}
function formatDateYYYYMMDD(date) {
let dateString = date.toLocaleDateString('en-GB');
let year = dateString.substring(6, 10);
let month = dateString.substring(3, 5);
let day = dateString.substring(0, 2);
return `${year}-${month}-${day}`;
}
function getWeekdaysOfTheCurrentMonthDividedByWeek() {
let currentDate = new Date();
let month = currentDate.getMonth();
let weekdays = [];
let tempDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
let week = [];
while (tempDate.getMonth() == month) {
if (isWeekDay(tempDate.getDay())) {
week.push(formatDateYYYYMMDD(tempDate));
} else if (week.length > 0) {
weekdays.push(week);
week = [];
}
tempDate.setDate(tempDate.getDate() + 1);
}
return weekdays;
}
console.log(getWeekdaysOfTheCurrentMonthDividedByWeek());
You could split the month up into calendar weeks. (e.g. for July 2022 it would be: July 1-2, 3-9, 10-16, ect…)
Then, depending on the day, take either the first or second half of the weeks.
Iterate over the filtered weeks, counting the weekdays.
I choose to include the third week in the first half of the month if there were 5 weeks, but you could change that by changing Math.ceil to Math.floor
/**
* Get the last item in an array, or undefined if the array is empty.
* #template T
* #param {[T]} array
* #returns {T|undefined}
*/
const lastItem = array => array[array.length - 1];
const getWeekdays = current => {
/** #type {[[Date]]} */
const weeks = [];
// Get the weeks
/**
* Get the calendar week of the given date.
* #param {Date} firstDay The first day of the week.
* #returns {[Date]}
*/
const getWeek = firstDay => {
/** #type {[Date]} */
let days = [];
let dateToTest = new Date(firstDay);
// Continue until the end of the week or month, whichever comes first.
while (
dateToTest.getDay() <= 6 &&
dateToTest.getMonth() == firstDay.getMonth()
) {
days.push(new Date(dateToTest));
dateToTest.setDate(dateToTest.getDate() + 1);
}
return days;
};
// The first day of the month
const firstDay = new Date(current.getFullYear(), current.getMonth());
let dateToTest = new Date(firstDay);
do {
weeks.push(getWeek(dateToTest));
dateToTest = new Date(lastItem(lastItem(weeks)));
dateToTest.setDate(dateToTest.getDate() + 1);
} while (dateToTest.getMonth() == firstDay.getMonth());
// Filter to half of the month
// Get the week of the given date
let currentWeek = 0;
weekLoop: for (let i = 0; i < weeks.length; i++) {
const week = weeks[i];
for (const day of week) {
if (day == current) {
currentWeek = i;
break weekLoop;
}
}
}
/** #type {[[Date]]} */
let weeksInHalf = [];
const numOfWeeksInFirstHalf = Math.ceil(weeks.length / 2),
numOfWeeksInSecondHalf = weeks.length - numOfWeeksInFirstHalf;
for (
let i = 0;
i <
(currentWeek < numOfWeeksInFirstHalf
? numOfWeeksInFirstHalf
: numOfWeeksInSecondHalf);
i++
) {
weeksInHalf.push(weeks[i]);
}
// Filter out weekends
// Format dates
return weeksInHalf
.flat()
.filter(day => day.getDay() > 0 && day.getDay() < 6)
.map(
day => `${day.getFullYear()}-${day.getMonth() + 1}-${day.getDate()}`
);
};
// Tests
for (let i = 0; i < 12; i++) {
const weekdays = getWeekdays(new Date(2022, i));
weekdays.forEach(dateString => {
const [year, month, day] = dateString.split("-");
const date = new Date(year, month - 1, day);
if (date.getDay() == 0 || date.getDay() == 6)
throw new Error("Invalid day: (day)");
else console.log(dateString)
});
}
I want to add all Saturdays of 2020/2021 into an object like following:
saturdays = {
"Saturday, 22.02.2020" : "Saturday, 22.02.2020" ,
"Saturday, 29.02.2020" : "Saturday, 29.02.2020"
}
1) First find out number of days in year (daysInYear method)
2) Find out first saturday date (firstSatDate method)
3) Go over for loop from first saturday to end of the year day and build the date string requirement format.
const daysInYear = year =>
(new Date(year + 1, 0, 1) - new Date(year, 0, 1)) / (24 * 60 * 60 * 1000);
const firstSatDate = year => {
const week_day = new Date(year, 0, 1).getDay();
const satDate = new Date(year, 0, 7 - week_day);
return satDate.getDate();
};
const getSaturdays = year => {
const yearDays = daysInYear(year);
const first = firstSatDate(year);
const saturdays = {};
for (let day = first; day <= yearDays; day = day + 7) {
const date = new Date(year, 0, day);
const day_str = String(date.getDate()).padStart(2, '0');
const month_str = String(date.getMonth() + 1).padStart(2, '0');
const date_str = `Saturday, ${day_str}.${month_str}.${date.getFullYear()}`;
saturdays[date_str] = date_str;
}
return saturdays;
};
console.log(getSaturdays(2020));
console.log(getSaturdays(2021));
It is an unusual format (name is the same as the value) and objects are not ordered so don't expect to get them back in any particular order . . . but it is pretty trivial to do if you want . . .
saturdays = {};
function loadSaturdays(startYear, endYear) {
const SATURDAY = 6;
let start = new Date("01/01/" + startYear);
let end = new Date("12/31/" + endYear);
var dateOptions = {weekday: 'long', year: 'numeric', month: 'numeric', day: 'numeric'};
var current = new Date(start);
while (current <= end) {
if (SATURDAY === current.getDay()) {
let newSaturday = "\"" + current.toLocaleString('en-GB', dateOptions).replace(/\//gi, '.') + "\"";
// if you want to see the individual ones as you are building the object
// console.log(newSaturday);
saturdays[newSaturday] = newSaturday;
}
current = new Date(current.setDate(current.getDate() + 1));
}
}
loadSaturdays("2020", "2021");
// if you want to see the entire object
//console.log(saturdays);
// objects are not ordered but they are all there
for (saturday in saturdays) {
console.log(saturday);
}
I'm setting a default follow-up date two days from current date, which currently works:
const Notify = moment().add(2, 'days').toDate();
However, I would like to exclude weekends. So I installed moment WeekDay, but I can't seem to get it to work with adding days to the current date. The documentation calls for:
moment().weekday(0)
But I can't get that to work with adding in two days forward. Any ideas?
This solution is simple, easy to follow, and works well for me:
function addBusinessDays(originalDate, numDaysToAdd) {
const Sunday = 0;
const Saturday = 6;
let daysRemaining = numDaysToAdd;
const newDate = originalDate.clone();
while (daysRemaining > 0) {
newDate.add(1, 'days');
if (newDate.day() !== Sunday && newDate.day() !== Saturday) {
daysRemaining--;
}
}
return newDate;
}
Try: moment-business-days
It should help you.
Example:
var momentBusinessDays = require("moment-business-days")
momentBusinessDays('20-09-2018', 'DD-MM-YYYY').businessAdd(3)._d
Result:
Tue Sep 25 2018 00:00:00 GMT+0530 (IST)
You could also not use external lib and do a simple function like one of these two:
const WEEKEND = [moment().day("Saturday").weekday(), moment().day("Sunday").weekday()]
const addBusinessDays1 = (date, daysToAdd) => {
var daysAdded = 0,
momentDate = moment(new Date(date));
while (daysAdded < daysToAdd) {
momentDate = momentDate.add(1, 'days');
if (!WEEKEND.includes(momentDate.weekday())) {
daysAdded++
}
}
return momentDate;
}
console.log(addBusinessDays1(new Date(), 7).format('MM/DD/YYYY'))
console.log(addBusinessDays1('09-20-2018', 3).format('MM/DD/YYYY'))
// This is the somewhat faster version
const addBusinessDays2 = (date, days) => {
var d = moment(new Date(date)).add(Math.floor(days / 5) * 7, 'd');
var remaining = days % 5;
while (remaining) {
d.add(1, 'd');
if (d.day() !== 0 && d.day() !== 6)
remaining--;
}
return d;
};
console.log(addBusinessDays2(new Date(), 7).format('MM/DD/YYYY'))
console.log(addBusinessDays2('09-20-2018', 3).format('MM/DD/YYYY'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
They are slightly modified from this post and I think are a good alternative to external library you have to carry/deal with (assuming this is the only part you need and not other features of that lib).
This will do it based on any starting date, and without a costly loop. You calculate the number of weekend days you need to skip over, then just offset by the number of weekdays and weekends, together.
function addWeekdays(year, month, day, numberOfWeekdays) {
var originalDate = year + '-' + month + '-' + day;
var futureDate = moment(originalDate);
var currentDayOfWeek = futureDate.day(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
var numberOfWeekends = Math.floor((currentDayOfWeek + numberOfWeekdays - 1) / 5); // calculate the number of weekends to skip over
futureDate.add(numberOfWeekdays + numberOfWeekends * 2, 'days'); // account for the 2 days per weekend
return futureDate;
}
const addWorkingDays = (date: Moment, days: number) => {
let newDate = date.clone();
for (let i = 0; i < days; i++) {
if (newDate.isoWeekday() !== 6 && newDate.isoWeekday() !== 7) {
newDate = newDate.add(1, "days");
} else {
newDate = newDate.add(1, "days");
i--;
}
}
return newDate.format("YYYY/MM/DD");
};
var moment = require("moment")
function addWorkingDay(date, days){
let daysToAdd = days
const today = moment(date);
const nextWeekStart = today.clone().add(1, 'week').weekday(1);
const weekEnd = today.clone().weekday(5);
const daysTillWeekEnd = Math.max(0, weekEnd.diff(today, 'days'));
if(daysTillWeekEnd >= daysToAdd) return today.clone().add(daysToAdd, 'days');
daysToAdd = daysToAdd - daysTillWeekEnd - 1;
return nextWeekStart.add(Math.floor(daysToAdd/5), 'week').add(daysToAdd % 5, 'days')
}
I think this code will be faster:
var businessDays = 10;
var days = businessDays + Math.floor((Math.min(moment().day(),5)+businessDays)/6)*2;
moment.add(days, 'days');
// using pure JS
function addBusinessDays(originalDate, numDaysToAdd) {
const Sunday = 0;
const Saturday = 6;
let daysRemaining = numDaysToAdd;
const newDate = originalDate;
while (daysRemaining > 0) {
newDate.setDate(newDate.getDate() + 1);
if (newDate.getDay() !== 0 && newDate.getDay() !== 6) {
// skip sunday & saturday
daysRemaining--;
}
}
return newDate;
}
var dt = new Date(); // get date
var business_days = 8;
newDate = addBusinessDays(dt, business_days);
console.log(newDate.toString());
From JavaScript is there a way to get list of days between two dates from MySQL format. I don't want to use any library for this.
This is what i did.
function generateDateList(from, to) {
var getDate = function(date) { //Mysql Format
var m = date.getMonth(), d = date.getDate();
return date.getFullYear() + '-' + (m < 10 ? '0' + m : m) + '-' + (d < 10 ? '0' + d : d);
}
var fs = from.split('-'), startDate = new Date(fs[0], fs[1], fs[2]), result = [getDate(startDate)], start = startDate.getTime(), ts, end;
if ( typeof to == 'undefined') {
end = new Date().getTime();
} else {
ts = to.split('-');
end = new Date(ts[0], ts[1], ts[2]).getTime();
}
while (start < end) {
start += 86400000;
startDate.setTime(start);
result.push(getDate(startDate));
}
return result;
}
console.log(generateDateList('2014-2-27', '2014-3-2'));
I test it from chrome and nodejs below are the result.
[ '2014-02-27',
'2014-02-28',
'2014-02-29',
'2014-02-30',
'2014-02-31',
'2014-03-01',
'2014-03-02' ]
yeh big leap year:-D..., how can i fix this? or is there any better way.?
const listDate = [];
const startDate ='2017-02-01';
const endDate = '2017-02-10';
const dateMove = new Date(startDate);
let strDate = startDate;
while (strDate < endDate) {
strDate = dateMove.toISOString().slice(0, 10);
listDate.push(strDate);
dateMove.setDate(dateMove.getDate() + 1);
};
Take the start date and increment it by one day until you reach the end date.
Note: MySQL dates are standard format, no need to parse it by hand just pass it to the Date constructor: new Date('2008-06-13').
const addDays = (date, days = 1) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const dateRange = (start, end, range = []) => {
if (start > end) return range;
const next = addDays(start, 1);
return dateRange(next, end, [...range, start]);
};
const range = dateRange(new Date("2014-02-27"), new Date("2014-03-02"));
console.log(range);
console.log(range.map(date => date.toISOString().slice(0, 10)))
Here I use a recursive function, but you could achieve the same thing using a while (see other answers).
I have used this one from
https://flaviocopes.com/how-to-get-days-between-dates-javascript/
const getDatesBetweenDates = (startDate, endDate) => {
let dates = []
//to avoid modifying the original date
const theDate = new Date(startDate)
while (theDate < new Date(endDate)) {
dates = [...dates, new Date(theDate)]
theDate.setDate(theDate.getDate() + 1)
}
dates = [...dates, new Date(endDate)]
return dates
}
Invoke the function as follows:
getDatesBetweenDates("2021-12-28", "2021-03-01")
Note - I just had to fix issues with the Date object creation (new Date()) in the while loop and in the dates array. Other than that the code is pretty much same as seen on the above link
dateRange(startDate, endDate) {
var start = startDate.split('-');
var end = endDate.split('-');
var startYear = parseInt(start[0]);
var endYear = parseInt(end[0]);
var dates = [];
for(var i = startYear; i <= endYear; i++) {
var endMonth = i != endYear ? 11 : parseInt(end[1]) - 1;
var startMon = i === startYear ? parseInt(start[1])-1 : 0;
for(var j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j+1) {
var month = j+1;
var displayMonth = month < 10 ? '0'+month : month;
dates.push([i, displayMonth, '01'].join('-'));
}
}
return dates;
}
var oDate1 = oEvent.getParameter("from"),
oDate2 = oEvent.getParameter("to");
var aDates = [];
var currentDate = oDate1;
while (currentDate <= oDate2) {
aDates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1);
}
I expanded Công Thắng's great answer to return {years, months, days}, thought it was worth sharing:
function getDates(startDate, endDate) {
const days = [],
months = new Set(),
years = new Set()
const dateMove = new Date(startDate)
let date = startDate
while (date < endDate){
date = dateMove.toISOString().slice(0,10)
months.add(date.slice(0, 7))
years.add(date.slice(0, 4))
days.push(date)
dateMove.setDate(dateMove.getDate()+1) // increment day
}
return {years: [...years], months: [...months], days} // return arrays
}
console.log(getDates('2016-02-28', '2016-03-01')) // leap year
/* =>
{
years: [ '2016' ],
months: [ '2016-02', '2016-03' ],
days: [ '2016-02-28', '2016-02-29', '2016-03-01' ]
}
*/
const {months} = getDates('2016-02-28', '2016-03-01') // get only months
Basically the function just increments the built-in Date object by one day from start to end, while the Sets capture unique months and years.