9

I need a Date.prototype.addBusDays function that'll take an integer as the number of working days to add to the date.

However, there are two considerations: 1. Weekends, 2. Holidays (which I imagine would be a preset array to compare against. If beginning date and end date contain 3 holidays, then you push out the end date by 3)

I have come across some scripts online, one dilemma I can think of is, lets say you address all the weekends first, then you do the holidays, what if you +1 day (due to holiday), and your end date is pushed into a weekends again...<

Any ideas? Thanks!

EDIT:

This is a part of a scheduling tool I am developing, which mean the dates will be tied to tasks which are linked together. Adding 1 day to a task, will trigger a recalculation of everything tied to it, potentially all dates in the database.

isherwood
  • 58,414
  • 16
  • 114
  • 157
William Sham
  • 12,849
  • 11
  • 50
  • 67
  • Which method is most efficient depends on the number of days liable to be added. If the number is small, a simple loop suffices. – Orbling Jun 27 '11 at 23:07
  • @William Sham: If you are doing a scheduling tool, it might be better to store the relative dates in such a way that such a large scale update is rarely, if ever, caused. – Orbling Jun 27 '11 at 23:29
  • @Orbling: it won't be possible simply because it's scheduling. You want to see the effect on the final date of your project when you expand the duration of a task by 1 week, and push everything out. – William Sham Jun 27 '11 at 23:34
  • @William Sham: Yes, I am very familiar with scheduling software. Usually most events within a critical path are stored as durations and sets of edges to parents, so a change in one event does not usually lead to changes to the rest of the table, just a different rendering of the critical path. – Orbling Jun 27 '11 at 23:41
  • @Orbling: that's a very interesting way to look at it. Is there any article I can read about this? – William Sham Jun 27 '11 at 23:47
  • @Orbling: What's the name of the concept you talking about, so I can look it up – William Sham Jun 28 '11 at 00:02
  • @William Sham: [Critical Path Analysis](http://en.wikipedia.org/wiki/Critical_path_method) for the most part. Have a look at [PERT charts](http://en.wikipedia.org/wiki/Program_Evaluation_and_Review_Technique) too, they can explain the methodology better. – Orbling Jun 28 '11 at 00:35
  • @Orbling: Yes, I'm familiar with those. But what do you mean stored as duration and sets of edges < – William Sham Jun 28 '11 at 06:56
  • @William Sham: CPA is a subset of graph theory, so if you know it, you must know about edges, vertices and weights. – Orbling Jun 28 '11 at 16:07
  • @orbling: yes. But how you do this in programing. You said with critical path stored as duration and set of edges, you don't have to recalculate start and finish of the next activities if preceding one lengthens by 100 days, and would render correctly still. How would u know how many holiday the succeeding tasks would span such that its end date would change accordingly. – William Sham Jun 28 '11 at 16:59

6 Answers6

9

Datageek's solution helped me but I needed to augment it. This still doesn't do holidays but does do working days with the option of including Sat and/or Sun, and does support adding negative days:-

function AddWorkingDays(datStartDate, lngNumberOfWorkingDays, blnIncSat, blnIncSun) {
    var intWorkingDays = 5;
    var intNonWorkingDays = 2;
    var intStartDay = datStartDate.getDay(); // 0=Sunday ... 6=Saturday
    var intOffset;
    var intModifier = 0;

    if (blnIncSat) { intWorkingDays++; intNonWorkingDays--; }
    if (blnIncSun) { intWorkingDays++; intNonWorkingDays--; }
    var newDate = new Date(datStartDate)
    if (lngNumberOfWorkingDays >= 0) {
        // Moving Forward
        if (!blnIncSat && blnIncSun) {
            intOffset = intStartDay;
        } else {
            intOffset = intStartDay - 1;
        }
        // Special start Saturday rule for 5 day week
        if (intStartDay == 6 && !blnIncSat && !blnIncSun) {
            intOffset -= 6;
            intModifier = 1;
        }
    } else {
        // Moving Backward
        if (blnIncSat && !blnIncSun) {
            intOffset = intStartDay - 6;
        } else {
            intOffset = intStartDay - 5;
        }
        // Special start Sunday rule for 5 day week
        if (intStartDay == 0 && !blnIncSat && !blnIncSun) {
            intOffset++;
            intModifier = 1;
        }
    }
    // ~~ is used to achieve integer division for both positive and negative numbers
    newDate.setTime(datStartDate.getTime() + (new Number((~~((lngNumberOfWorkingDays + intOffset) / intWorkingDays) * intNonWorkingDays) + lngNumberOfWorkingDays + intModifier)*86400000));
    return newDate;
}
EpochGrinder
  • 109
  • 1
  • 5
4

Have a look at the following implementation. Sourced from about.com

addWeekdays = function(date, dd) {
  var wks = Math.floor(dd/5);
  var dys = dd.mod(5);
  var dy = this.getDay();
  if (dy === 6 && dys > -1) {
     if (dys === 0) {dys-=2; dy+=2;}
     dys++; dy -= 6;
  }
  if (dy === 0 && dys < 1) {
    if (dys === 0) {dys+=2; dy-=2;}
    dys--; dy += 6;
  }
  if (dy + dys > 5) dys += 2;
  if (dy + dys < 1) dys -= 2;
  date.setDate(date.getDate()+wks*7+dys);
}

var date = new Date();
addWeekdays(date, 9);
Datageek
  • 25,977
  • 6
  • 66
  • 70
1

(Updated) I've put this algorithm through its paces and it seems stable, though it does use recursion for holiday processing:

holidays = [new Date("2/13/2019"), new Date("2/19/2019")];

function addWorkdays(workdays, startDate) {
  //Make adjustments if the start date is on a weekend
  let dayOfWeek = startDate.getDay();
  let adjustedWorkdays = Math.abs(workdays);
  if (0 == dayOfWeek || 6 == dayOfWeek) {
    adjustedWorkdays += (Math.abs((dayOfWeek % 5) + Math.sign(workdays)) % 2) + 1;
    dayOfWeek = (dayOfWeek - 6) * -1;
  }
  let endDate = new Date(startDate);
  endDate.setDate(endDate.getDate() + (((Math.floor(((workdays >= 0 ? dayOfWeek - 1 : 6 - dayOfWeek) + adjustedWorkdays) / 5) * 2) + adjustedWorkdays) * (workdays < 0 ? -1 : 1)));
  //If we cross holidays, recompute our end date accordingly
  let numHolidays = holidays.reduce(function(total, holiday) { return (holiday >= Math.min(startDate, endDate) && holiday <= Math.max(startDate, endDate)) ? total + 1 : total; }, 0);
  if (numHolidays > 0) {
    endDate.setDate(endDate.getDate() + Math.sign(workdays));
    return addWorkdays((numHolidays - 1) * Math.sign(workdays), endDate);
  } else return endDate;
}
Ellie
  • 11
  • 2
  • How does this account for holidays (one of the two basic requirements posed in the question)? – JaMiT Feb 15 '19 at 00:49
0

I expanded on khellendros74's answer for a project of mine that needed to disable Sundays and mailing holidays in the datepicker and return two dates on press of a button: three business days (i.e. non-holiday and non-Sunday) after the date picked in the datepicker (a field with an id of "calendar") and six business days after the date picked in the datepicker and then put those two results into a couple of disabled input fields (handDelivered and mailed). The button press calls the function calculateDates. Here is that code:

var disabledDates = ['11/11/2015', '11/26/2015', '12/25/2015', '01/01/2016','01/18/2016', '02/15/2016','05/30/2016', '07/04/2016','09/05/2016','10/10/2016','11/11/2016','11/24/2016', '12/26/2016','01/02/2017','01/16/2017', '02/20/2017','05/29/2017', '07/04/2017','09/04/2017','10/09/2017','11/10/2017','11/23/2017', '12/25/2017','01/01/2018','01/15/2018', '02/19/2018','05/28/2018', '07/04/2018','09/03/2018','10/08/2018','11/12/2018','11/22/2018', '12/25/2018','01/01/2019','01/21/2019', '02/18/2019','05/27/2019', '07/04/2019','09/02/2019','10/14/2019','11/11/2019','11/28/2019', '12/25/2019','01/01/2020','01/20/2020', '02/17/2020','05/25/2020', '07/03/2020','09/07/2020','10/11/2020','11/26/2020','11/26/2020', '12/25/2020'];

$(function(){

    $('#calendar').datepicker({
        dateFormat: 'mm/dd/yy',
        beforeShowDay: editDays
    });

    function editDays(date) {
        for (var i = 0; i < disabledDates.length; i++) {
            if (new Date(disabledDates[i]).toString() == date.toString() || date.getDay() == 0) {             
                 return [false];
            }
        }
        return [true];
     }   

});

function calculateDates()
{
    if( !$('#calendar').val()){
        alert("Please enter a date.");
        document.getElementById('calendar').focus();
        return false;
    }

    var dayThreeAdd = 0;
    var daySixAdd = 0;

    for (var i = 0; i < disabledDates.length; i++) {
        var oneDays = AddWorkingDays($('#calendar').val(),1,true,false);
        var twoDays = AddWorkingDays($('#calendar').val(),2,true,false);
        var threeDays = AddWorkingDays($('#calendar').val(),3,true,false);
        var fourDays = AddWorkingDays($('#calendar').val(),4,true,false);
        var fiveDays = AddWorkingDays($('#calendar').val(),5,true,false);
        var sixDays = AddWorkingDays($('#calendar').val(),6,true,false);

        if (new Date(disabledDates[i]).toString() == oneDays.toString()) {
             dayThreeAdd++;
             daySixAdd++;
        }
        if (new Date(disabledDates[i]).toString() == twoDays.toString()) {             
             dayThreeAdd++;
             daySixAdd++;
        }
        if (new Date(disabledDates[i]).toString() == threeDays.toString()) {             
             dayThreeAdd++;
             daySixAdd++;
        }
        if (new Date(disabledDates[i]).toString() == fourDays.toString()) {
            daySixAdd++;
        }
        if (new Date(disabledDates[i]).toString() == fiveDays.toString()) {
            daySixAdd++;
        }
        if (new Date(disabledDates[i]).toString() == sixDays.toString()) {
            daySixAdd++;
        }

    }

    var threeDays = AddWorkingDays($('#calendar').val(),(3 + dayThreeAdd),true,false);
    var sixDays = AddWorkingDays($('#calendar').val(),(6 + daySixAdd),true,false);

    $('#handDelivered').val((threeDays.getMonth()+1) + '/' + threeDays.getDate() + '/' + (threeDays.getYear()+1900));
    $('#mailed').val((sixDays.getMonth()+1) + '/' + sixDays.getDate() + '/' + (sixDays.getYear()+1900));



}

function AddWorkingDays(datStartDate, lngNumberOfWorkingDays, blnIncSat, blnIncSun) {
    datStartDate = new Date(datStartDate);
    var intWorkingDays = 5;
    var intNonWorkingDays = 2;
    var intStartDay = datStartDate.getDay(); // 0=Sunday ... 6=Saturday
    var intOffset;
    var intModifier = 0;

    if (blnIncSat) { intWorkingDays++; intNonWorkingDays--; }
    if (blnIncSun) { intWorkingDays++; intNonWorkingDays--; }
    var newDate = new Date(datStartDate)
    if (lngNumberOfWorkingDays >= 0) {
        // Moving Forward
        if (!blnIncSat && blnIncSun) {
            intOffset = intStartDay;
        } else {
            intOffset = intStartDay - 1;
        }
        // Special start Saturday rule for 5 day week
        if (intStartDay == 6 && !blnIncSat && !blnIncSun) {
            intOffset -= 6;
            intModifier = 1;
        }
    } else {
        // Moving Backward
        if (blnIncSat && !blnIncSun) {
            intOffset = intStartDay - 6;
        } else {
            intOffset = intStartDay - 5;
        }
        // Special start Sunday rule for 5 day week
        if (intStartDay == 0 && !blnIncSat && !blnIncSun) {
            intOffset++;
            intModifier = 1;
        }
    }
    // ~~ is used to achieve integer division for both positive and negative numbers
    newDate.setTime(datStartDate.getTime() + (new Number((~~((lngNumberOfWorkingDays + intOffset) / intWorkingDays) * intNonWorkingDays) + lngNumberOfWorkingDays + intModifier)*86400000));
    return newDate;
}

Joshua Walcher
  • 506
  • 4
  • 14
0

Simple solution to solve the whole problem; you can just loop through the days to skip weekdays and holidays:

Date.prototype.holidays = {
  // fill in common holidays
  all: [
    '0101', // Jan 01
    '1225' // Dec 25
  ],
  2016: [
    // add year specific holidays
    '0104' // Jan 04 2016
  ],
  2017: [
    // And so on for other years.
  ]
};

Date.prototype.addWorkingDays = function(days) {
  while (days > 0) {
    this.setDate(this.getDate() + 1);
    if (!this.isHoliday()) days--;
  }

  return this;
};

Date.prototype.substractWorkingDays = function(days) {
  while (days > 0) {
    this.setDate(this.getDate() - 1);
    if (!this.isHoliday()) days--;
  }

  return this;
};

Date.prototype.isHoliday = function() {
  function zeroPad(n) {
    n |= 0;
    return (n < 10 ? '0' : '') + n;
  }

  // if weekend return true from here it self;
  if (this.getDay() == 0 || this.getDay() == 6) {
    return true;
  }

  var day = zeroPad(this.getMonth() + 1) + zeroPad(this.getDate());

  // if date is present in the holiday list return true;
  return !!~this.holidays.all.indexOf(day) ||      
    (this.holidays[this.getFullYear()] ?
!!~this.holidays[this.getFullYear()].indexOf(day) : false);
};

// Uasage
var date = new Date('2015-12-31');

date.addWorkingDays(10);
alert(date.toDateString()); // Mon Jan 18 2016

date.substractWorkingDays(10);
alert(date.toDateString()) // Thu Dec 31 2015
Harish Ambady
  • 12,525
  • 4
  • 29
  • 54
0

This only takes weekends into account and not holidays, but it's a start...

function mod(x, y) {
  // https://stackoverflow.com/a/4467559/2173455
  return ((x % y) + y) % y;
}

function calculateDateDiff(date, diff) {
        let returnDate = new Date(date.getTime());
        let daysLeftToAdd = Math.abs(diff);     
        let weekendDays = 0;
        let weekDay = returnDate.getDay();
        while(daysLeftToAdd >= 0) {
          if(weekDay == 0 || weekDay == 6) {
            weekendDays++;
          }
          else {
            daysLeftToAdd--;
          }
          weekDay = mod(diff > 0 ? weekDay + 1 : weekDay - 1, 7);
        }
        returnDate.setDate(diff > 0 ?
            returnDate.getDate() + diff + weekendDays :
          returnDate.getDate() + diff - weekendDays
          );
        return returnDate;
}
Vincent Gagnon
  • 650
  • 2
  • 7
  • 15