0
\$\begingroup\$

I realize there is already a review about this here, but it's using strings and stuff rather than date objects.

I'm trying to make a simple function to calculate days from a point in time and return object(s). The days have to follow an ignore list (like weekends and holidays) to calculate a range of days, then consider itself landing on any of those ignore days/dates in the end range, then compensate to skoot the final range if needed.

After trial and error with different methods of doing this (++ iterations mostly), I settled on this function. It seems too heavy though considering (mostly) all it's doing is skipping certain ISO day numbers to do some adds.

Are there some PHP date hidden secrets that I am missing? Or a way to boil this down? It seems like a feature such as this (skipping day codes) would be baked into PHP DatePeriod() or DateInterval() but sadly it's not. How would you approach this, pref without counters/strtotime stuff, and pref without extra classes or libs?

<?php

function getWorkingDaysFrom($from, $amtOfDays) {
    $globalDays = 0; // a global addition to $amtOfDays (such as a minimum shipping time of 3 days across all objects)
    $amtOfDays = $amtOfDays + $globalDays; // set base amount of days for initial range before calculations applied
    $ignore = array('6', '7'); // days of the week to ignore (weekends are 6 & 7) in ISO-8061
    $holidays = array('*-06-13', '*-11-23', '*-12-25', '*-01-01', '*-02-29'); // holiday dates (holidays are ignored if they fall into ignore list)
    $oneDay = new DateInterval('P1D');
    $from = new DateTime($from);
    $to = new DateTime();
    $to->add(new DateInterval('P' . $amtOfDays . 'D'));

    foreach(new DatePeriod($from, $oneDay, $to) as $day) {  
        if (in_array($day->format('N'), $ignore)) {
            $to->modify('+1 day');
        } elseif (in_array($day->format('*-m-d'), $holidays)) {
            $to->modify('+1 day');
        }
        while (in_array($to->format('N'), $ignore) || (!in_array($to->format('N'), $ignore) && in_array($to->format('*-m-d'), $holidays))) {
            $to->modify('+1 day');
        }
    }
    return $to;
}

getWorkingDaysFrom('now', 20);

?>

Thanks for your time and insights.

\$\endgroup\$
  • \$\begingroup\$ Oh a [minor] -1, understandable. Should be using cached $oneDay with $to->add(); rather than $to->modify('+1 day'); in this paste right :) \$\endgroup\$ – dhaupin Jun 5 '17 at 23:08
2
\$\begingroup\$

First of all, you can get away from having to deal with weekends altogether by using:

$to->modify('+1 weekday');

I think using the above and due to the nature of your function just needing to count X number of "valid" days from given day, that you are kind of wasting your time using DatePeriod and DateInterval (both classes that usually I am a big proponent for their use).

Here this can be as simple as the following code:

function addWorkingDays(DateTime $from, $dayCount)
{
    $datetime = clone $from;
    while($dayCount > 0) {
        $datetime->modify('+1 weekday');
        if(!isHoliday($datetime)) {
            $dayCount--;
        }
    }
    return $datetime;
}

function isHoliday(DateTime $datetime)
{
    // holiday calculation logic here
    // return boolean
}

Note a few things here:

  • Here I require the caller to pass a valid DateTime object for $from. Your current function just kind of assumes $from is valid value from which a DateTime object can be instantiated. I would go ahead and remove all uncertainty from the function and require a DateTime object to operate against. In your usage example, it would be just as easy to pass new DateTime('now') to the function.
  • I have defined a separate function isHoliday() where one could define their holiday determination logic. This also gives possible re-use outside the context of this function. I wasn't quite sold on your holiday logic. I would think it would be rather trivial to actually get better data structure here to look up against (why the *- part? that is not necessary to simply compare month and day) Should you be using holiday dates as key in array instead of value to allow for direct lookup, vs. iterating array using in_array()? I did not specify an implementation of this function, as really, you might want to build a more robust listing of holidays well out into the future if you have any variable date holidays.
  • I renamed the function to perhaps better indicate what it is doing - adding working days to a given date, not getting the number of working days in some interval.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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