| 1 |
<?php |
| 2 |
|
| 3 |
namespace TablePress\PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; |
| 4 |
|
| 5 |
use DateTime; |
| 6 |
use TablePress\PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; |
| 7 |
use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Exception; |
| 8 |
use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Functions; |
| 9 |
|
| 10 |
class WorkDay |
| 11 |
{ |
| 12 |
use ArrayEnabled; |
| 13 |
|
| 14 |
/** |
| 15 |
* WORKDAY. |
| 16 |
* |
| 17 |
* Returns the date that is the indicated number of working days before or after a date (the |
| 18 |
* starting date). Working days exclude weekends and any dates identified as holidays. |
| 19 |
* Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected |
| 20 |
* delivery times, or the number of days of work performed. |
| 21 |
* |
| 22 |
* Excel Function: |
| 23 |
* WORKDAY(startDate,endDays[,holidays[,holiday[,...]]]) |
| 24 |
* |
| 25 |
* @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), |
| 26 |
* PHP DateTime object, or a standard date string |
| 27 |
* Or can be an array of date values |
| 28 |
* @param array<mixed>|int $endDays The number of nonweekend and nonholiday days before or after |
| 29 |
* startDate. A positive value for days yields a future date; a |
| 30 |
* negative value yields a past date. |
| 31 |
* Or can be an array of int values |
| 32 |
* @param mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation |
| 33 |
* |
| 34 |
* @return array<mixed>|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object, |
| 35 |
* depending on the value of the ReturnDateType flag |
| 36 |
* If an array of values is passed for the $startDate or $endDays,arguments, then the returned result |
| 37 |
* will also be an array with matching dimensions |
| 38 |
*/ |
| 39 |
public static function date($startDate, $endDays, ...$dateArgs) |
| 40 |
{ |
| 41 |
if (is_array($startDate) || is_array($endDays)) { |
| 42 |
return self::evaluateArrayArgumentsSubset( |
| 43 |
[self::class, __FUNCTION__], |
| 44 |
2, |
| 45 |
$startDate, |
| 46 |
$endDays, |
| 47 |
...$dateArgs |
| 48 |
); |
| 49 |
} |
| 50 |
|
| 51 |
// Retrieve the mandatory start date and days that are referenced in the function definition |
| 52 |
try { |
| 53 |
$startDate = Helpers::getDateValue($startDate); |
| 54 |
$endDays = Helpers::validateNumericNull($endDays); |
| 55 |
$holidayArray = array_map([Helpers::class, 'getDateValue'], Functions::flattenArray($dateArgs)); |
| 56 |
} catch (Exception $e) { |
| 57 |
return $e->getMessage(); |
| 58 |
} |
| 59 |
|
| 60 |
$startDate = (float) floor($startDate); |
| 61 |
$endDays = (int) floor($endDays); |
| 62 |
// If endDays is 0, we always return startDate |
| 63 |
if ($endDays == 0) { |
| 64 |
return $startDate; |
| 65 |
} |
| 66 |
if ($endDays < 0) { |
| 67 |
return self::decrementing($startDate, $endDays, $holidayArray); |
| 68 |
} |
| 69 |
|
| 70 |
return self::incrementing($startDate, $endDays, $holidayArray); |
| 71 |
} |
| 72 |
|
| 73 |
/** |
| 74 |
* Use incrementing logic to determine Workday. |
| 75 |
* |
| 76 |
* @param array<mixed> $holidayArray |
| 77 |
* @return float|int|\DateTime |
| 78 |
*/ |
| 79 |
private static function incrementing(float $startDate, int $endDays, array $holidayArray) |
| 80 |
{ |
| 81 |
// Adjust the start date if it falls over a weekend |
| 82 |
$startDoW = self::getWeekDay($startDate, 3); |
| 83 |
if ($startDoW >= 5) { |
| 84 |
$startDate += 7 - $startDoW; |
| 85 |
--$endDays; |
| 86 |
} |
| 87 |
|
| 88 |
// Add endDays |
| 89 |
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7); |
| 90 |
$endDays = $endDays % 5; |
| 91 |
while ($endDays > 0) { |
| 92 |
++$endDate; |
| 93 |
// Adjust the calculated end date if it falls over a weekend |
| 94 |
$endDow = self::getWeekDay($endDate, 3); |
| 95 |
if ($endDow >= 5) { |
| 96 |
$endDate += 7 - $endDow; |
| 97 |
} |
| 98 |
--$endDays; |
| 99 |
} |
| 100 |
|
| 101 |
// Test any extra holiday parameters |
| 102 |
if (!empty($holidayArray)) { |
| 103 |
$endDate = self::incrementingArray($startDate, $endDate, $holidayArray); |
| 104 |
} |
| 105 |
|
| 106 |
return Helpers::returnIn3FormatsFloat($endDate); |
| 107 |
} |
| 108 |
|
| 109 |
/** @param array<mixed> $holidayArray */ |
| 110 |
private static function incrementingArray(float $startDate, float $endDate, array $holidayArray): float |
| 111 |
{ |
| 112 |
$holidayCountedArray = $holidayDates = []; |
| 113 |
foreach ($holidayArray as $holidayDate) { |
| 114 |
/** @var float $holidayDate */ |
| 115 |
if (self::getWeekDay($holidayDate, 3) < 5) { |
| 116 |
$holidayDates[] = $holidayDate; |
| 117 |
} |
| 118 |
} |
| 119 |
sort($holidayDates, SORT_NUMERIC); |
| 120 |
foreach ($holidayDates as $holidayDate) { |
| 121 |
if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { |
| 122 |
if (!in_array($holidayDate, $holidayCountedArray)) { |
| 123 |
++$endDate; |
| 124 |
$holidayCountedArray[] = $holidayDate; |
| 125 |
} |
| 126 |
} |
| 127 |
// Adjust the calculated end date if it falls over a weekend |
| 128 |
$endDoW = self::getWeekDay($endDate, 3); |
| 129 |
if ($endDoW >= 5) { |
| 130 |
$endDate += 7 - $endDoW; |
| 131 |
} |
| 132 |
} |
| 133 |
|
| 134 |
return $endDate; |
| 135 |
} |
| 136 |
|
| 137 |
/** |
| 138 |
* Use decrementing logic to determine Workday. |
| 139 |
* |
| 140 |
* @param array<mixed> $holidayArray |
| 141 |
* @return float|int|\DateTime |
| 142 |
*/ |
| 143 |
private static function decrementing(float $startDate, int $endDays, array $holidayArray) |
| 144 |
{ |
| 145 |
// Adjust the start date if it falls over a weekend |
| 146 |
$startDoW = self::getWeekDay($startDate, 3); |
| 147 |
if ($startDoW >= 5) { |
| 148 |
$startDate += -$startDoW + 4; |
| 149 |
++$endDays; |
| 150 |
} |
| 151 |
|
| 152 |
// Add endDays |
| 153 |
$endDate = (float) $startDate + ((int) ($endDays / 5) * 7); |
| 154 |
$endDays = $endDays % 5; |
| 155 |
while ($endDays < 0) { |
| 156 |
--$endDate; |
| 157 |
// Adjust the calculated end date if it falls over a weekend |
| 158 |
$endDow = self::getWeekDay($endDate, 3); |
| 159 |
if ($endDow >= 5) { |
| 160 |
$endDate += 4 - $endDow; |
| 161 |
} |
| 162 |
++$endDays; |
| 163 |
} |
| 164 |
|
| 165 |
// Test any extra holiday parameters |
| 166 |
if (!empty($holidayArray)) { |
| 167 |
$endDate = self::decrementingArray($startDate, $endDate, $holidayArray); |
| 168 |
} |
| 169 |
|
| 170 |
return Helpers::returnIn3FormatsFloat($endDate); |
| 171 |
} |
| 172 |
|
| 173 |
/** @param array<mixed> $holidayArray */ |
| 174 |
private static function decrementingArray(float $startDate, float $endDate, array $holidayArray): float |
| 175 |
{ |
| 176 |
$holidayCountedArray = $holidayDates = []; |
| 177 |
foreach ($holidayArray as $holidayDate) { |
| 178 |
/** @var float $holidayDate */ |
| 179 |
if (self::getWeekDay($holidayDate, 3) < 5) { |
| 180 |
$holidayDates[] = $holidayDate; |
| 181 |
} |
| 182 |
} |
| 183 |
rsort($holidayDates, SORT_NUMERIC); |
| 184 |
foreach ($holidayDates as $holidayDate) { |
| 185 |
if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) { |
| 186 |
if (!in_array($holidayDate, $holidayCountedArray)) { |
| 187 |
--$endDate; |
| 188 |
$holidayCountedArray[] = $holidayDate; |
| 189 |
} |
| 190 |
} |
| 191 |
// Adjust the calculated end date if it falls over a weekend |
| 192 |
$endDoW = self::getWeekDay($endDate, 3); |
| 193 |
/** int $endDoW */ |
| 194 |
if ($endDoW >= 5) { |
| 195 |
$endDate += -$endDoW + 4; |
| 196 |
} |
| 197 |
} |
| 198 |
|
| 199 |
return $endDate; |
| 200 |
} |
| 201 |
|
| 202 |
private static function getWeekDay(float $date, int $wd): int |
| 203 |
{ |
| 204 |
$result = Functions::scalar(Week::day($date, $wd)); |
| 205 |
|
| 206 |
return is_int($result) ? $result : -1; |
| 207 |
} |
| 208 |
} |
| 209 |
|