I've written a program for calculating the future date after certain number of business days.

function add_business_days($startdate,$businessdays,$holidays,$dateformat){  
   $i=1;  
   $dayx = strtotime($startdate);  

  while($i < $businessdays){  
     $day = date('N',$dayx);  
     $date = date('Y-m-d',$dayx);  
     if($day < 6 && !in_array($date,$holidays))$i++;  
     $dayx = strtotime($date.' +1 day');  
  }  
  return date($dateformat,$dayx);
}

But the problem with this function is that it doesn't check the last date of business day for Saturday and Sunday. Also, the last day could be the holiday. Can anyone provide me a better solution for this?


I've made a solution to this problem as

function add_business_days($startdate,$businessdays,$holidays,$dateformat){  
   $i=1;  
   $dayx = strtotime($startdate);  

  while($i <= $businessdays){  
     $day = date('N',$dayx);  
     $date = date('Y-m-d',$dayx);  
     if($day < 6 && !in_array($date,$holidays))$i++;  
     $dayx = strtotime($date.' +1 day');  
  }  
  return date($dateformat,strtotime(date('Y-m-d', $dayx) . ' -1 day'));
}

But it doesn't seems good.


Usage Example

I'm using this method for calculating the shipment date on which a product will be delivered to the customer.

$startdate = "2014-05-28";  //Order placing date  
$businessdays = 7; //number of days for delivery  
$holidays = array('2014-05-30','2014-06-03');  //array of dates having holidays. excluding business days.  
$dateformat = 'd-m-Y';  //output date format for displaying.
$delivery = add_business_days($startdate, $businessdays, $holidays, $dateformat);

The $delivery is supposed to have date 09-06-2014 but the first code returns 07-06-2014 which doesn't check for the last date in loop for weekends (Saturday or Sunday). I've made the solution in the second program by execution the loop one more time and then return the previous date. But this approach doesn't seem good. Any suggestions on not going over and returning the previous date?

  • Oh, I believe the DateTime::modify function does the job straight away: $startdate = new \DateTime("2014-05-28"); $my_date->modify("+ 7 weekday"); Ok, we still have to deal with the holidays, but we are halfway through... – mika Apr 10 '15 at 14:18
  • A detail blog:goo.gl/cV6kjl – Suresh Kamrushi Jan 14 '16 at 8:56

I propose a class, BusinessDayCalculator, which would handle all this logic in one place. Here's an example:

<?php

class BusinessDaysCalculator {

    const MONDAY    = 1;
    const TUESDAY   = 2;
    const WEDNESDAY = 3;
    const THURSDAY  = 4;
    const FRIDAY    = 5;
    const SATURDAY  = 6;
    const SUNDAY    = 7;

    /**
     * @param DateTime   $startDate       Date to start calculations from
     * @param DateTime[] $holidays        Array of holidays, holidays are no conisdered business days.
     * @param int[]      $nonBusinessDays Array of days of the week which are not business days.
     */
    public function __construct(DateTime $startDate, array $holidays, array $nonBusinessDays) {
        $this->date = $startDate;
        $this->holidays = $holidays;
        $this->nonBusinessDays = $nonBusinessDays;
    }

    public function addBusinessDays($howManyDays) {
        $i = 0;
        while ($i < $howManyDays) {
            $this->date->modify("+1 day");
            if ($this->isBusinessDay($this->date)) {
                $i++;
            }
        }
    }

    public function getDate() {
        return $this->date;
    }

    private function isBusinessDay(DateTime $date) {
        if (in_array((int)$date->format('N'), $this->nonBusinessDays)) {
            return false; //Date is a nonBusinessDay.
        }
        foreach ($this->holidays as $day) {
            if ($date->format('Y-m-d') == $day->format('Y-m-d')) {
                return false; //Date is a holiday.
            }
        }
        return true; //Date is a business day.
    }
}

$calculator = new BusinessDaysCalculator(
    new DateTime(), // Today
    [new DateTime("2014-06-01"), new DateTime("2014-06-02")],
    [BusinessDaysCalculator::SATURDAY, BusinessDaysCalculator::FRIDAY]
);

$calculator->addBusinessDays(3); // Add three business days 

var_dump($calculator->getDate());

In the above example the nonBusinessDays of the week are SATURDAY and FRIDAY, and 1st and 2nd of June are holidays. In that case, 3 business days from now is June 5th.

This can probably be improved further, let your imagination run wild :P

  • thanks for your time to reply. it works fine. but it doesn't start counting dates from today. also could there be an option for counting today or not. because there is a need of cut-off time to place order. If an order placed before that time then today's date will be calculated else it will count from tomorrow. – Mohammad Faisal May 29 '14 at 7:47
  • It starts counting days from whichever day you tell it to. The good thing about a DateTime object is that it includes Time, so you can check that and see if the time of the day is before a certain point. Try adding it yourself. – Madara Uchiha May 29 '14 at 8:02
  • Would be cool to have this class in composer library. That would help a lot and could be enhanced. – wormhit Jul 24 '14 at 10:32
  • @wormhit Feel free to take it to GitHub under your name and make a composer library out of it. You have my full permission. – Madara Uchiha Jul 24 '14 at 10:33
  • nice Article :) – amit_game Feb 16 '16 at 8:47

In your while loop, change the line:

$dayx = strtotime($date.' +1 day');

to:

$dayx = strtotime($dayx.' +1 day');

I found that the above change worked in an example where I had to work out the date where date = current date + business days:

function addBusinessDays($startDate, $businessDays, $holidays)
{
    $date = strtotime($startDate);
    $i = 0;

    while($i < $businessDays)
    {
        //get number of week day (1-7)
        $day = date('N',$date);
        //get just Y-m-d date
        $dateYmd = date("Y-m-d",$date);

        if($day < 6 && !in_array($dateYmd, $holidays)){
            $i++;
        }       
        $date = strtotime($dateYmd . ' +1 day');
    }       

    return date('Y-m-d H:i:s',$date);

}

Calling the method:

addBusinessDays(date("Y-m-d H:i:s"), 5, $holidayArray);

I modified your second code, and here's how it looks like now:

function add_business_days($startdate,$businessdays,$holidays,$dateformat){  
    $i = 1;
    $date = date('Y-m-d',strtotime($startdate. ' -1 day'));

    while($i <= $businessdays){  
        $date = date('Y-m-d',strtotime($date. ' +1 Weekday'));
        if(!in_array($date,$holidays))$i++;
    }  
    return date($dateformat,strtotime($date));
}

Before it enters the while loop, we start at the previous day of the $startdate. This allows us to work on weekdays alone, since the very first line in the loop will always get the next weekday. The only comparison we need in the loop is if the weekdays fall under the given holidays or not.

I don't like the while loop either: when you'll have to add, maybe 60 days, is going to take a while, so I propose you take the problem form a different angle. Instead of working day per day using the while loop, calculate the total of days you have to add, according to the number of business days.

A business week is five days, a normal week is 7, so, let transform the number of business days in normal weeks and days dividing by 5 and calculating the rest, and then multiply by 7 and add the rest days. Of course, you have to check if you finish on a weekend day, so, add days if you finish on a weekend day (You see this in add_business_days_no_holidays).

Lately, you should check if there are any holidays between the last and the first date. As, at the beginning, you don't know what the last and first days are, you first make the calculation between dates, then check if you have to add more days, and pass the add_business_days_no_holidays function.

Code and Tests Below:

<?php
function add_business_days( $start_date, $business_days, $holidays, $date_format ) {
    $first_end_date = add_business_days_no_holidays( $start_date, $business_days, $date_format );

    // Now, what about holidays?
    $add_more_days = 0;
    foreach( $holidays as $holiday ) {
        if ( ( strtotime( $start_date )     <= strtotime( $holiday ) )
          && ( strtotime( $first_end_date ) >= strtotime( $holiday ) ) ) {
            $add_more_days++;
        }
    }

    $end_date   = $first_end_date;
    if ( $add_more_days != 0 ) {
        $end_date   = add_business_days_no_holidays( $first_end_date, $add_more_days, $date_format );
    }

    return $end_date;
}

function add_business_days_no_holidays( $start_date, $business_days, $date_format ) {
    $ts_start_date  = strtotime( $start_date );

    $weeks  = floor( $business_days / 5 );
    $days1  = $business_days % 5;

    // Now, the end day is at a weekend?
    $init_day   = date( 'N', $ts_start_date );
    $last_day   = $init_day + $days1;
    $days2  = 0;
    if ( $last_day > 5 ) {
        $days2  = 8 - $last_day;
    }

    $total_days = ( $weeks * 7 ) + ( $days1 + $days2 );

    return date( 'Y-m-d', strtotime( '+'.$total_days.' days', $ts_start_date ) ) ;
}

$date_format = 'Y-m-d';

// This day was Monday
$start_date = '2014-05-05';

$days = array( 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 );
echo '</h1>Test "No Holidays" day, Start date is ' . $start_date . '</h1>';
foreach ( $days as $b_day ) {
    $final_date = add_business_days_no_holidays( $start_date, $b_day, $date_format );
    echo '<p>Adding ' . $b_day . ' business days, final date: ' . $final_date . '</p>';
}

$holidays = array( '2014-04-01', '2014-05-25' );
echo '</h1>Test "Holidays" day, holidays out. Start date is ' . $start_date . '</h1>';
foreach ( $days as $b_day ) {
    $final_date = add_business_days( $start_date, $b_day, $holidays, $date_format );
    echo '<p>Adding ' . $b_day . ' business days, final date: ' . $final_date . '</p>';
}

$holidays = array( '2014-05-08', '2014-05-10', '2014-05-13' );
echo '</h1>Test "Holidays" day, 2 holidays days to add, 1 to skip. Start date is ' . $start_date . '</h1>';
foreach ( $days as $b_day ) {
    $final_date = add_business_days( $start_date, $b_day, $holidays, $date_format );
    echo '<p>Adding ' . $b_day . ' business days, final date: ' . $final_date . '</p>';
}

The drag of the code is if you have a lot of holidays dates, but I suppose that you get them from a database, so you could check first what is the end date without holidays using add_business_days_no_holidays_function, retrieve from DDBB the holidays between the two dates, and then use add_business_days_function, passing the start_date and end_date, and avoiding the first add_business_days_no_holidays_function, as you passed it before.

Works well but there is an error in calculations If $init_day + $days1 is higer than 7

if ($last_day > 7):
$last_day = $last_day-7;
endif;

before

if ($last_day > 7):
        $last_day = $last_day-7;
    endif;
  • Welcome to Code Review! You have presented a fix, but haven't reviewed the code. Please explain your reasoning (how your fix works and how it improves upon the original) so that the author can learn from your thought process. – SuperBiasedMan Oct 3 '15 at 10:55

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.