4

I am trying to get the next 3 days using php but if the date is a weekend or a defined holiday I would like to skip that date.

For example if the date is Monday, December 23, 2013 My holiday dates are array('2013-12-24', '2013-12-25'); The script would return

Monday, December 23, 2013
Thursday, December 26, 2013
Friday, December 27, 2013
Monday, December 30, 2013

Her is my current code:

$arr_date = explode('-', '2013-12-23');
$counter = 0;
for ($iLoop = 0; $iLoop < 4; $iLoop++)
{
    $dayOfTheWeek = date("N", mktime(0, 0, 0, $arr_date[1], $arr_date[2]+$counter, $arr_date[0]));
    if ($dayOfTheWeek == 6) { $counter += 2; }
    $date = date("Y-m-d", mktime(0, 0, 0, $arr_date[1], $arr_date[2]+$counter, $arr_date[0]));
    echo $date;
    $counter++;
}

The issue I am having is I am not sure to how to even go about excluding holidays dates.

Robert E. McIntosh
  • 5,557
  • 4
  • 26
  • 35

2 Answers2

5

Use a DateTime to recurse over the next days and have a string comparison check to see if the next generated day belongs in the holiday or weekend arrays using in_array

$holidays = array('12-24', '12-25');
$weekend = array('Sun','Sat');
$date = new DateTime('2013-12-23');
$nextDay = clone $date;
$i = 0; // We have 0 future dates to start with
$nextDates = array(); // Empty array to hold the next 3 dates
while ($i < 3)
{
    $nextDay->add(new DateInterval('P1D')); // Add 1 day
    if (in_array($nextDay->format('m-d'), $holidays)) continue; // Don't include year to ensure the check is year independent
    // Note that you may need to do more complicated things for special holidays that don't use specific dates like "the last Friday of this month"
    if (in_array($nextDay->format('D'), $weekend)) continue;
    // These next lines will only execute if continue isn't called for this iteration
    $nextDates[] = $nextDay->format('Y-m-d');
    $i++;
}

Using the suggestion of isset() for O(1) instead of O(n):

$holidays = array('12-24' => '', '12-25' => '');
$weekend = array('Sun' => '','Sat' => '');
$date = new DateTime('2013-12-23');
$dayInterval = new DateInterval('P1D');
$nextDay = clone $date;
$i = 0;
$nextDates = array();
while ($i < 3)
{
    $nextDay->add($dayInterval);
    if (isset($holidays[$nextDay->format('m-d')])) continue;
    if (isset($weekend[$nextDay->format('D')])) continue;
    $nextDates[] = $nextDay->format('Y-m-d');
    $i++;
}
sjagr
  • 15,983
  • 5
  • 40
  • 67
  • This works perfectly, was even really easy to modify to work with my database! Thank you. – Robert E. McIntosh Jan 15 '14 at 16:52
  • @RobertE.McIntosh no problem - I've added some commenting. You may have to also find an elegant way to check on holidays with non-specific dates like [Victoria Day](http://en.wikipedia.org/wiki/Victoria_Day) (if you were Canadian, for example) – sjagr Jan 15 '14 at 17:03
  • isn't that as simple as saying `if (in_array($nextDay->format('Y-m-d'), $holidays))`? – Robert E. McIntosh Jan 15 '14 at 17:04
  • @RobertE.McIntosh You could do that, but then you're manually adding dates to `$holidays` every year (this is not elegant.) Notice how I left the year out so it would work every year without any intervention? Good practice would be to do the same thing for these other kind of dates. Not necessary, but a "nice to have" – sjagr Jan 15 '14 at 17:07
  • You are correct. But the application relies on the client enter off days already. I am not to terrible worried about it, but I do understand where you are coming from, maybe make a third check for off days and keep holidays alone. – Robert E. McIntosh Jan 15 '14 at 17:11
  • I would suggest using `isset` with a pseudo-map instead of `in_array`. The former is O(1) and the latter is O(n). – NobleUplift Jan 15 '14 at 17:18
  • @NobleUplift I accepted your edit but kept my original code and appended yours to demonstrate the solution easily for future readers. Micro optimization is trivial when it's not the purpose of the question being asked. – sjagr Jan 15 '14 at 17:29
  • The code doesn't work if the start date ($date) is a holiday, though. – The Jakester Jan 16 '15 at 18:13
  • @TheJakester The definition of the question is the **next** _three working dates_. The current date is not applicable and not considered even if it wasn't a holiday. For example if it's Dec 26th (Boxing Day) and I wanted the next 3 working dates, the 27th, 28th, and 29th would be valid return responses. If the starting date was Dec 25th, I would get the 27th, 28th, and the 29th still. (All assuming the 27th, 28th, and 29th are not weekend dates.) – sjagr Jan 16 '15 at 22:46
  • You're right. I was trying to adapt the code to calculate the end date from a start date and a length of working days, but your answer totally works for the original question. – The Jakester Jan 17 '15 at 18:13
  • In 2020, the solution does **NOT** work, as is. It throws the following error: **Warning: DateTime::add() expects parameter 1 to be DateInterval, string given in...**. To fix this, replace the line `$nextDay->add('P1D');` with `$nextDay->add(new DateInterval('P1D'));` – Devner Jan 07 '20 at 10:40
  • @Devner Thanks, I've edited the answer based on your comment and [this documentation](https://www.php.net/manual/en/datetime.add.php) - the string parameter probably broke on PHP 7. – sjagr Jan 07 '20 at 20:01
  • @sjagr Thanks for the edit, but seems like you missed making the same update for the 2nd code block. Please update it when you get a chance. Thanks. – Devner Jan 07 '20 at 22:41
  • @sjagr I performed some tests using your function and looks like it does **NOT** consider Leap Year. For example, the current year i.e. 2020 is a leap year and that causes a difference in calculating the business days. If you look at the calendar, Feb 29, 2020 is a Saturday!!! It appears that Feb 29, 2020 is considered as a working day whereas the actual working day is on the following Monday. So here, it needs to add 2 more days to the total, especially because Feb 29, 2020 is a Saturday. So if your function could be made to consider even a leap year, then I think it would be perfect! – Devner Jan 08 '20 at 21:22
5

Here is a simple example of how to get number of working days between 2 dates; which you can simply modify to fit your needs:

function number_of_working_dates($from, $days) {
    $workingDays = [1, 2, 3, 4, 5]; # date format = N (1 = Monday, ...)
    $holidayDays = ['*-12-25', '*-01-01', '2013-12-24', '2013-12-25']; # variable and fixed holidays

    $from = new DateTime($from);
    $dates = [];
    $dates[] = $from->format('Y-m-d');
    while ($days) {
        $from->modify('+1 day');

        if (!in_array($from->format('N'), $workingDays)) continue;
        if (in_array($from->format('Y-m-d'), $holidayDays)) continue;
        if (in_array($from->format('*-m-d'), $holidayDays)) continue;

        $dates[] = $from->format('Y-m-d');
        $days--;
    }
    return $dates;
}

print_r( number_of_working_dates('2013-12-23', 3) );

demo

Community
  • 1
  • 1
Glavić
  • 42,781
  • 13
  • 77
  • 107
  • This works perfectly, thank you very much. I want to generate `$holidayDays` variable from database like that `$qry_hk = "SELECT holiday FROM dim_holiday_dates"; $result_hk = mysqli_query($db,$qry_hk); $holidayDays = [while ($data_hk = mysqli_fetch_array($result_hk)) {echo '"'.$data_hk['holiday'].'",';}]; ` but error `Parse error: syntax error, unexpected 'while' (T_WHILE), expecting ']' in` help me – flyingbird013 Dec 21 '19 at 08:55