diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e36a8d4b0..ad246e48c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a ### Fixed -- Nothing yet. +- Unexpected Exception in Php DateTime. [Issue #4696](https://github.com/PHPOffice/PhpSpreadsheet/issues/4696) [Issue #917](https://github.com/PHPOffice/PhpSpreadsheet/issues/917) [PR #4697](https://github.com/PHPOffice/PhpSpreadsheet/pull/4697) ## 2025-10-25 - 5.2.0 diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php index ee1d4866c1..492447dd0a 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; +use Throwable; class Helpers { @@ -56,6 +57,12 @@ public static function getDateValue(mixed $dateValue, bool $allowBool = true): f throw new Exception(ExcelError::NAN()); } + try { + SharedDateHelper::excelToDateTimeObject((float) $dateValue); + } catch (Throwable) { + throw new Exception(ExcelError::NAN()); + } + return (float) $dateValue; } diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php index a7f415d331..86007d4b58 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php @@ -4,7 +4,9 @@ use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; +use Throwable; class TimeParts { @@ -44,6 +46,11 @@ public static function hour(mixed $timeValue): array|string|int } // Execute function + try { + SharedDateHelper::excelToDateTimeObject($timeValue); + } catch (Throwable) { + return ExcelError::NAN(); + } $timeValue = fmod($timeValue, 1); $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); SharedDateHelper::roundMicroseconds($timeValue); @@ -85,6 +92,11 @@ public static function minute(mixed $timeValue): array|string|int } // Execute function + try { + SharedDateHelper::excelToDateTimeObject($timeValue); + } catch (Throwable) { + return ExcelError::NAN(); + } $timeValue = fmod($timeValue, 1); $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); SharedDateHelper::roundMicroseconds($timeValue); @@ -126,6 +138,11 @@ public static function second(mixed $timeValue): array|string|int } // Execute function + try { + SharedDateHelper::excelToDateTimeObject($timeValue); + } catch (Throwable) { + return ExcelError::NAN(); + } $timeValue = fmod($timeValue, 1); $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); SharedDateHelper::roundMicroseconds($timeValue); diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 3dc16964b6..d30f0ed942 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -11,6 +11,7 @@ use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use Throwable; class Date { @@ -376,15 +377,18 @@ public static function isDateTime(Cell $cell, mixed $value = null, bool $dateWit $cell->getCalculatedValue() ); } - $result = is_numeric($value) - && self::isDateTimeFormat( + if (is_numeric($value)) { + $result = self::isDateTimeFormat( $worksheet->getStyle( $cell->getCoordinate() )->getNumberFormat(), $dateWithoutTimeOkay ); - } catch (Exception) { - // Result is already false, so no need to actually do anything here + /** @var float|int $value */ + self::excelToDateTimeObject($value); + } + } catch (Throwable) { + $result = false; } $worksheet->setSelectedCells($selected); $spreadsheet->setActiveSheetIndex($index); diff --git a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php index 6630888599..8b87062aa0 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php @@ -3,6 +3,8 @@ namespace PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; +use Throwable; class DateFormatter { @@ -161,7 +163,11 @@ public static function format(mixed $value, string $format): string $callback = [self::class, 'escapeQuotesCallback']; $format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format); - $dateObj = Date::excelToDateTimeObject($value); + try { + $dateObj = Date::excelToDateTimeObject($value); + } catch (Throwable) { + return StringHelper::convertToString($value); + } // If the colon preceding minute had been quoted, as happens in // Excel 2003 XML formats, m will not have been changed to i above. // Change it now. diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 9074288a2f..63f4e69170 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -15,6 +15,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter\Column\Rule; use Stringable; +use Throwable; class AutoFilter implements Stringable { @@ -339,7 +340,12 @@ protected static function filterTestInDateGroupSet(mixed $cellValue, array $data $timeZone = new DateTimeZone('UTC'); if (is_numeric($cellValue)) { - $dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone); + try { + $dateTime = Date::excelToDateTimeObject((float) $cellValue, $timeZone); + } catch (Throwable) { + return false; + } + $cellValue = (float) $cellValue; if ($cellValue < 1) { // Just the time part @@ -489,7 +495,12 @@ protected static function filterTestInPeriodDateSet(mixed $cellValue, array $mon } if (is_numeric($cellValue)) { - $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC')); + try { + $dateObject = Date::excelToDateTimeObject((float) $cellValue, new DateTimeZone('UTC')); + } catch (Throwable) { + return false; + } + $dateValue = (int) $dateObject->format('m'); if (in_array($dateValue, $monthSet)) { return true; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/HelpersTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/HelpersTest.php new file mode 100644 index 0000000000..4a6c07492f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/HelpersTest.php @@ -0,0 +1,19 @@ +expectException(CalcExp::class); + $this->expectExceptionMessage('#VALUE!'); + Helpers::getDateValue($this); + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Issue4696Test.php b/tests/PhpSpreadsheetTests/Shared/Issue4696Test.php new file mode 100644 index 0000000000..3878984067 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Issue4696Test.php @@ -0,0 +1,103 @@ +spreadsheet !== null) { + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + } + } + + #[DataProvider('providerIsDateTime')] + public function testIsDateTime(bool $expectedResult, string $expectedFormatted, int|float|string $value): void + { + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + if (is_string($value) && $value[0] !== '=') { + $sheet->getCell('A1')->setValueExplicit($value, DataType::TYPE_STRING); + } else { + $sheet->getCell('A1')->setValue($value); + } + $sheet->getStyle('A1')->getNumberFormat() + ->setFormatCode('yyyy-mm-dd'); + self::assertSame( + $expectedResult, + Date::isDateTime($sheet->getCell('A1')) + ); + self::assertSame( + $expectedFormatted, + $sheet->getCell('A1')->getFormattedValue() + ); + } + + public static function providerIsDateTime(): array + { + return [ + 'valid integer' => [true, '1903-12-31', 1461], + 'valid integer stored as string' => [true, '1904-01-01', '1462'], + 'valid integer stored as concatenated string' => [true, '1904-01-01', '="14"&"62"'], + 'valid float' => [true, '1903-12-31', 1461.5], + 'valid float stored as string' => [true, '1903-12-31', '1461.5'], + 'out-of-range integer' => [false, '7000989091802000122', 7000989091802000122], + 'out-of-range float' => [false, '7.000989091802E+18', 7000989091802000122.1], + 'out-of-range float stored as string' => [false, '7000989091802000122.1', '7000989091802000122.1'], + 'non-numeric' => [false, 'xyz', 'xyz'], + 'issue 917' => [false, '5e8630b8-603c-43fe-b038-6154a3f893ab', '5e8630b8-603c-43fe-b038-6154a3f893ab'], + ]; + } + + #[DataProvider('providerOtherFunctions')] + public function testOtherFunctions(string $function): void + { + $this->spreadsheet = new Spreadsheet(); + $sheet = $this->spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue(7000989091802000122); + $sheet->getCell('A3')->setValue(39107); // 2007-01-25 + $sheet->getCell('A4')->setValue(39767); // 2008-11-15 + $sheet->getCell('A5')->setValue(2); + $sheet->getCell('A6')->setValue(1); + $sheet->getCell('B1')->setValue($function); + self::assertSame( + '#NUM!', + $sheet->getCell('B1')->getFormattedValue() + ); + } + + public static function providerOtherFunctions(): array + { + return [ + ['=YEAR(A1)'], + ['=MONTH(A1)'], + ['=DAY(A1)'], + ['=DAYS(A1,A1)'], + ['=DAYS360(A1,A1)'], + ['=DATEDIF(A1,A1,"D")'], + ['=HOUR(A1)'], + ['=MINUTE(A1)'], + ['=SECOND(A1)'], + ['=WEEKNUM(A1)'], + ['=ISOWEEKNUM(A1)'], + ['=WEEKDAY(A1)'], + ['=COUPDAYBS(A1,A4,A5,A6)'], + ['=COUPDAYS(A3,A2,A5,A6)'], + ['=COUPDAYSNC(A3,A2,A5,A6)'], + ['=COUPNCD(A3,A2,A5,A6)'], + ['=COUPNUM(A3,A2,A5,A6)'], + ['=COUPPCD(A3,A2,A5,A6)'], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php index c1e7c9ad44..3e91e30078 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/AutoFilterYearTest.php @@ -47,6 +47,8 @@ public function testYears(array $expectedVisible, string $rule): void } ++$row; $sheet->getCell("A$row")->setValue('=DATE(2041, 1, 1)'); // beyond epoch + ++$row; + $sheet->getCell("A$row")->setValue(7000989091802000122); // issue 4696 ++$row; // empty row at end $this->maxRow = $maxRow = $row; $autoFilter = $sheet->getAutoFilter(); diff --git a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/DateGroupTest.php b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/DateGroupTest.php index c06cb633fb..1460856328 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/DateGroupTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/AutoFilter/DateGroupTest.php @@ -54,6 +54,28 @@ public function testYearMonthDayGroup(): void self::assertEquals([6], $this->getVisible()); } + public function testIssue4696(): void + { + $year = 2011; + $sheet = $this->initSheet($year); + $sheet->getCell('A2')->setValue(7000989091802000122); + $columnFilter = $sheet->getAutoFilter()->getColumn('C'); + $columnFilter->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); + $columnFilter->createRule() + ->setRule( + Rule::AUTOFILTER_COLUMN_RULE_EQUAL, + [ + 'year' => $year, + 'month' => 12, + 'day' => 6, + ] + ) + ->setRuleType( + Rule::AUTOFILTER_RULETYPE_DATEGROUP + ); + self::assertEquals([6], $this->getVisible()); + } + public function testYearMonthDayHourMinuteSecond1Group(): void { $year = 2011;