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;
}
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);
}
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
)
)
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;
}
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;
}
This is one of the simplest possible ways:
function get_n_working_days($date,$n) {
$holidays_list = ['2021-01-26', '2021-03-11', '2021-03-29', '2021-04-02', '2021-04-14', '2021-04-21', '2021-05-13', '2021-07-21', '2021-08-19', '2021-09-10', '2021-10-15', '2021-11-04', '2021-11-05', '2021-11-19'];
$working_day_list = [];
$temp_date = $date;
while(count($working_day_list)<$n)
{
$temp_date = date('Y-m-d',strtotime('last weekday '.$temp_date));
if (!in_array($temp_date,$holidays_list))
array_push($working_day_list,$temp_date);
}
return $working_day_list;
}
The way to call the function is:
$list = get_n_working_days($some_date,2);
And some_date
is of form Y-m-d
:
for example - some_date = date('Y-m-d',strtotime('14-08-2021');
精彩评论