Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Find three previous working days from a given date

Tags:

date

php

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 Y-m-d format
  • The function returns an array with three dates in Y-m-d 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;
}
like image 316
Tatu Ulmanen Avatar asked Jun 22 '10 12:06

Tatu Ulmanen


4 Answers

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.

like image 160
Wireblue Avatar answered Oct 16 '22 21:10

Wireblue


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;
}
like image 28
Keith Minkler Avatar answered Oct 16 '22 22:10

Keith Minkler


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;
}
like image 44
Emil Vikström Avatar answered Oct 16 '22 20:10

Emil Vikström


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);
}
like image 25
Daniel Egeberg Avatar answered Oct 16 '22 21:10

Daniel Egeberg