From ff9ac57c53da155177972947be852fa5a63e10db Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sat, 20 Feb 2021 20:14:59 -0800 Subject: [PATCH 1/3] 100% Coverage for Calculation/DateTime The code in DateTime is now completely covered. Along the way, some errors were discovered and corrected. - The tests which have had to be changed at the start of every year are replaced by more robust equivalents which do not require annual changes. - Several places in the code where Gnumeric and OpenOffice were thought to differ from Excel do not appear to have had any justification. I have left a comment where such code has been removed. - Use DateTime when possible rather than date, time, or strftime functions to avoid potential Y2038 problems. - Some impossible code has been removed, replaced by an explanatory comment. - NETWORKDAYS had a bug when the start date was Sunday. There had been no tests of this condition. - Some functions allow boolean and null arguments where a number is expected. This is more complicated than the equivalent situations in MathTrig because the initial date for these calculations can be Day 1 rather than Day 0. - More testing for dates from 1900-01-01 through the fictitious everywhere-but-Excel 1900-01-29. - This showed that there is an additional Excel bug - Excel evaluates WEEKNUM(emptycell) as 0, which is not a valid result for WEEKNUM without a second argument. PhpSpreadsheet now duplicates this bug. - There is a similar and even worse bug for 1904-01-01 in 1904 calculations. Weeknum returns 0 for this, but returns the correct value for arguments of 0 or null. - DATEVALUE should accept 1900-02-29 (sigh) and relatives. PhpSpreadsheet now duplicates this bug. - Testing bootstrap sets default timezone. This appears to be a relic from the releases of PHP where the unwise decision, subsequenly reversed, was made to issue messages for "no default timezone is set" rather than just use a sensible default. This was a disruptive setting for some of the tests I added. There is only one test in the entire suite which is default-timezone-dependent. Setting and resetting of default timezone is moved to that test (Reader/ODS/ODSTest), and out of bootstrap. - There had been no testing of NOW() function. - DATEVALUE test had no tests for 1904 calendar and needs some. - DATE test changed 1900/1904 calendar in use without restoring it. - WEEKDAY test had no tests for 1904 calendar and needs some. - Which revealed a bug in Shared/Date (excelToDateTimeObject was not recognizing 1904-01-01 as valid when 1904 calendar is in use). - And an additional bug in that legal 1904-calendar values in the 0.0-1.0 range yielded the same "wrong" answers as 1900-calendar (see "One note" below). Also the comment for one of the calendar-1904 tests was wrong in attempting to identify what time of day the fraction represented. I had wanted to break this up into a set of smaller modules, a process already started for Engineering and MathTrig. However the number of source code changes was sufficient that I wanted a clean delta for this request. If it is merged, I will work on breaking it up afterwards. One note - Shared/Date/excelToDateTimeObject, when calendar-1900 is in use, returns an unexpected result if its argument is between 0 and 1, which is nominally invalid for that calendar. It uses a base-1970 calendar in that instance. That check is not justifiable for calendar-1904, where values in that range are legal, so I made the check specific to calendar-1900, and adjusted 3 1904 unit test results accordingly. However, I have to admit that I don't understand why that check should be made even for calendar-1900. It certainly doesn't match anything that Excel does. I would recommend scrapping that code altogether. If agreed, I would do this as part of the break-up into smaller modules. Another note - more controversially, it is clear that PhpSpreadsheet needs to support the Excel and PHP date formats. Although it requires further study, I am not convinced that it needs to support Unix timestamp format. Since that is a potential source of Y2038 problems on 32-bit systems, I would like to open a PR to deprecate the use of that format. Please let me know if you are aware of a valid reason to continue to support it. --- src/PhpSpreadsheet/Calculation/DateTime.php | 446 ++++++++++-------- src/PhpSpreadsheet/Shared/Date.php | 4 +- .../Functions/DateTime/DateTest.php | 14 +- .../Functions/DateTime/DateValueTest.php | 40 +- .../Functions/DateTime/NowTest.php | 38 ++ .../Functions/DateTime/TimeTest.php | 32 +- .../Functions/DateTime/WeekDayTest.php | 20 +- .../Functions/DateTime/WeekNumTest.php | 22 +- .../Reader/Ods/OdsTest.php | 19 +- tests/bootstrap.php | 2 +- tests/data/Calculation/DateTime/DATEVALUE.php | 43 +- tests/data/Calculation/DateTime/DAY.php | 15 + .../data/Calculation/DateTime/ISOWEEKNUM.php | 10 + .../data/Calculation/DateTime/NETWORKDAYS.php | 18 + tests/data/Calculation/DateTime/WEEKDAY.php | 7 + tests/data/Calculation/DateTime/WEEKNUM.php | 27 ++ tests/data/Calculation/DateTime/WORKDAY.php | 16 + tests/data/Calculation/DateTime/YEARFRAC.php | 3 +- .../data/Shared/Date/ExcelToTimestamp1904.php | 8 +- 19 files changed, 539 insertions(+), 245 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index 4c2b108ad9..c11ac7b575 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -144,26 +144,10 @@ private static function adjustDateByMonths($dateValue = 0, $adjustmentMonths = 0 */ public static function DATETIMENOW() { - $saveTimeZone = date_default_timezone_get(); - date_default_timezone_set('UTC'); - $retValue = false; - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - $retValue = (float) Date::PHPToExcel(time()); + $dti = new DateTimeImmutable(); + $dateArray = date_parse($dti->format('c')); - break; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - $retValue = (int) time(); - - break; - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - $retValue = new \DateTime(); - - break; - } - date_default_timezone_set($saveTimeZone); - - return $retValue; + return self::returnIn3FormatsArray($dateArray); } /** @@ -185,27 +169,10 @@ public static function DATETIMENOW() */ public static function DATENOW() { - $saveTimeZone = date_default_timezone_get(); - date_default_timezone_set('UTC'); - $retValue = false; - $excelDateTime = floor(Date::PHPToExcel(time())); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - $retValue = (float) $excelDateTime; - - break; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - $retValue = (int) Date::excelToTimestamp($excelDateTime); + $dti = new DateTimeImmutable(); + $dateArray = date_parse($dti->format('c')); - break; - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - $retValue = Date::excelToDateTimeObject($excelDateTime); - - break; - } - date_default_timezone_set($saveTimeZone); - - return $retValue; + return self::returnIn3FormatsArray($dateArray, true); } /** @@ -316,14 +283,8 @@ public static function DATE($year = 0, $month = 1, $day = 1) // Execute function $excelDateValue = Date::formattedPHPToExcel($year, $month, $day); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $excelDateValue; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp($excelDateValue); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return Date::excelToDateTimeObject($excelDateValue); - } + + return self::returnIn3FormatsFloat($excelDateValue); } /** @@ -403,36 +364,24 @@ public static function TIME($hour = 0, $minute = 0, $second = 0) } // Execute function - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - $date = 0; - $calendar = Date::getExcelCalendar(); - if ($calendar != Date::CALENDAR_WINDOWS_1900) { - $date = 1; - } - - return (float) Date::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp(Date::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - $dayAdjust = 0; - if ($hour < 0) { - $dayAdjust = floor($hour / 24); - $hour = 24 - abs($hour % 24); - if ($hour == 24) { - $hour = 0; - } - } elseif ($hour >= 24) { - $dayAdjust = floor($hour / 24); - $hour = $hour % 24; - } - $phpDateObject = new \DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); - if ($dayAdjust != 0) { - $phpDateObject->modify($dayAdjust . ' days'); - } + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $date = 0; + $calendar = Date::getExcelCalendar(); + if ($calendar != Date::CALENDAR_WINDOWS_1900) { + $date = 1; + } - return $phpDateObject; + return (float) Date::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) Date::excelToTimestamp(Date::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 + } + // RETURNDATE_PHP_DATETIME_OBJECT + // Hour has already been normalized (0-23) above + $phpDateObject = new \DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); + + return $phpDateObject; } /** @@ -462,6 +411,8 @@ public static function TIME($hour = 0, $minute = 0, $second = 0) */ public static function DATEVALUE($dateValue = 1) { + $dti = new DateTimeImmutable(); + $baseYear = Date::getExcelCalendar(); $dateValue = trim(Functions::flattenSingleValue($dateValue), '"'); // Strip any ordinals because they're allowed in Excel (English only) $dateValue = preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); @@ -481,10 +432,11 @@ public static function DATEVALUE($dateValue = 1) $yearFound = true; } } - if ((count($t1) == 1) && (strpos($t, ':') !== false)) { + if (count($t1) === 1) { // We've been fed a time value without any date - return 0.0; - } elseif (count($t1) == 2) { + return ((strpos($t, ':') === false)) ? Functions::Value() : 0.0; + } + if (count($t1) == 2) { // We only have two parts of the date: either day/month or month/year if ($yearFound) { array_unshift($t1, 1); @@ -493,7 +445,7 @@ public static function DATEVALUE($dateValue = 1) $t1[1] += 1900; array_unshift($t1, 1); } else { - $t1[] = date('Y'); + $t1[] = $dti->format('Y'); } } } @@ -502,23 +454,13 @@ public static function DATEVALUE($dateValue = 1) $PHPDateArray = date_parse($dateValue); if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { + // If original count was 1, we've already returned. + // If it was 2, we added another. + // Therefore, neither of the first 2 stroks below can fail. $testVal1 = strtok($dateValue, '- '); - if ($testVal1 !== false) { - $testVal2 = strtok('- '); - if ($testVal2 !== false) { - $testVal3 = strtok('- '); - if ($testVal3 === false) { - $testVal3 = strftime('%Y'); - } - } else { - return Functions::VALUE(); - } - } else { - return Functions::VALUE(); - } - if ($testVal1 < 31 && $testVal2 < 12 && $testVal3 < 12 && strlen($testVal3) == 2) { - $testVal3 += 2000; - } + $testVal2 = strtok('- '); + $testVal3 = strtok('- ') ?: $dti->format('Y'); + self::adjustYear($testVal1, $testVal2, $testVal3); $PHPDateArray = date_parse($testVal1 . '-' . $testVal2 . '-' . $testVal3); if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { $PHPDateArray = date_parse($testVal2 . '-' . $testVal1 . '-' . $testVal3); @@ -528,44 +470,131 @@ public static function DATEVALUE($dateValue = 1) } } + $retValue = Functions::Value(); if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { // Execute function - if ($PHPDateArray['year'] == '') { - $PHPDateArray['year'] = strftime('%Y'); - } - if ($PHPDateArray['year'] < 1900) { + self::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y')); + if ($PHPDateArray['year'] < $baseYear) { return Functions::VALUE(); } - if ($PHPDateArray['month'] == '') { - $PHPDateArray['month'] = strftime('%m'); - } - if ($PHPDateArray['day'] == '') { - $PHPDateArray['day'] = strftime('%d'); + self::replaceIfEmpty($PHPDateArray['month'], $dti->format('m')); + self::replaceIfEmpty($PHPDateArray['day'], $dti->format('d')); + $PHPDateArray['hour'] = 0; + $PHPDateArray['minute'] = 0; + $PHPDateArray['second'] = 0; + $month = (int) $PHPDateArray['month']; + $day = (int) $PHPDateArray['day']; + $year = (int) $PHPDateArray['year']; + if (!checkdate($month, $day, $year)) { + return self::check19000229($year, $month, $day); } - if (!checkdate($PHPDateArray['month'], $PHPDateArray['day'], $PHPDateArray['year'])) { - return Functions::VALUE(); + $retValue = self::returnIn3FormatsArray($PHPDateArray, true); + } + + return $retValue; + } + + private static function check19000229(int $year, int $month, int $day): string + { + return ($year === 1900 && $month === 2 && $day === 29) ? self::returnIn3FormatsFloat(60.0) : Functions::VALUE(); + } + + /** + * Help reduce perceived complexity of some tests. + * + * @param mixed $value + * @param mixed $altValue + */ + private static function replaceIfEmpty(&$value, $altValue): void + { + $value = $value ?: $altValue; + } + + /** + * Adjust year in ambiguous situations. + */ + private static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void + { + if (!is_numeric($testVal1) || $testVal1 < 31) { + if (!is_numeric($testVal2) || $testVal2 < 12) { + if (is_numeric($testVal3) && $testVal3 < 12) { + $testVal3 += 2000; + } } - $excelDateValue = floor( - Date::formattedPHPToExcel( - $PHPDateArray['year'], - $PHPDateArray['month'], - $PHPDateArray['day'], - $PHPDateArray['hour'], - $PHPDateArray['minute'], - $PHPDateArray['second'] - ) + } + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + private static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return new \DateTime( + $dateArray['year'] + . '-' . $dateArray['month'] + . '-' . $dateArray['day'] + . ' ' . $dateArray['hour'] + . ':' . $dateArray['minute'] + . ':' . $dateArray['second'] ); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $excelDateValue; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp($excelDateValue); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return new \DateTime($PHPDateArray['year'] . '-' . $PHPDateArray['month'] . '-' . $PHPDateArray['day'] . ' 00:00:00'); - } } + $excelDateValue = + Date::formattedPHPToExcel( + $dateArray['year'], + $dateArray['month'], + $dateArray['day'], + $dateArray['hour'], + $dateArray['minute'], + $dateArray['second'] + ); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $noFrac ? floor($excelDateValue) : (float) $excelDateValue; + } + // RETURNDATE_UNIX_TIMESTAMP) - return Functions::VALUE(); + return (int) Date::excelToTimestamp($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + private static function returnIn3FormatsFloat(float $excelDateValue) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $excelDateValue; + } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) Date::excelToTimestamp($excelDateValue); + } + // RETURNDATE_PHP_DATETIME_OBJECT + + return Date::excelToDateTimeObject($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + private static function returnIn3FormatsObject(\DateTime $PHPDateObject) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return $PHPDateObject; + } + if ($retType === Functions::RETURNDATE_EXCEL) { + return (float) Date::PHPToExcel($PHPDateObject); + } + // RETURNDATE_UNIX_TIMESTAMP + + return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); } /** @@ -601,31 +630,22 @@ public static function TIMEVALUE($timeValue) } $PHPDateArray = date_parse($timeValue); + $retValue = Functions::VALUE(); if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE) { - $excelDateValue = Date::formattedPHPToExcel( - $PHPDateArray['year'], - $PHPDateArray['month'], - $PHPDateArray['day'], - $PHPDateArray['hour'], - $PHPDateArray['minute'], - $PHPDateArray['second'] - ); + // OpenOffice-specific code removed - it works just like Excel + $excelDateValue = Date::formattedPHPToExcel(1900, 1, 1, $PHPDateArray['hour'], $PHPDateArray['minute'], $PHPDateArray['second']) - 1; + + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $retValue = (float) $excelDateValue; + } elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + $retValue = (int) $phpDateValue = Date::excelToTimestamp($excelDateValue + 25569) - 3600; } else { - $excelDateValue = Date::formattedPHPToExcel(1900, 1, 1, $PHPDateArray['hour'], $PHPDateArray['minute'], $PHPDateArray['second']) - 1; - } - - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $excelDateValue; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) $phpDateValue = Date::excelToTimestamp($excelDateValue + 25569) - 3600; - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return new \DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); + $retValue = new \DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); } } - return Functions::VALUE(); + return $retValue; } /** @@ -980,7 +1000,7 @@ public static function NETWORKDAYS($startDate, $endDate, ...$dateArgs) // Execute function $startDoW = 6 - self::WEEKDAY($startDate, 2); if ($startDoW < 0) { - $startDoW = 0; + $startDoW = 5; } $endDoW = self::WEEKDAY($endDate, 2); if ($endDoW >= 6) { @@ -1113,14 +1133,7 @@ public static function WORKDAY($startDate, $endDays, ...$dateArgs) } } - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $endDate; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp($endDate); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return Date::excelToDateTimeObject($endDate); - } + return self::returnIn3FormatsFloat($endDate); } /** @@ -1141,9 +1154,10 @@ public static function DAYOFMONTH($dateValue = 1) { $dateValue = Functions::flattenSingleValue($dateValue); - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { + if ($dateValue === null || is_bool($dateValue)) { + return (int) $dateValue; + } + if (is_string($dateValue = self::getDateValue($dateValue))) { return Functions::VALUE(); } @@ -1182,6 +1196,7 @@ public static function DAYOFMONTH($dateValue = 1) public static function WEEKDAY($dateValue = 1, $style = 1) { $dateValue = Functions::flattenSingleValue($dateValue); + self::nullFalseTrueToNumber($dateValue); $style = Functions::flattenSingleValue($style); if (!is_numeric($style)) { @@ -1191,16 +1206,17 @@ public static function WEEKDAY($dateValue = 1, $style = 1) } $style = floor($style); - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { + $dateValue = self::getDateValue($dateValue); + if (is_string($dateValue)) { return Functions::VALUE(); - } elseif ($dateValue < 0.0) { + } + if ($dateValue < 0.0) { return Functions::NAN(); } // Execute function $PHPDateObject = Date::excelToDateTimeObject($dateValue); + self::silly1900($PHPDateObject); $DoW = (int) $PHPDateObject->format('w'); $firstDay = 1; @@ -1224,15 +1240,6 @@ public static function WEEKDAY($dateValue = 1, $style = 1) break; } - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { - // Test for Excel's 1900 leap year, and introduce the error as required - if (($PHPDateObject->format('Y') == 1900) && ($PHPDateObject->format('n') <= 2)) { - --$DoW; - if ($DoW < $firstDay) { - $DoW += 7; - } - } - } return $DoW; } @@ -1298,17 +1305,26 @@ public static function WEEKDAY($dateValue = 1, $style = 1) */ public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) { + $origDateValueNull = $dateValue === null; $dateValue = Functions::flattenSingleValue($dateValue); $method = Functions::flattenSingleValue($method); - if (!is_numeric($method)) { return Functions::VALUE(); } + $method = (int) $method; if (!array_key_exists($method, self::METHODARR)) { return Functions::NaN(); } $method = self::METHODARR[$method]; + if ($dateValue === null) { // boolean not allowed + // This seems to be an additional Excel bug. + if (self::buggyWeekNum1900($method)) { + return 0; + } + //$dateValue = 1; + $dateValue = (Date::getExcelCalendar() === DATE::CALENDAR_MAC_1904) ? 0 : 1; + } $dateValue = self::getDateValue($dateValue); if (is_string($dateValue)) { @@ -1321,8 +1337,14 @@ public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) // Execute function $PHPDateObject = Date::excelToDateTimeObject($dateValue); if ($method == self::STARTWEEK_MONDAY_ISO) { + self::silly1900($PHPDateObject); + return (int) $PHPDateObject->format('W'); } + if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) { + return 0; + } + self::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches $dayOfYear = $PHPDateObject->format('z'); $PHPDateObject->modify('-' . $dayOfYear . ' days'); $firstDayOfFirstWeek = $PHPDateObject->format('w'); @@ -1334,6 +1356,18 @@ public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) return (int) $weekOfYear; } + private static function buggyWeekNum1900(int $method): bool + { + return $method === self::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_WINDOWS_1900; + } + + private static function buggyWeekNum1904(int $method, bool $origNull, \DateTime $dateObject): bool + { + // This appears to be another Excel bug. + + return $method === self::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_MAC_1904 && !$origNull && $dateObject->format('Y-m-d') === '1904-01-01'; + } + /** * ISOWEEKNUM. * @@ -1350,17 +1384,19 @@ public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) public static function ISOWEEKNUM($dateValue = 1) { $dateValue = Functions::flattenSingleValue($dateValue); + self::nullFalseTrueToNumber($dateValue); - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { + $dateValue = self::getDateValue($dateValue); + if (!is_numeric($dateValue)) { return Functions::VALUE(); - } elseif ($dateValue < 0.0) { + } + if ($dateValue < 0.0) { return Functions::NAN(); } // Execute function $PHPDateObject = Date::excelToDateTimeObject($dateValue); + self::silly1900($PHPDateObject); return (int) $PHPDateObject->format('W'); } @@ -1449,12 +1485,7 @@ public static function HOUROFDAY($timeValue = 0) $timeValue = Functions::flattenSingleValue($timeValue); if (!is_numeric($timeValue)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $testVal = strtok($timeValue, '/-: '); - if (strlen($testVal) < strlen($timeValue)) { - return Functions::VALUE(); - } - } + // Gnumeric test removed - it operates like Excel $timeValue = self::getTimeValue($timeValue); if (is_string($timeValue)) { return Functions::VALUE(); @@ -1490,12 +1521,7 @@ public static function MINUTE($timeValue = 0) $timeValue = $timeTester = Functions::flattenSingleValue($timeValue); if (!is_numeric($timeValue)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $testVal = strtok($timeValue, '/-: '); - if (strlen($testVal) < strlen($timeValue)) { - return Functions::VALUE(); - } - } + // Gnumeric test removed - it operates like Excel $timeValue = self::getTimeValue($timeValue); if (is_string($timeValue)) { return Functions::VALUE(); @@ -1531,12 +1557,7 @@ public static function SECOND($timeValue = 0) $timeValue = Functions::flattenSingleValue($timeValue); if (!is_numeric($timeValue)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $testVal = strtok($timeValue, '/-: '); - if (strlen($testVal) < strlen($timeValue)) { - return Functions::VALUE(); - } - } + // Gnumeric test removed - it operates like Excel $timeValue = self::getTimeValue($timeValue); if (is_string($timeValue)) { return Functions::VALUE(); @@ -1590,14 +1611,7 @@ public static function EDATE($dateValue = 1, $adjustmentMonths = 0) // Execute function $PHPDateObject = self::adjustDateByMonths($dateValue, $adjustmentMonths); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) Date::PHPToExcel($PHPDateObject); - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return $PHPDateObject; - } + return self::returnIn3FormatsObject($PHPDateObject); } /** @@ -1639,13 +1653,31 @@ public static function EOMONTH($dateValue = 1, $adjustmentMonths = 0) $adjustDaysString = '-' . $adjustDays . ' days'; $PHPDateObject->modify($adjustDaysString); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) Date::PHPToExcel($PHPDateObject); - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return $PHPDateObject; + return self::returnIn3FormatsObject($PHPDateObject); + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @param mixed $number + */ + private static function nullFalseTrueToNumber(&$number): void + { + $number = Functions::flattenSingleValue($number); + $baseYear = Date::getExcelCalendar(); + $nullVal = $baseYear === DATE::CALENDAR_MAC_1904 ? 0 : 1; + if ($number === null) { + $number = $nullVal; + } elseif (is_bool($number)) { + $number = $nullVal + (int) $number; + } + } + + private static function silly1900(\DateTime $PHPDateObject, string $mod = '-1 day'): void + { + $isoDate = $PHPDateObject->format('c'); + if ($isoDate < '1900-03-01') { + $PHPDateObject->modify($mod); } } } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 180a71596d..1a8a2119f5 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -160,7 +160,9 @@ public static function excelToDateTimeObject($excelTimestamp, $timeZone = null) { $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone); if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { - if ($excelTimestamp < 1.0) { + $baseYear = self::getExcelCalendar(); + //if ($excelTimestamp < ((self::$excelCalendar === self::CALENDAR_MAC_1904) ? 0 : 1)) { + if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) { // Unix timestamp base date $baseDate = new \DateTime('1970-01-01', $timeZone); } else { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php index 48f7cfd7ec..aad59729be 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php @@ -9,11 +9,21 @@ class DateTest extends TestCase { + private $returnDateType; + + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->returnDateType = Functions::getReturnDateType(); + $this->excelCalendar = Date::getExcelCalendar(); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + } + + protected function tearDown(): void + { + Functions::setReturnDateType($this->returnDateType); + Date::setExcelCalendar($this->excelCalendar); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php index 51e4f7c0cf..72e036f95d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; +use DateTimeImmutable; use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\DateTime; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -10,11 +11,21 @@ class DateValueTest extends TestCase { + private $returnDateType; + + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->returnDateType = Functions::getReturnDateType(); + $this->excelCalendar = Date::getExcelCalendar(); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + } + + protected function tearDown(): void + { + Functions::setReturnDateType($this->returnDateType); + Date::setExcelCalendar($this->excelCalendar); } /** @@ -25,7 +36,21 @@ protected function setUp(): void */ public function testDATEVALUE($expectedResult, $dateValue): void { - $result = DateTime::DATEVALUE($dateValue); + // Loop to avoid extraordinarily rare edge case where first calculation + // and second do not take place on same day. + do { + $dtStart = new DateTimeImmutable(); + $startDay = $dtStart->format('d'); + if (is_string($expectedResult)) { + $replYMD = str_replace('Y', date('Y'), $expectedResult); + if ($replYMD !== $expectedResult) { + $expectedResult = DateTime::DATEVALUE($replYMD); + } + } + $result = DateTime::DATEVALUE($dateValue); + $dtEnd = new DateTimeImmutable(); + $endDay = $dtEnd->format('d'); + } while ($startDay !== $endDay); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } @@ -55,4 +80,13 @@ public function testDATEVALUEtoDateTimeObject(): void // ... with the correct value self::assertEquals($result->format('d-M-Y'), '31-Jan-2012'); } + + public function testDATEVALUEwith1904Calendar(): void + { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + self::assertEquals(5428, DateTime::DATEVALUE('1918-11-11')); + self::assertEquals(0, DateTime::DATEVALUE('1904-01-01')); + self::assertEquals('#VALUE!', DateTime::DATEVALUE('1903-12-31')); + self::assertEquals('#VALUE!', DateTime::DATEVALUE('1900-02-29')); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php new file mode 100644 index 0000000000..f139f70393 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php @@ -0,0 +1,38 @@ +getActiveSheet(); + // Loop to avoid rare edge case where first calculation + // and second do not take place in same second. + do { + $dtStart = new DateTimeImmutable(); + $startSecond = $dtStart->format('s'); + $sheet->setCellValue('A1', '=NOW()'); + $dtEnd = new DateTimeImmutable(); + $endSecond = $dtEnd->format('s'); + } while ($startSecond !== $endSecond); + //echo("\n"); var_dump($sheet->getCell('A1')->getCalculatedValue()); echo ("\n"); + $sheet->setCellValue('B1', '=YEAR(A1)'); + $sheet->setCellValue('C1', '=MONTH(A1)'); + $sheet->setCellValue('D1', '=DAY(A1)'); + $sheet->setCellValue('E1', '=HOUR(A1)'); + $sheet->setCellValue('F1', '=MINUTE(A1)'); + $sheet->setCellValue('G1', '=SECOND(A1)'); + self::assertEquals($dtStart->format('Y'), $sheet->getCell('B1')->getCalculatedValue()); + self::assertEquals($dtStart->format('m'), $sheet->getCell('C1')->getCalculatedValue()); + self::assertEquals($dtStart->format('d'), $sheet->getCell('D1')->getCalculatedValue()); + self::assertEquals($dtStart->format('H'), $sheet->getCell('E1')->getCalculatedValue()); + self::assertEquals($dtStart->format('i'), $sheet->getCell('F1')->getCalculatedValue()); + self::assertEquals($dtStart->format('s'), $sheet->getCell('G1')->getCalculatedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php index 344061d48b..3ef58374a6 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php @@ -9,11 +9,20 @@ class TimeTest extends TestCase { + private $returnDateType; + + private $calendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->returnDateType = Functions::getReturnDateType(); + $this->calendar = Date::getExcelCalendar(); + } + + protected function tearDown(): void + { + Functions::setReturnDateType($this->returnDateType); + Date::setExcelCalendar($this->calendar); } /** @@ -23,6 +32,7 @@ protected function setUp(): void */ public function testTIME($expectedResult, ...$args): void { + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); $result = DateTime::TIME(...$args); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } @@ -52,4 +62,20 @@ public function testTIMEtoDateTimeObject(): void // ... with the correct value self::assertEquals($result->format('H:i:s'), '07:30:20'); } + + public function testTIME1904(): void + { + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + $result = DateTime::TIME(0, 0, 0); + self::assertEquals(0, $result); + } + + public function testTIME1900(): void + { + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $result = DateTime::TIME(0, 0, 0); + self::assertEquals(0, $result); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php index c5b89e01e1..99aa6f7cb9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php @@ -3,17 +3,21 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Shared\Date; use PHPUnit\Framework\TestCase; class WeekDayTest extends TestCase { + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->excelCalendar = Date::getExcelCalendar(); + } + + protected function tearDown(): void + { + Date::setExcelCalendar($this->excelCalendar); } /** @@ -31,4 +35,12 @@ public function providerWEEKDAY() { return require 'tests/data/Calculation/DateTime/WEEKDAY.php'; } + + public function testWEEKDAYwith1904Calendar(): void + { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + self::assertEquals(7, DateTime::WEEKDAY('1904-01-02')); + self::assertEquals(6, DateTime::WEEKDAY('1904-01-01')); + self::assertEquals(6, DateTime::WEEKDAY(null)); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php index 9d8e1eb281..17119f2873 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php @@ -3,17 +3,21 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Shared\Date; use PHPUnit\Framework\TestCase; class WeekNumTest extends TestCase { + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->excelCalendar = Date::getExcelCalendar(); + } + + protected function tearDown(): void + { + Date::setExcelCalendar($this->excelCalendar); } /** @@ -31,4 +35,14 @@ public function providerWEEKNUM() { return require 'tests/data/Calculation/DateTime/WEEKNUM.php'; } + + public function testWEEKNUMwith1904Calendar(): void + { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + self::assertEquals(27, DateTime::WEEKNUM('2004-07-02')); + self::assertEquals(1, DateTime::WEEKNUM('1904-01-02')); + self::assertEquals(1, DateTime::WEEKNUM(null)); + // The following is a bug in Excel. + self::assertEquals(0, DateTime::WEEKNUM('1904-01-01')); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 8be1aa7cc1..0160f68d62 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -15,6 +15,19 @@ */ class OdsTest extends TestCase { + private $timeZone; + + protected function setUp(): void + { + $this->timeZone = date_default_timezone_get(); + date_default_timezone_set('UTC'); + } + + protected function tearDown(): void + { + date_default_timezone_set($this->timeZone); + } + /** * @var Spreadsheet */ @@ -153,13 +166,13 @@ public function testReadValueAndComments(): void self::assertEquals(0, $firstSheet->getCell('G10')->getValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A10')->getDataType()); // Date - self::assertEquals(22269.0, $firstSheet->getCell('A10')->getValue()); + self::assertEquals('19-Dec-60', $firstSheet->getCell('A10')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A13')->getDataType()); // Time - self::assertEquals(25569.0625, $firstSheet->getCell('A13')->getValue()); + self::assertEquals('2:30:00', $firstSheet->getCell('A13')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A15')->getDataType()); // Date + Time - self::assertEquals(22269.0625, $firstSheet->getCell('A15')->getValue()); + self::assertEquals('19-Dec-60 1:30:00', $firstSheet->getCell('A15')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A11')->getDataType()); // Fraction diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 77cd522838..9ebd3f2623 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -3,4 +3,4 @@ setlocale(LC_ALL, 'en_US.utf8'); // PHP 5.3 Compat -date_default_timezone_set('Europe/London'); +//date_default_timezone_set('Europe/London'); diff --git a/tests/data/Calculation/DateTime/DATEVALUE.php b/tests/data/Calculation/DateTime/DATEVALUE.php index 17110c749b..0b0f110f5e 100644 --- a/tests/data/Calculation/DateTime/DATEVALUE.php +++ b/tests/data/Calculation/DateTime/DATEVALUE.php @@ -20,12 +20,12 @@ '1900/2/28', ], [ - '#VALUE!', + '60', '29-02-1900', ], // MS Excel will fail with a #VALUE return, but PhpSpreadsheet can parse this date [ - '#VALUE!', + '60', '29th February 1900', ], [ @@ -159,30 +159,32 @@ '#VALUE!', 'The 1st day of March 2007', ], - // 01/01 of the current year + // Jan 1 of the current year [ - 44197, + 'Y-01-01', '1 Jan', ], - // 31/12 of the current year + // Dec 31 of the current year [ - 44561, + 'Y-12-31', '31/12', ], - // Excel reads as 1st December 1931, not 31st December in current year + // Excel reads as 1st December 1931, not 31st December in current year. + // This result is locale-dependent in Excel, in a manner not + // supported by PhpSpreadsheet. [ 11658, '12/31', ], - // 05/07 of the current year + // July 5 of the current year [ - 44382, + 'Y-07-05', '5-JUL', ], - // 05/07 of the current year + // July 5 of the current year [ - 44382, - '5 Jul', + 'Y-07-05', + '5 July', ], [ 39783, @@ -216,6 +218,11 @@ '#VALUE!', 12, ], + // implicit day of month is 1 + [ + 40210, + 'Feb-2010', + ], [ 40221, '12-Feb-2010', @@ -294,4 +301,16 @@ '#VALUE!', 'ABCDEFGHIJKMNOPQRSTUVWXYZ', ], + [ + '#VALUE!', + '1999', + ], + ['#VALUE!', '32/32'], + ['#VALUE!', '1910-'], + ['#VALUE!', '10--'], + ['#VALUE!', '--10'], + ['#VALUE!', '--1910'], + //['#VALUE!', '-JUL-1910'], We can parse this, Excel can't + ['#VALUE!', '2008-08-'], + [36751, '0-08-13'], ]; diff --git a/tests/data/Calculation/DateTime/DAY.php b/tests/data/Calculation/DateTime/DAY.php index 8ba4ad41e8..81fab933b4 100644 --- a/tests/data/Calculation/DateTime/DAY.php +++ b/tests/data/Calculation/DateTime/DAY.php @@ -53,4 +53,19 @@ 30, // Result for OpenOffice 0, ], + [ + 0, // Result for Excel + 0, // Result for OpenOffice + null, + ], + [ + 1, // Result for Excel + 1, // Result for OpenOffice + true, + ], + [ + 0, // Result for Excel + 0, // Result for OpenOffice + false, + ], ]; diff --git a/tests/data/Calculation/DateTime/ISOWEEKNUM.php b/tests/data/Calculation/DateTime/ISOWEEKNUM.php index 78a4d3e800..b6afd303e3 100644 --- a/tests/data/Calculation/DateTime/ISOWEEKNUM.php +++ b/tests/data/Calculation/DateTime/ISOWEEKNUM.php @@ -33,4 +33,14 @@ '#VALUE!', '1800-01-01', ], + ['52', null], + ['53', '1904-01-01'], + ['52', '1900-01-01'], + ['1', '1900-01-07'], + ['1', '1900-01-08'], + ['2', '1900-01-09'], + ['9', '1900-03-04'], + ['10', '1900-03-05'], + ['#NUM!', '-1'], + [39, '1000'], ]; diff --git a/tests/data/Calculation/DateTime/NETWORKDAYS.php b/tests/data/Calculation/DateTime/NETWORKDAYS.php index d62e501caf..db548ddc95 100644 --- a/tests/data/Calculation/DateTime/NETWORKDAYS.php +++ b/tests/data/Calculation/DateTime/NETWORKDAYS.php @@ -100,4 +100,22 @@ '31-Jan-2007', '1-Feb-2007', ], + ['#VALUE!', 'ABQZ', '1-Feb-2007'], + ['#VALUE!', '1-Feb-2007', 'ABQZ'], + [10, '2021-02-13', '2021-02-27'], + [10, '2021-02-14', '2021-02-27'], + [3, '2021-02-14', '2021-02-17'], + [8, '2021-02-14', '2021-02-24'], + [9, '2021-02-14', '2021-02-25'], + [10, '2021-02-14', '2021-02-26'], + [9, '2021-02-13', '2021-02-25'], + [10, '2021-02-12', '2021-02-25'], + [ + '#VALUE!', + '10-Jan-1961', + '19-Dec-1960', + '25-Dec-1960', + 'ABQZ', + '01-Jan-1961', + ], ]; diff --git a/tests/data/Calculation/DateTime/WEEKDAY.php b/tests/data/Calculation/DateTime/WEEKDAY.php index 8cc6808252..fadf11f72e 100644 --- a/tests/data/Calculation/DateTime/WEEKDAY.php +++ b/tests/data/Calculation/DateTime/WEEKDAY.php @@ -110,4 +110,11 @@ '#NUM!', -1, ], + [1, null], + [1, false], + [2, true], + [1, '1900-01-01'], + [7, '1900-01-01', 2], + [7, null, 2], + [7, '1900-02-05', 2], ]; diff --git a/tests/data/Calculation/DateTime/WEEKNUM.php b/tests/data/Calculation/DateTime/WEEKNUM.php index d73ee463ae..b109a5345f 100644 --- a/tests/data/Calculation/DateTime/WEEKNUM.php +++ b/tests/data/Calculation/DateTime/WEEKNUM.php @@ -173,4 +173,31 @@ 1, '2025-12-29', 21, ], + ['9', '1900-03-01'], + ['2', '1900-01-07', 2], + ['2', '1905-01-07', 2], + ['1', '1900-01-01'], + ['1', '1900-01-01', 2], + ['2', '1900-01-02', 2], + ['1', null, 11], + ['1', null, 12], + ['1', null, 13], + ['1', null, 14], + ['1', null, 15], + ['1', null, 16], + ['0', null, 17], + ['1', '1905-01-01', 17], + ['0', null], + ['1', null, 2], + ['1', '1906-01-01'], + ['#VALUE!', true], + ['#VALUE!', false, 21], + ['52', null, 21], + ['53', '1904-01-01', 21], + ['52', '1900-01-01', 21], + ['1', '1900-01-07', 21], + ['1', '1900-01-08', 21], + ['2', '1900-01-09', 21], + ['9', '1900-03-04', 21], + ['10', '1900-03-05', 21], ]; diff --git a/tests/data/Calculation/DateTime/WORKDAY.php b/tests/data/Calculation/DateTime/WORKDAY.php index 76c517f952..fc5f648339 100644 --- a/tests/data/Calculation/DateTime/WORKDAY.php +++ b/tests/data/Calculation/DateTime/WORKDAY.php @@ -89,4 +89,20 @@ ], ], ], + [ + 44242, + '15-Feb-2021', + 0, + ], + [ + '#VALUE!', + '5-Apr-2012', + 3, + [ + [ + '6-Apr-2012', + 'ABQZ', + ], + ], + ], ]; diff --git a/tests/data/Calculation/DateTime/YEARFRAC.php b/tests/data/Calculation/DateTime/YEARFRAC.php index 3e76087c6b..abdb71d9eb 100644 --- a/tests/data/Calculation/DateTime/YEARFRAC.php +++ b/tests/data/Calculation/DateTime/YEARFRAC.php @@ -559,5 +559,6 @@ '2025-05-28', 1, ], - + ['#VALUE!', '2023-04-27', 'ABQZ', 1], + ['#VALUE!', 'ABQZ', '2023-04-07', 1], ]; diff --git a/tests/data/Shared/Date/ExcelToTimestamp1904.php b/tests/data/Shared/Date/ExcelToTimestamp1904.php index e0f3075435..1c013ab4ef 100644 --- a/tests/data/Shared/Date/ExcelToTimestamp1904.php +++ b/tests/data/Shared/Date/ExcelToTimestamp1904.php @@ -29,17 +29,17 @@ ], // 06:00:00 [ - 21600, + gmmktime(6, 0, 0, 1, 1, 1904), // 32-bit safe - no Y2038 problem 0.25, ], // 08:00.00 [ - 28800, + gmmktime(8, 0, 0, 1, 1, 1904), // 32-bit safe - no Y2038 problem 0.3333333333333333333, ], - // 02:57:46 + // 13:02:13 [ - 46933, + gmmktime(13, 02, 13, 1, 1, 1904), // 32-bit safe - no Y2038 problem 0.54321, ], ]; From 264ad2b0ef7cd6ec335e9fb483192fbb52be25c3 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sat, 20 Feb 2021 21:05:12 -0800 Subject: [PATCH 2/3] Scrutinizer Recommendations The usual set of stuff. --- src/PhpSpreadsheet/Calculation/DateTime.php | 17 ++++++----------- src/PhpSpreadsheet/Shared/Date.php | 2 -- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index c11ac7b575..e051395a8a 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -147,7 +147,7 @@ public static function DATETIMENOW() $dti = new DateTimeImmutable(); $dateArray = date_parse($dti->format('c')); - return self::returnIn3FormatsArray($dateArray); + return self::returnIn3FormatsArray($dateArray ?: []); } /** @@ -172,7 +172,7 @@ public static function DATENOW() $dti = new DateTimeImmutable(); $dateArray = date_parse($dti->format('c')); - return self::returnIn3FormatsArray($dateArray, true); + return self::returnIn3FormatsArray($dateArray ?: [], true); } /** @@ -421,6 +421,7 @@ public static function DATEVALUE($dateValue = 1) $yearFound = false; $t1 = explode(' ', $dateValue); + $t = ''; foreach ($t1 as &$t) { if ((is_numeric($t)) && ($t > 31)) { if ($yearFound) { @@ -486,19 +487,14 @@ public static function DATEVALUE($dateValue = 1) $day = (int) $PHPDateArray['day']; $year = (int) $PHPDateArray['year']; if (!checkdate($month, $day, $year)) { - return self::check19000229($year, $month, $day); + return ($year === 1900 && $month === 2 && $day === 29) ? self::returnIn3FormatsFloat(60.0) : Functions::VALUE(); } - $retValue = self::returnIn3FormatsArray($PHPDateArray, true); + $retValue = self::returnIn3FormatsArray($PHPDateArray ?: [], true); } return $retValue; } - private static function check19000229(int $year, int $month, int $day): string - { - return ($year === 1900 && $month === 2 && $day === 29) ? self::returnIn3FormatsFloat(60.0) : Functions::VALUE(); - } - /** * Help reduce perceived complexity of some tests. * @@ -1184,7 +1180,7 @@ public static function DAYOFMONTH($dateValue = 1) * Excel Function: * WEEKDAY(dateValue[,style]) * - * @param int $dateValue Excel date serial value (float), PHP date timestamp (integer), + * @param float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string * @param int $style A number that determines the type of return value * 1 or omitted Numbers 1 (Sunday) through 7 (Saturday). @@ -1219,7 +1215,6 @@ public static function WEEKDAY($dateValue = 1, $style = 1) self::silly1900($PHPDateObject); $DoW = (int) $PHPDateObject->format('w'); - $firstDay = 1; switch ($style) { case 1: ++$DoW; diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 1a8a2119f5..28c39255e4 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -160,8 +160,6 @@ public static function excelToDateTimeObject($excelTimestamp, $timeZone = null) { $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone); if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { - $baseYear = self::getExcelCalendar(); - //if ($excelTimestamp < ((self::$excelCalendar === self::CALENDAR_MAC_1904) ? 0 : 1)) { if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) { // Unix timestamp base date $baseDate = new \DateTime('1970-01-01', $timeZone); From e0d5f3e0e55c31701d4abe0455843eb94bba1b05 Mon Sep 17 00:00:00 2001 From: Owen Leibman Date: Sat, 20 Feb 2021 21:31:24 -0800 Subject: [PATCH 3/3] More Scrutinizer If at first you don't succeed ... --- src/PhpSpreadsheet/Calculation/DateTime.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index e051395a8a..64d72c2b58 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -147,7 +147,7 @@ public static function DATETIMENOW() $dti = new DateTimeImmutable(); $dateArray = date_parse($dti->format('c')); - return self::returnIn3FormatsArray($dateArray ?: []); + return is_array($dateArray) ? self::returnIn3FormatsArray($dateArray) : Functions::VALUE(); } /** @@ -172,7 +172,7 @@ public static function DATENOW() $dti = new DateTimeImmutable(); $dateArray = date_parse($dti->format('c')); - return self::returnIn3FormatsArray($dateArray ?: [], true); + return is_array($dateArray) ? self::returnIn3FormatsArray($dateArray, true) : Functions::VALUE(); } /** @@ -489,7 +489,7 @@ public static function DATEVALUE($dateValue = 1) if (!checkdate($month, $day, $year)) { return ($year === 1900 && $month === 2 && $day === 29) ? self::returnIn3FormatsFloat(60.0) : Functions::VALUE(); } - $retValue = self::returnIn3FormatsArray($PHPDateArray ?: [], true); + $retValue = is_array($PHPDateArray) ? self::returnIn3FormatsArray($PHPDateArray, true) : Functions::VALUE(); } return $retValue; @@ -1230,7 +1230,6 @@ public static function WEEKDAY($dateValue = 1, $style = 1) if ($DoW === 0) { $DoW = 7; } - $firstDay = 0; --$DoW; break;