简体   繁体   中英

Find three previous working days from a given date

I need to find three previous working days from a given date, omitting weekends and holidays. This isn't a hard task in itself, but it seems that the way I was going to do it would be overly complicated, so I thought I'd ask for your opinion first.

To make things more interesting, let's make this a contest. I'm offering 300 as a bounty to whoever comes up with the shortest, cleanest solution that adheres to this specification:

  • Write a function that returns three previous working days from a given date
  • Working day is defined as any day that is not saturday or sunday and isn't an holiday
  • The function knows the holidays for the year of the given date and can take these into account
  • The function accepts one parameter, the date, in Ymd format
  • The function returns an array with three dates in Ymd format, sorted from oldest to newest.

Extra:

  • The function can find also the next three working days in addition to the previous three

An example of the holidays array:

$holidays = array(
    '2010-01-01',
    '2010-01-06',
    '2010-04-02',
    '2010-04-04',
    '2010-04-05',
    '2010-05-01',
    '2010-05-13',
    '2010-05-23',
    '2010-06-26',
    '2010-11-06',
    '2010-12-06',
    '2010-12-25',
    '2010-12-26'
);

Note that in the real scenario, the holidays aren't hardcoded but come from get_holidays($year) function. You can include / use that in your answer if you wish.

As I'm offering a bounty, that means there will be at least three days before I can mark an answer as accepted (2 days to add a bounty, 1 day until I can accept).


Note

If you use a fixed day length such as 86400 seconds to jump from day to another, you'll run into problems with daylight savings time. Use strtotime('-1 day', $timestamp) instead.

An example of this problem:

http://codepad.org/uSYiIu5w


Final solution

Here's the final solution I ended up using, adapted from Keith Minkler's idea of using strtotime 's last weekday . Detects the direction from the passed count, if negative, searches backwards, and forwards on positive:

function working_days($date, $count) {

    $working_days = array();
    $direction    = $count < 0 ? 'last' : 'next';
    $holidays     = get_holidays(date("Y", strtotime($date)));

    while(count($working_days) < abs($count)) {
        $date = date("Y-m-d", strtotime("$direction weekday", strtotime($date)));
        if(!in_array($date, $holidays)) {
            $working_days[] = $date;
        }
    }

    sort($working_days);
    return $working_days;
}

This should do the trick:

    // Start Date must be in "Y-m-d" Format
    function LastThreeWorkdays($start_date) {
        $current_date = strtotime($start_date);
        $workdays = array();
        $holidays = get_holidays('2010');

        while (count($workdays) < 3) {
            $current_date = strtotime('-1 day', $current_date);

            if (in_array(date('Y-m-d', $current_date), $holidays)) {    
                // Public Holiday, Ignore.
                continue;
            }

            if (date('N', $current_date) < 6) {
                // Weekday. Add to Array.
                $workdays[] = date('Y-m-d', $current_date);
            }
        }

        return array_reverse($workdays);
    }

I've hard-coded in the get_holidays() function, but I'm sure you'll get the idea and tweak it to suit. The rest is all working code.

You can use expressions like "last weekday" or "next thursday" in strtotime, such as this:

function last_working_days($date, $backwards = true)
{
    $holidays = get_holidays(date("Y", strtotime($date)));

    $working_days = array();

    do
    {
        $direction = $backwards ? 'last' : 'next';
        $date = date("Y-m-d", strtotime("$direction weekday", strtotime($date)));
        if (!in_array($date, $holidays))
        {
            $working_days[] = $date;
        }
    }
    while (count($working_days) < 3);

    return $working_days;
}

Pass true as the second argument to go forward in time instead of backwards. I've also edited the function to allow for more than three days if you should want to in the future.

function last_workingdays($date, $forward = false, $numberofdays = 3) {
        $time = strtotime($date);
        $holidays = get_holidays();
        $found = array();
        while(count($found) < $numberofdays) {
                $time -= 86400 * ($forward?-1:1);
                $new = date('Y-m-d', $time);
                $weekday = date('w', $time);
                if($weekday == 0 || $weekday == 6 || in_array($new, $holidays)) {
                        continue;
                }
                $found[] = $new;
        }
        if(!$forward) {
                $found = array_reverse($found);
        }
        return $found;
}

Here is my take on it using PHP's DateTime class. Regarding the holidays, it takes into account that you may start in one year and end in another.

function get_workdays($date, $num = 3, $next = false)
{
    $date = DateTime::createFromFormat('Y-m-d', $date);
    $interval = new DateInterval('P1D');
    $holidays = array();

    $res = array();
    while (count($res) < $num) {
        $date->{$next ? 'add' : 'sub'}($interval);

        $year = (int) $date->format('Y');
        $formatted = $date->format('Y-m-d');

        if (!isset($holidays[$year]))
            $holidays[$year] = get_holidays($year);

        if ($date->format('N') <= 5 && !in_array($formatted, $holidays[$year]))
            $res[] = $formatted;
    }
    return $next ? $res : array_reverse($res);
}

Here's my go at it:

function business_days($date) {
    $out = array();
    $day = 60*60*24;

    //three back
    $count = 0;
    $prev = strtotime($date);
    while ($count < 3) {
        $prev -= $day;
        $info = getdate($prev);
        $holidays = get_holidays($info['year']);
        if ($info['wday'] == 0 || $info['wday'] == 6 || in_array($date,$holidays))
                continue;
        else {
            $out[] = date('Y-m-d',$prev);
            $count++;
        }
    }

    $count = 0;
    $next = strtotime($date);
    while ($count < 3) {
        $next += $day;
        $info = getdate($next);
        $holidays = get_holidays($info['year']);
        if ($info['wday']==0 || $info['wday']==6 || in_array($date,$holidays))
                continue;
        else {
            $out[] = date('Y-m-d',$next);
            $count++;
        }
    }

    sort($out);

    return $out;
}

Edit:

Changed the 86400 to -1 day although I don't fully understand if this was really an issue.

Made some modifications to the original functions but it's pretty much the same.

// -----------------------
// Previous 3 working days # this is almost the same that someone already posted
function getWorkingDays($date){
    $workdays = array();
    $holidays = getHolidays();
    $date     = strtotime($date);

    while(count($workdays) < 3){
        $date = strtotime("-1 day", $date);

        if(date('N',$date) < 6 && !in_array(date('Y-m-d',$date),$holidays))
            $workdays[] = date('Y-m-d',$date);
    }

    krsort($workdays);
    return $workdays;
}
// --------------------------------
// Previous and Next 3 working days
function getWorkingDays2($date){
    $workdays['prev'] = $workdays['next'] = array();
    $holidays = getHolidays();
    $date     = strtotime($date);

    $start_date = $date;
    while(count($workdays['prev']) < 3){
        $date = strtotime("-1 day", $date);

        if(date('N',$date) < 6 && !in_array(date('Y-m-d',$date),$holidays))
            $workdays['prev'][] = date('Y-m-d',$date);
    }
    $date = $start_date;
    while(count($workdays['next']) < 3){
        $date = strtotime("+1 day", $date);

        if(date('N',$date) < 6 && !in_array(date('Y-m-d',$date),$holidays))
            $workdays['next'][] = date('Y-m-d',$date);
    }

    krsort($workdays['prev']);
    return $workdays;
}

function getHolidays(){
    $holidays = array(
        '2010-01-01', '2010-01-06',
        '2010-04-02', '2010-04-04', '2010-04-05',
        '2010-05-01', '2010-05-13', '2010-05-23',
        '2010-06-26',
        '2010-11-06',
        '2010-12-06', '2010-12-25', '2010-12-26'
    );
    return $holidays;
}

echo '<pre>';
print_r( getWorkingDays( '2010-04-04' ) );
print_r( getWorkingDays2( '2010-04-04' ) );
echo '</pre>';

Outputs:

Array
(
    [2] => 2010-03-30
    [1] => 2010-03-31
    [0] => 2010-04-01
)
Array
(
    [next] => Array
        (
            [0] => 2010-04-06
            [1] => 2010-04-07
            [2] => 2010-04-08
        )

    [prev] => Array
        (
            [2] => 2010-03-30
            [1] => 2010-03-31
            [0] => 2010-04-01
        )

)

I'm adding another answer since it follows a different approach from the ones I've posted before:

function getWorkDays($date){
    list($year,$month,$day) = explode('-',$date);
    $holidays = getHolidays();
    $dates    = array();

    while(count($dates) < 3){
        $newDate = date('Y-m-d',mktime(0,0,0,$month,--$day,$year));
        if(date('N',strtotime($newDate)) < 6 && !in_array($newDate,$holidays))
            $dates[] = $newDate;
    }

    return array_reverse($dates);
}

print_r(getWorkDays('2010-12-08'));

Output:

Array
(
    [0] => 2010-12-02
    [1] => 2010-12-03
    [2] => 2010-12-07
)

You mean like the WORKDAY() function in Excel

If you take a look at the WORKDAYS function in PHPExcel , you'll find an example of how to code such a function

Try this one (fair warning - I don't have access to test this out so please correct any syntax errors).

function LastThreeWorkdays($start_date) { 
    $startdateseed = strtotime($start_date); 
    $workdays = array(); 
    $holidays = get_holidays('2010'); 

    for ($counter = -1; $counter >= -10; $counter--) 
      if (date('N', $current_date = strtotime($counter.' day', $startdateseed)) < 6) $workdays[] = date('Y-m-d', $currentdate);

    return array_slice(array_reverse(array_diff($workdays, $holidays)), 0, 3);
}

Basically create a "chunk" of dates and then use array diff to remove the holidays from it. Return only the top (last) three items. Obviously it takes a miniscule more storage space and time to compute than previous answers but the code is much shorter.

The "chunk" size can be tweaked for further optimization. Ideally it would be the maximum number of consecutive holidays plus 2 plus 3 but that assumes realistic holiday scenarios (an entire week of holidays isn't possible, etc).

The code can be "unrolled" too to make some of the tricks easier to read. Overall shows off some of the PHP functions a little bit better - could be combined with the other ideas as well though.

/**
  * @param $currentdate like 'YYYY-MM-DD'
  * @param $n number of workdays to return
  * @param $direction 'previous' or 'next', default is 'next'
  **/
function adjacentWorkingDays($currentdate, $n, $direction='next') {
    $sign = ($direction == 'previous') ? '-' : '+';
    $workdays = array();
    $holidays = get_holidays();
    $i = 1;
    while (count($workdays) < $n) {
        $dateinteger = strtotime("{$currentdate} {$sign}{$i} days");
        $date = date('Y-m-d', $dateinteger);
        if (!in_array($date, $holidays) && date('N', $dateinteger) < 6) {
            $workdays[] = $date;
        }
        $i++;
    }
    return $workdays;
}

// you pass a year into get_holidays, make sure folks
// are accounting for the fact that adjacent holidays
// might cross a year boundary
function get_holidays() {
    $holidays = array(
        '2010-01-01',
        '2010-01-06',
        '2010-04-02',
        '2010-04-04',
        '2010-04-05',
        '2010-05-01',
        '2010-05-13',
        '2010-05-23',
        '2010-06-26',
        '2010-11-06',
        '2010-12-06',
        '2010-12-25',
        '2010-12-26'
    );
    return $holidays;
}

In these functions we use the adjacentWorkingDays() function:

// next $n working days, in ascending order
function nextWorkingDays($date, $n) {
    return adjacentWorkingDays($date, $n, 'next');
}

// previous $n workind days, in ascending order
function previousWorkingDays($date, $n) {
    return array_reverse(adjacentWorkingDays($date, $n, 'previous'));
}

Here's testing it out:

print "<pre>";
print_r(nextWorkingDays('2010-06-24', 3));
print_r(previousWorkingDays('2010-06-24', 3));
print "<pre>";

Results:

Array
(
    [0] => 2010-06-25
    [1] => 2010-06-28
    [2] => 2010-06-29
)
Array
(
    [0] => 2010-06-21
    [1] => 2010-06-22
    [2] => 2010-06-23
)

here is my submission ;)

/**
 * Helper function to handle year overflow
 */
function isHoliday($date) {
  static $holidays = array(); // static cache
  $year = date('Y', $date);

  if(!isset($holidays["$year"])) {
    $holidays["$year"] = get_holidays($year);
  }

  return in_array(date('Y-m-d', $date), $holidays["$year"]);
}

/**
 * Returns adjacent working days (by default: the previous three)
 */
function adjacentWorkingDays($start_date, $limit = 3, $direction = 'previous') {
  $current_date = strtotime($start_date);
  $direction = ($direction === 'next') ? 'next' : 'previous'; // sanity
  $workdays = array();

  // no need to verify the count before checking the first day.
  do {
    // using weekday here skips weekends.
    $current_date = strtotime("$direction weekday", $current_date);
    if (!isHoliday()) {
      // not a public holiday.
      $workdays[] = date('Y-m-d', $current_date);
    }
  } while (count($workdays) < $limit)

  return array_reverse($workdays);
}

Here's my take. This function (unlike most of the others posted) will not fail if you input a date at the beginning of the year. If you were to only call the get_holidays function on one year, the resulting array might include dates that are holidays from the previous year. My solution will call get_holidays again if we slip back into the previous year.

function get_working_days($date)
{
    $date_timestamp = strtotime($date);
    $year = date('Y', $date_timestamp);
    $holidays = get_holidays($year);
    $days = array();

    while (count($days) < 3)
    {
        $date_timestamp = strtotime('-1 day', $date_timestamp);
        $date = date('Y-m-d', $date_timestamp);         

        if (!in_array($date, $holidays) && date('N', $date_timestamp) < 6)
            $days[] = $date;


        $year2 = date('Y', $date_timestamp);
        if ($year2 != $year)
        {
            $holidays = array_merge($holidays, get_holidays($year2));
            $year = $year2;
        }
    }

    return $days;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM