diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index 4c2b108ad9..64d72c2b58 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 is_array($dateArray) ? self::returnIn3FormatsArray($dateArray) : Functions::VALUE(); } /** @@ -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 is_array($dateArray) ? self::returnIn3FormatsArray($dateArray, true) : Functions::VALUE(); } /** @@ -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); @@ -470,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) { @@ -481,10 +433,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 +446,7 @@ public static function DATEVALUE($dateValue = 1) $t1[1] += 1900; array_unshift($t1, 1); } else { - $t1[] = date('Y'); + $t1[] = $dti->format('Y'); } } } @@ -502,23 +455,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 +471,126 @@ 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 ($year === 1900 && $month === 2 && $day === 29) ? self::returnIn3FormatsFloat(60.0) : Functions::VALUE(); } - if (!checkdate($PHPDateArray['month'], $PHPDateArray['day'], $PHPDateArray['year'])) { - return Functions::VALUE(); + $retValue = is_array($PHPDateArray) ? self::returnIn3FormatsArray($PHPDateArray, true) : Functions::VALUE(); + } + + return $retValue; + } + + /** + * 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 +626,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 +996,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 +1129,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 +1150,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(); } @@ -1170,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). @@ -1182,6 +1192,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,19 +1202,19 @@ 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; switch ($style) { case 1: ++$DoW; @@ -1219,20 +1230,10 @@ public static function WEEKDAY($dateValue = 1, $style = 1) if ($DoW === 0) { $DoW = 7; } - $firstDay = 0; --$DoW; 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 +1299,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 +1331,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 +1350,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 +1378,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 +1479,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 +1515,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 +1551,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 +1605,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 +1647,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..28c39255e4 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -160,7 +160,7 @@ 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) { + 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, ], ];