20
\$\begingroup\$

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?

\$\endgroup\$
2
  • \$\begingroup\$ 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... \$\endgroup\$
    – mika
    Apr 10, 2015 at 14:18
  • \$\begingroup\$ A detail blog:goo.gl/cV6kjl \$\endgroup\$ Jan 14, 2016 at 8:56

5 Answers 5

10
\$\begingroup\$

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

\$\endgroup\$
3
  • \$\begingroup\$ 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. \$\endgroup\$ May 29, 2014 at 8:02
  • \$\begingroup\$ Would be cool to have this class in composer library. That would help a lot and could be enhanced. \$\endgroup\$
    – wormhit
    Jul 24, 2014 at 10:32
  • \$\begingroup\$ @wormhit Feel free to take it to GitHub under your name and make a composer library out of it. You have my full permission. \$\endgroup\$ Jul 24, 2014 at 10:33
2
\$\begingroup\$

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);
\$\endgroup\$
1
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

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.

\$\endgroup\$
1
\$\begingroup\$

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;
\$\endgroup\$
1
  • \$\begingroup\$ 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. \$\endgroup\$ Oct 3, 2015 at 10:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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