Calculate an expected delivery date (accounting for holidays) in business days using JavaScript?

After revisiting this script, and some modifications, the following is available to allow a user to add a feature that calculates the expected delivery date.

// array of ISO YYYY-MM-DD format dates
publicHolidays = {
    uk:["2020-01-01","2020-04-10","2020-04-13","2020-05-08","2020-05-25",
        "2020-08-03","2020-08-31","2020-12-25","2020-12-28"],
    usa:["2020-01-01","2020-01-20","2020-02-14","2020-02-17","2020-04-10",
        "2020-04-12","2020-05-10","2020-05-25","2020-06-21","2020-07-03",
        "2020-07-04","2020-09-07","2020-10-12","2020-10-31","2020,11,11",
        "2020-11-26","2020-12-25"]
}
// check if there is a match in the array
Date.prototype.isPublicHoliday = function( data ){// we check for a public holiday
    if(!data) return 1;
return data.indexOf(this.toISOString().slice(0,10))>-1? 0:1;
}

// calculation of business days
Date.prototype.businessDays = function( d, holidays ){
    var holidays = holidays || false, t = new Date( this ); // copy date.
    while( d ){ // we loop while d is not zero...   
        t.setDate( t.getDate() + 1 ); // set a date and test it
        switch( t.getDay() ){ // switch is used to allow easier addition of other days of the week
            case 0: case 6: break;// sunday & saturday
            default: // check if we are a public holiday or not
                d -= t.isPublicHoliday( holidays ); 
        }
    }
    return t.toISOString().slice(0,10); // just the YYY-MM-DD 
}

// dummy var, could be a form field input
OrderDate = "2020-02-12";
// test with a UK holiday date
var deliveryDate = new Date(OrderDate).businessDays(7, publicHolidays.usa);
// expected output 2020-02-25
console.log("Order date: %s, Delivery date: %s",OrderDate,deliveryDate );

Order date: 2020-02-12, Delivery date: 2020-02-25


The prototype is written to allow inputs from forms (HTML5 forms) of date type inputs as they are already in an ISO YYYY-MM-DD format and the output is formatted as such should that be needing to update a particular field.

The typical use would be...

var delDate = new Date( ISOdate ).businessDays( addBusinessDays, holidayData );

where the delDate is an ISO format date, eg, 2020-01-01

Answers:

Answer

I've adapted Mark Giblin's revised code to better deal with end of year dates and also U.S. federal holidays. See below...

function businessDaysFromDate(date,businessDays) {
  var counter = 0, tmp = new Date(date);
  while( businessDays>=0 ) {
    tmp.setTime( date.getTime() + counter * 86400000 );
    if(isBusinessDay (tmp)) {
      --businessDays;
    }
    ++counter;
  }
  return tmp;
}

function isBusinessDay (date) {
  var dayOfWeek = date.getDay();
  if(dayOfWeek === 0 || dayOfWeek === 6) {
    // Weekend
    return false;
  }

  holidays = [
    '12/31+5', // New Year's Day on a saturday celebrated on previous friday
    '1/1',     // New Year's Day
    '1/2+1',   // New Year's Day on a sunday celebrated on next monday
    '1-3/1',   // Birthday of Martin Luther King, third Monday in January
    '2-3/1',   // Washington's Birthday, third Monday in February
    '5~1/1',   // Memorial Day, last Monday in May
    '7/3+5',   // Independence Day
    '7/4',     // Independence Day
    '7/5+1',   // Independence Day
    '9-1/1',   // Labor Day, first Monday in September
    '10-2/1',  // Columbus Day, second Monday in October
    '11/10+5', // Veterans Day
    '11/11',   // Veterans Day
    '11/12+1', // Veterans Day
    '11-4/4',  // Thanksgiving Day, fourth Thursday in November
    '12/24+5', // Christmas Day
    '12/25',   // Christmas Day
    '12/26+1',  // Christmas Day
  ];

  var dayOfMonth = date.getDate(),
  month = date.getMonth() + 1,
  monthDay = month + '/' + dayOfMonth;

  if(holidays.indexOf(monthDay)>-1){
    return false;
  }

  var monthDayDay = monthDay + '+' + dayOfWeek;
  if(holidays.indexOf(monthDayDay)>-1){
    return false;
  }

  var weekOfMonth = Math.floor((dayOfMonth - 1) / 7) + 1,
      monthWeekDay = month + '-' + weekOfMonth + '/' + dayOfWeek;
  if(holidays.indexOf(monthWeekDay)>-1){
    return false;
  }

  var lastDayOfMonth = new Date(date);
  lastDayOfMonth.setMonth(lastDayOfMonth.getMonth() + 1);
  lastDayOfMonth.setDate(0);
  var negWeekOfMonth = Math.floor((lastDayOfMonth.getDate() - dayOfMonth - 1) / 7) + 1,
      monthNegWeekDay = month + '~' + negWeekOfMonth + '/' + dayOfWeek;
  if(holidays.indexOf(monthNegWeekDay)>-1){
    return false;
  }

  return true;
}
Answer

Thanks for your input guys, I had a long hard re-think over the approach I was making for this and came up with this little number...

var businessDays = 7, counter = 0; // set to 1 to count from next business day
while( businessDays>0 ){
    var tmp = new Date();
    var startDate = new Date();
    tmp.setDate( startDate .getDate() + counter++ );
    switch( tmp.getDay() ){
            case 0: case 6: break;// sunday & saturday
            default:
                businessDays--;
            }; 
}

The idea was to start with the business days and count backwards to zero for each day encountered that fell in to the range of a business day. This use of switch would enable a person to declare a day in the week as a non-business day, for example someone may not work on a monday, therefore the addition of case:1 would include a monday.

This is a simple script and does not take in to account public or bank holidays, that would be asking for a much more complex script to work with.

The result is a date that is set to the date of shipping, the user can then extract the date info in any format that they please, eg.

var shipDate = tmp.toUTCString().slice(1,15);
Answer

We have UI that defaults search inputs to last business day or a week-ago. Here's something that works both forward and backward.

// add (or subtract) business days to provided date
addBusinessDays = function (startingDate, daysToAdjust) {
    var newDate = new Date(startingDate.valueOf()),
        businessDaysLeft,
        isWeekend,
        direction;

    // Timezones are scary, let's work with whole-days only
    if (daysToAdjust !== parseInt(daysToAdjust, 10)) {
        throw new TypeError('addBusinessDays can only adjust by whole days');
    }

    // short-circuit no work; make direction assignment simpler
    if (daysToAdjust === 0) {
        return startingDate;
    }
    direction = daysToAdjust > 0 ? 1 : -1;

    // Move the date in the correct direction
    // but only count business days toward movement
    businessDaysLeft = Math.abs(daysToAdjust);
    while (businessDaysLeft) {
        newDate.setDate(newDate.getDate() + direction);
        isWeekend = newDate.getDay() in {0: 'Sunday', 6: 'Saturday'};
        if (!isWeekend) {
            businessDaysLeft--;
        }
    }
    return newDate;
};

It would be easy to pass in an optional holidays data structure and adjust for that as well.

However, generating a holidays data structure, well, that will take a little more effort and is specific not only to every country and region, but also to every organization.

Answer

Change line 7 from

ship.setDate( safety );  // add a number of days

to

ship.setDate( ship.getDate() + safety );

The problem was that you want to add days, not set days.

Answer

Your main problem was that adding safety each time meant you were adding multiple days each time it looped, instead of 1. So first loop = 1, second = 1+2, etc.

I believe this works as you'd like:

var businessDays = 10; // this will come from a form
var counter = 0;  // I have a counter
var safety = 0;  // I have a safety variable
var ship = today = new Date();  // I have the current date and an initialized shipping variable but the buy date will come from a form
console.log(">>> today = " + today);
// now the loop...

while( ++safety <30 ){
ship.setDate(ship.getDate()+1 );
switch( ship.getDay() ){

    case 0: // Sunday
    case 6: // Saturday

    break;

    default:
        counter++;
    }

if( counter >= businessDays ) break;

}
  // add a number of days
// the expected shipping date


console.log(">>> days  = " + businessDays);
console.log(">>> ship = " + ship);
Answer

I needed something similar but a little different and this is what I came up with.

Holidays are added in an object with one key for each month that has holidays. That key then has an array of days in that month that are considered holidays.

function getDueDate(date) {

    var numBusinessDays = 20;

    var saturday = 6;
    var sunday = 0;

    var holidays = {

        /* Months are zero-based. 0 = Jan, 11 = Dec */
        0: [1, 2],
        1: [6],
        3: [24],
        11: [25, 26]

    };

    var dayOfWeek = null;
    var dayOfMonth = null;
    var month = null;
    var isWeekday = null;
    var monthHolidays = null;
    var isHoliday = null;

    while (numBusinessDays > 0) {

        dayOfWeek = date.getDay();
        dayOfMonth = date.getDate();
        month = date.getMonth();
        isWeekday = dayOfWeek !== saturday && dayOfWeek !== sunday;
        monthHolidays = holidays[month];

        isHoliday = monthHolidays
            ? monthHolidays.indexOf(dayOfMonth) > -1
            : false;

        if (isWeekday && !isHoliday) --numBusinessDays;

        date.setDate(dayOfMonth + 1);

    }

    return date;

}
Answer
ship.setDate( safety );  // add a number of days

Doesn't add days. It sets the day. More info

The setDate() method sets the day of the Date object relative to the beginning of the currently set month.

If you want to add days do something like this:

ship.setDate(ship.getDate()+1);
Answer

I just found this script which is working nice, you can give an optional array for your country's holidays

function addBusinessDays(date, days, holidays) {
    var calendar = java.util.Calendar.getInstance();
    calendar.setTimeInMillis(date.getTime());
    var numberOfDaysToAdd = Math.abs(days);
    var daysToAdd = days < 0 ? -1 : 1;
    var businessDaysAdded = 0;


    function isHoliday(dateToCheck) {
        if (holidays && holidays.length > 0) {
            for (var i = 0; i < holidays.length; i++) {
                if (holidays[i].getFullYear() == dateToCheck.get(java.util.Calendar.YEAR) && holidays[i].getMonth() == dateToCheck.get(java.util.Calendar.MONTH) && holidays[i].getDate() == dateToCheck.get(java.util.Calendar.DAY_OF_MONTH)) {
                    return true;
                }
            }
        }
        return false;
    }

    while (businessDaysAdded < numberOfDaysToAdd) {
        calendar.add(java.util.Calendar.DATE, daysToAdd);
        if (calendar.get(java.util.Calendar.DAY_OF_WEEK) == java.util.Calendar.SATURDAY || calendar.get(java.util.Calendar.DAY_OF_WEEK) == java.util.Calendar.SUNDAY) {
            // add another day
            continue;
        }
        if (isHoliday(calendar)) {
            // add another day
            continue;
        }
        businessDaysAdded ++;
    }

    return new Date(calendar.getTimeInMillis());
}

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.