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:
Y-m-d
formatY-m-d
format, sorted from oldest to newest.Extra:
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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With