Calculate Business Days

Calculate business days

Here's a function from the user comments on the date() function page in the PHP manual. It's an improvement of an earlier function in the comments that adds support for leap years.

Enter the starting and ending dates, along with an array of any holidays that might be in between, and it returns the working days as an integer:

<?php
//The function returns the no. of business days between two dates and it skips the holidays
function getWorkingDays($startDate,$endDate,$holidays){
// do strtotime calculations just once
$endDate = strtotime($endDate);
$startDate = strtotime($startDate);


//The total number of days between the two dates. We compute the no. of seconds and divide it to 60*60*24
//We add one to inlude both dates in the interval.
$days = ($endDate - $startDate) / 86400 + 1;

$no_full_weeks = floor($days / 7);
$no_remaining_days = fmod($days, 7);

//It will return 1 if it's Monday,.. ,7 for Sunday
$the_first_day_of_week = date("N", $startDate);
$the_last_day_of_week = date("N", $endDate);

//---->The two can be equal in leap years when february has 29 days, the equal sign is added here
//In the first case the whole interval is within a week, in the second case the interval falls in two weeks.
if ($the_first_day_of_week <= $the_last_day_of_week) {
if ($the_first_day_of_week <= 6 && 6 <= $the_last_day_of_week) $no_remaining_days--;
if ($the_first_day_of_week <= 7 && 7 <= $the_last_day_of_week) $no_remaining_days--;
}
else {
// (edit by Tokes to fix an edge case where the start day was a Sunday
// and the end day was NOT a Saturday)

// the day of the week for start is later than the day of the week for end
if ($the_first_day_of_week == 7) {
// if the start date is a Sunday, then we definitely subtract 1 day
$no_remaining_days--;

if ($the_last_day_of_week == 6) {
// if the end date is a Saturday, then we subtract another day
$no_remaining_days--;
}
}
else {
// the start date was a Saturday (or earlier), and the end date was (Mon..Fri)
// so we skip an entire weekend and subtract 2 days
$no_remaining_days -= 2;
}
}

//The no. of business days is: (number of weeks between the two dates) * (5 working days) + the remainder
//---->february in none leap years gave a remainder of 0 but still calculated weekends between first and last day, this is one way to fix it
$workingDays = $no_full_weeks * 5;
if ($no_remaining_days > 0 )
{
$workingDays += $no_remaining_days;
}

//We subtract the holidays
foreach($holidays as $holiday){
$time_stamp=strtotime($holiday);
//If the holiday doesn't fall in weekend
if ($startDate <= $time_stamp && $time_stamp <= $endDate && date("N",$time_stamp) != 6 && date("N",$time_stamp) != 7)
$workingDays--;
}

return $workingDays;
}

//Example:

$holidays=array("2008-12-25","2008-12-26","2009-01-01");

echo getWorkingDays("2008-12-22","2009-01-02",$holidays)
// => will return 7
?>

Calculate business days skipping holidays and weekends

Also don't forget when adding the accumulated extra days (being weekends and holidays), those might cover new weekends and holidays, so you have to do this "recursively".

Simplest solution

The simplest solution could start from the initial date, increment it by a day, and check each if it's a skippable (weekend or holiday) day or not. If not, decrement the number of days, and repeat until you added as many as needed.

This is how it could look like:

func addDays(start time.Time, days int) (end time.Time) {
for end = start; days > 0; {
end = end.AddDate(0, 0, 1)
if !skippable(end) {
days--
}
}
return end
}

func skippable(day time.Time) bool {
if wd := day.Weekday(); wd == time.Saturday || wd == time.Sunday {
return true
}
if isHoliday(day) {
return true
}
return false
}

func isHoliday(day time.Time) bool {
return false // TODO
}

Testing it:

d := time.Date(2022, time.April, 14, 0, 0, 0, 0, time.UTC)
fmt.Println(addDays(d, 0))
fmt.Println(addDays(d, 1))
fmt.Println(addDays(d, 10))

Which outputs (try it on the Go Playground):

2022-04-14 00:00:00 +0000 UTC
2022-04-15 00:00:00 +0000 UTC
2022-04-28 00:00:00 +0000 UTC

Faster solution

A faster solution can avoid the loop to step day by day.

Calculating weekend days: Knowing what day the initial date is, and knowing how many days you want to step, we can calculate the number of weekend days in between. E.g. if we have to step 14 days, that's 2 full weeks, that surely includes exactly 4 weekend days. If we have to step a little more, e.g. 16 days, that also includes 2 full weeks (4 weekend days), and optionally 1 or 2 more days which we can easily check.

Calculating holidays: We may use a trick to list the holidays in a sorted slice (sorted by date), so we can easily / quickly find the number of days between 2 dates. We can binary search in a sorted slice for the start and end date of some period, and the number of holidays in a period is the number of elements between these 2 indices. Note: holidays falling on weekends must not be included in this slice (else they would be accounted twice).

Let's see how this implementation looks like:

// holidays is a sorted list of holidays
var holidays = []time.Time{
time.Date(2022, time.April, 15, 0, 0, 0, 0, time.UTC),
}

func addDaysFast(start time.Time, days int) (end time.Time) {
weekendDays := days / 7 * 2 // Full weeks
// Account for weekends if there's fraction week:
for day, fraction := start.AddDate(0, 0, 1), days%7; fraction > 0; day, fraction = day.AddDate(0, 0, 1), fraction-1 {
if wd := day.Weekday(); wd == time.Saturday || wd == time.Sunday {
weekendDays++
}
}

end = start.AddDate(0, 0, days+weekendDays)

first := sort.Search(len(holidays), func(i int) bool {
return !holidays[i].Before(start)
})
last := sort.Search(len(holidays), func(i int) bool {
return !holidays[i].Before(end)
})

// There are last - first holidays in the range [start..end]
numHolidays := last - first
if last < len(holidays) && holidays[last].Equal(end) {
numHolidays++ // end is exactly a holiday
}

if numHolidays == 0 {
return end // We're done
}

// We have to add numHolidays, using the same "rules" above:
return addDaysFast(end, numHolidays)
}

Testing it:

d := time.Date(2022, time.April, 14, 0, 0, 0, 0, time.UTC)
fmt.Println(addDaysFast(d, 0))
fmt.Println(addDaysFast(d, 1))
fmt.Println(addDaysFast(d, 10))

Output (try it on the Go Playground):

2022-04-14 00:00:00 +0000 UTC
2022-04-18 00:00:00 +0000 UTC
2022-04-29 00:00:00 +0000 UTC

Improving addDaysFast()

There are still ways to improve addDaysFast():

  • the initial loop to check for weekend days in the fraction week could be substituted with an arithmetic calculation (see example)
  • the recursion could be substituted with an iterative solution
  • an alternative solution could list weekend days as holidays, so the first part to calculate weekend days could be eliminated (duplicates must not be included)

Calculate the number of business days between two dates?

I've had such a task before and I've got the solution.
I would avoid enumerating all days in between when it's avoidable, which is the case here. I don't even mention creating a bunch of DateTime instances, as I saw in one of the answers above. This is really waste of processing power. Especially in the real world situation, when you have to examine time intervals of several months.
See my code, with comments, below.

    /// <summary>
/// Calculates number of business days, taking into account:
/// - weekends (Saturdays and Sundays)
/// - bank holidays in the middle of the week
/// </summary>
/// <param name="firstDay">First day in the time interval</param>
/// <param name="lastDay">Last day in the time interval</param>
/// <param name="bankHolidays">List of bank holidays excluding weekends</param>
/// <returns>Number of business days during the 'span'</returns>
public static int BusinessDaysUntil(this DateTime firstDay, DateTime lastDay, params DateTime[] bankHolidays)
{
firstDay = firstDay.Date;
lastDay = lastDay.Date;
if (firstDay > lastDay)
throw new ArgumentException("Incorrect last day " + lastDay);

TimeSpan span = lastDay - firstDay;
int businessDays = span.Days + 1;
int fullWeekCount = businessDays / 7;
// find out if there are weekends during the time exceedng the full weeks
if (businessDays > fullWeekCount*7)
{
// we are here to find out if there is a 1-day or 2-days weekend
// in the time interval remaining after subtracting the complete weeks
int firstDayOfWeek = (int) firstDay.DayOfWeek;
int lastDayOfWeek = (int) lastDay.DayOfWeek;
if (lastDayOfWeek < firstDayOfWeek)
lastDayOfWeek += 7;
if (firstDayOfWeek <= 6)
{
if (lastDayOfWeek >= 7)// Both Saturday and Sunday are in the remaining time interval
businessDays -= 2;
else if (lastDayOfWeek >= 6)// Only Saturday is in the remaining time interval
businessDays -= 1;
}
else if (firstDayOfWeek <= 7 && lastDayOfWeek >= 7)// Only Sunday is in the remaining time interval
businessDays -= 1;
}

// subtract the weekends during the full weeks in the interval
businessDays -= fullWeekCount + fullWeekCount;

// subtract the number of bank holidays during the time interval
foreach (DateTime bankHoliday in bankHolidays)
{
DateTime bh = bankHoliday.Date;
if (firstDay <= bh && bh <= lastDay)
--businessDays;
}

return businessDays;
}

Edit by Slauma, August 2011

Great answer! There is little bug though. I take the freedom to edit this answer since the answerer is absent since 2009.

The code above assumes that DayOfWeek.Sunday has the value 7 which is not the case. The value is actually 0. It leads to a wrong calculation if for example firstDay and lastDay are both the same Sunday. The method returns 1 in this case but it should be 0.

Easiest fix for this bug: Replace in the code above the lines where firstDayOfWeek and lastDayOfWeek are declared by the following:

int firstDayOfWeek = firstDay.DayOfWeek == DayOfWeek.Sunday 
? 7 : (int)firstDay.DayOfWeek;
int lastDayOfWeek = lastDay.DayOfWeek == DayOfWeek.Sunday
? 7 : (int)lastDay.DayOfWeek;

Now the result is:

  • Friday to Friday -> 1
  • Saturday to Saturday -> 0
  • Sunday to Sunday -> 0
  • Friday to Saturday -> 1
  • Friday to Sunday -> 1
  • Friday to Monday -> 2
  • Saturday to Monday -> 1
  • Sunday to Monday -> 1
  • Monday to Monday -> 1

Method to calculate business days

Here is what Lee Painton's excellent (and up-voted) answer would look like using this free, open-source C++11/14 date library which is built on top of <chrono>:

#include "date.h"
#include <iostream>

date::year_month_day
get_end_job_date(date::year_month_day start, date::days length)
{
using namespace date;
--length;
auto w = weeks{length / days{5}};
length %= 5;
auto end = sys_days{start} + w + length;
auto wd = weekday{end};
if (wd == sat)
end += days{2};
else if (wd == sun)
end += days{1};
return end;
}

You could exercise it like this:

int
main()
{
using namespace date::literals;
std::cout << get_end_job_date(12_d/jul/2016, date::days{8}) << '\n';
}

Which outputs:

2016-07-21

This simplistic calculator has a precondition that start is not on a weekend. If that is not a desirable precondition then you could detect that prior to the computation and increment start internally by a day or two.

The date library takes care of things like the relationship between days and weeks, and how to add days to a date. It is based on very efficient (non-iterative) algorithms shown and described here.

calculate number of 'Real' days between two dates when given a number of business days as input

For business days over 5 days you can take the business day, divide by 5 and make at an integer (floor), multiply by 7, and add the modulus of business days and 5. E.g for 26/5 => 5 (weeks) * 7 = 35 + 1 (modulus of 26 and 5) = 36

For under 5 days you will need some logic to check the day of the week for your current and to see if it does cross a weekend or not, and add 2 if it does.

Calculating Business Days

Updated your function a bit so holidays can be added...

Nweekdays <- function(a, b, holidays, weekend) { 
possible_days <- seq(a, b, "days")
# Count all days that are not weekend and
# are not holidays
sum(!weekdays(possible_days) %in% weekend & !possible_days %in% holidays)
}

weekend <- c("Saturday", "Sunday")
holidays <- as.Date(c("2017-12-31", "2017-12-24", "2017-07-04"))
Nweekdays(as.Date("2017-08-01"), as.Date("2017-12-31"), holidays, weekend)
[1] 109

While the Gregorian calendar is pretty global, the definition of weekend and holidays is dependent on country, region, etc.

Calculate business days and weekend days between 2 NSDates

First, if you are using Swift 2 you should make NSDate conform to Comparable protocol:

extension NSDate: Comparable { }

public func <(lhs: NSDate, rhs: NSDate) -> Bool {
return lhs.compare(rhs) == .OrderedAscending
}

Second, You can use Calendar isDateInWeekend to check if any date it is a weekend day or not, and you can use dateByAddingUnit to get add a day to the start date until the end date:

Create those extensions to help you:

edit/update: Swift 4

extension Calendar {
static let iso8601 = Calendar(identifier: .iso8601)
}


extension Date {
var isDateInWeekend: Bool {
return Calendar.iso8601.isDateInWeekend(self)
}
var tomorrow: Date {
return Calendar.iso8601.date(byAdding: .day, value: 1, to: noon)!
}
var noon: Date {
return Calendar.iso8601.date(bySettingHour: 12, minute: 0, second: 0, of: self)!
}
}

and a method to count the days:

func coutDays(from start: Date, to end: Date) -> (weekendDays: Int, workingDays: Int) {
guard start < end else { return (0,0) }
var weekendDays = 0
var workingDays = 0
var date = start.noon
repeat {
if date.isDateInWeekend {
weekendDays += 1
} else {
workingDays += 1
}
date = date.tomorrow
} while date < end
return (weekendDays, workingDays)
}

Testing:

import UIKit

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()

let start = DateComponents(calendar: .iso8601, year: 2016).date! // "Jan 1, 2016, 12:00 AM"
let end = DateComponents(calendar: .iso8601, year: 2017).date! // "Jan 1, 2017, 12:00 AM"
print(coutDays(from: start, to: end)) // 105, 261
}
}


Related Topics



Leave a reply



Submit