From 95cf51b412abc643e005ed8a4b53cced0abaabbb Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sun, 10 Jul 2022 14:51:05 +0200 Subject: [PATCH 1/4] Modify the general settings ExcelCalendar to be simply a default, Create a new ExcelCalendar property for Spreadsheets, and modify the Xlsx and Xls Readers/Writers to maintain 1900 or 1904 calendar against the spreadsheet --- src/PhpSpreadsheet/Reader/Xls.php | 4 +-- src/PhpSpreadsheet/Reader/Xlsx.php | 4 +-- src/PhpSpreadsheet/Shared/Date.php | 4 +-- src/PhpSpreadsheet/Spreadsheet.php | 37 +++++++++++++++++++++ src/PhpSpreadsheet/Writer/Xls/Workbook.php | 6 ++-- src/PhpSpreadsheet/Writer/Xlsx/Workbook.php | 6 ++-- 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 9aee18ee18..98de1c029d 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -2026,9 +2026,9 @@ private function readDateMode(): void $this->pos += 4 + $length; // offset: 0; size: 2; 0 = base 1900, 1 = base 1904 - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->spreadsheet->setExcelCalendar(Date::CALENDAR_WINDOWS_1900); if (ord($recordData[0]) == 1) { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + $this->spreadsheet->setExcelCalendar(Date::CALENDAR_MAC_1904); } } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index f793a66ae6..8f88f29412 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -681,11 +681,11 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet // Set base date if ($xmlWorkbookNS->workbookPr) { - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900); $attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr); if (isset($attrs1904['date1904'])) { if (self::boolean((string) $attrs1904['date1904'])) { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + $excel->setExcelCalendar(Date::CALENDAR_MAC_1904); } } } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 8cac2c63e8..6c43bca4a3 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -67,7 +67,7 @@ class Date protected static $defaultTimeZone; /** - * Set the Excel calendar (Windows 1900 or Mac 1904). + * Set the Excel Default Calendar (Windows 1900 or Mac 1904). * * @param int $baseYear Excel base date (1900 or 1904) * @@ -85,7 +85,7 @@ public static function setExcelCalendar($baseYear) } /** - * Return the Excel calendar (Windows 1900 or Mac 1904). + * Return the Excel Default Calendar (Windows 1900 or Mac 1904). * * @return int Excel base date (1900 or 1904) */ diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 33b4fe0c4d..904a8437e4 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; @@ -52,6 +53,14 @@ class Spreadsheet */ private $workSheetCollection = []; + /** + * Base calendar year to use for calculations + * Value is either CALENDAR_WINDOWS_1900 (1900) or CALENDAR_MAC_1904 (1904). + * + * @var int + */ + protected $excelCalendar = Date::CALENDAR_WINDOWS_1900; + /** * Calculation Engine. * @@ -199,6 +208,34 @@ class Spreadsheet */ private $tabRatio = 600; + /** + * Set the Excel Calendar (Windows 1900 or Mac 1904). + * + * @param int $baseYear Excel base date (1900 or 1904) + * + * @return bool Success or failure + */ + public function setExcelCalendar(int $baseYear): bool + { + if (($baseYear == Date::CALENDAR_WINDOWS_1900) || ($baseYear == Date::CALENDAR_MAC_1904)) { + $this->excelCalendar = $baseYear; + + return true; + } + + return false; + } + + /** + * Return the Excel Calendar (Windows 1900 or Mac 1904). + * + * @return int Excel base date (1900 or 1904) + */ + public function getExcelCalendar(): int + { + return $this->excelCalendar; + } + /** * The workbook has macros ? * diff --git a/src/PhpSpreadsheet/Writer/Xls/Workbook.php b/src/PhpSpreadsheet/Writer/Xls/Workbook.php index ccffa181b6..876f2bded0 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xls/Workbook.php @@ -960,9 +960,9 @@ private function writeDateMode(): void $record = 0x0022; // Record identifier $length = 0x0002; // Bytes to follow - $f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904) - ? 1 - : 0; // Flag for 1904 date system + $f1904 = ($this->spreadsheet->getExcelCalendar() === Date::CALENDAR_MAC_1904) + ? 1 // Flag for 1904 date system + : 0; // Flag for 1900 date system $header = pack('vv', $record, $length); $data = pack('v', $f1904); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index f9d7197d7a..176039d660 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -39,7 +39,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, $recalcRequired = false) $this->writeFileVersion($objWriter); // workbookPr - $this->writeWorkbookPr($objWriter); + $this->writeWorkbookPr($objWriter, $spreadsheet); // workbookProtection $this->writeWorkbookProtection($objWriter, $spreadsheet); @@ -80,11 +80,11 @@ private function writeFileVersion(XMLWriter $objWriter): void /** * Write WorkbookPr. */ - private function writeWorkbookPr(XMLWriter $objWriter): void + private function writeWorkbookPr(XMLWriter $objWriter, Spreadsheet $spreadsheet): void { $objWriter->startElement('workbookPr'); - if (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904) { + if ($spreadsheet->getExcelCalendar() === Date::CALENDAR_MAC_1904) { $objWriter->writeAttribute('date1904', '1'); } From 39640877799f3d41cda66f89def6253d9db3548e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 13 Jul 2022 17:42:40 +0200 Subject: [PATCH 2/4] Test for calendar when reading Excel Files. Set Excel calendar to be used for calculations and formatting for the calendar stored against the spreadsheet. --- phpstan-baseline.neon | 10 -- src/PhpSpreadsheet/Cell/Cell.php | 21 +++- src/PhpSpreadsheet/Shared/Date.php | 5 +- .../Reader/Xls/DateReaderTest.php | 45 ++++++++ .../Reader/Xlsx/DateReaderTest.php | 103 ++++++++++++++++++ tests/data/Reader/XLS/1900_Calendar.xls | Bin 0 -> 28160 bytes tests/data/Reader/XLS/1904_Calendar.xls | Bin 0 -> 28160 bytes tests/data/Reader/XLSX/1900_Calendar.xlsx | Bin 0 -> 10134 bytes tests/data/Reader/XLSX/1904_Calendar.xlsx | Bin 0 -> 10119 bytes 9 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php create mode 100644 tests/data/Reader/XLS/1900_Calendar.xls create mode 100644 tests/data/Reader/XLS/1904_Calendar.xls create mode 100644 tests/data/Reader/XLSX/1900_Calendar.xlsx create mode 100644 tests/data/Reader/XLSX/1904_Calendar.xlsx diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 65789e1ebe..ffff21e48c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -990,11 +990,6 @@ parameters: count: 6 path: src/PhpSpreadsheet/Cell/Cell.php - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 1 - path: src/PhpSpreadsheet/Cell/Cell.php - - message: "#^Call to an undefined method object\\:\\:getHashCode\\(\\)\\.$#" count: 1 @@ -2580,11 +2575,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/OLERead.php - - - message: "#^Call to an undefined method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\Calculation\\:\\:_calculateFormulaValue\\(\\)\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/StringHelper.php - - message: "#^Static method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\TimeZone\\:\\:validateTimeZone\\(\\) is unused\\.$#" count: 1 diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 56b5f5f8ca..28c2ef2cd5 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -15,6 +15,7 @@ use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; +use function _PHPStan_59fb0a3b2\RingCentral\Psr7\str; class Cell { @@ -176,16 +177,22 @@ public function getValue() /** * Get cell value with formatting. - * - * @return string */ - public function getFormattedValue() + public function getFormattedValue(): string { - return (string) NumberFormat::toFormattedString( + $currentCalendar = SharedDate::getExcelCalendar(); + SharedDate::setExcelCalendar($this->getWorksheet()->getParent()->getExcelCalendar()); + + $formattedValue = (string) NumberFormat::toFormattedString( $this->getCalculatedValue(), $this->getStyle() ->getNumberFormat()->getFormatCode() ); + + SharedDate::setExcelCalendar($currentCalendar); + + return $formattedValue; + } /** @@ -342,8 +349,6 @@ public function setValueExplicit($value, $dataType, bool $isArrayFormula = false break; default: throw new Exception('Invalid datatype: ' . $dataType); - - break; } // set the datatype @@ -403,6 +408,8 @@ public function getCalculatedValue(bool $asArray = false, bool $resetLog = true) { if ($this->dataType === DataType::TYPE_FORMULA) { try { +// $currentCalendar = SharedDate::getExcelCalendar(); +// SharedDate::setExcelCalendar($this->getWorksheet()->getParent()->getExcelCalendar()); $coordinate = $this->getCoordinate(); $worksheet = $this->getWorksheet(); $value = $this->value; @@ -430,6 +437,7 @@ public function getCalculatedValue(bool $asArray = false, bool $resetLog = true) $this->getWorksheet()->setSelectedCells($selected); $this->getWorksheet()->getParent()->setActiveSheetIndex($index); } catch (Exception $ex) { +// SharedDate::setExcelCalendar($currentCalendar); if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) { return $this->calculatedValue; // Fallback for calculations referencing external files. } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) { @@ -441,6 +449,7 @@ public function getCalculatedValue(bool $asArray = false, bool $resetLog = true) ); } +// SharedDate::setExcelCalendar($currentCalendar); if ($result === '#Not Yet Implemented') { return $this->calculatedValue; // Fallback if calculation engine does not support the formula. } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 6c43bca4a3..547e875ca7 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -67,7 +67,7 @@ class Date protected static $defaultTimeZone; /** - * Set the Excel Default Calendar (Windows 1900 or Mac 1904). + * Set the Excel Date Calendar (Windows 1900 or Mac 1904) used for calculations and formatting. * * @param int $baseYear Excel base date (1900 or 1904) * @@ -85,7 +85,8 @@ public static function setExcelCalendar($baseYear) } /** - * Return the Excel Default Calendar (Windows 1900 or Mac 1904). + * Return the Excel Date Calendar (Windows 1900 or Mac 1904) + * to be used for current calculations and formatting. * * @return int Excel base date (1900 or 1904) */ diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php new file mode 100644 index 0000000000..30bbb660bc --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php @@ -0,0 +1,45 @@ +load($filename); + + self::assertSame(Date::CALENDAR_WINDOWS_1900, $spreadsheet->getExcelCalendar()); + + $worksheet = $spreadsheet->getActiveSheet(); + self::assertSame(44562, $worksheet->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet->getCell('A1')->getFormattedValue()); + self::assertSame(44926, $worksheet->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet->getCell('A2')->getFormattedValue()); + } + + public function testReadExcel1904Spreadsheet(): void + { + $filename = 'tests/data/Reader/XLS/1904_Calendar.xls'; + $reader = new Xls(); + $spreadsheet = $reader->load($filename); + + self::assertSame(Date::CALENDAR_MAC_1904, $spreadsheet->getExcelCalendar()); + + $worksheet = $spreadsheet->getActiveSheet(); + self::assertSame(43100, $worksheet->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet->getCell('A1')->getFormattedValue()); + self::assertSame(43464, $worksheet->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet->getCell('A2')->getFormattedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php new file mode 100644 index 0000000000..51e59d3bbb --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php @@ -0,0 +1,103 @@ +load($filename); + + self::assertSame(Date::CALENDAR_WINDOWS_1900, $spreadsheet->getExcelCalendar()); + + $worksheet = $spreadsheet->getActiveSheet(); + self::assertSame(44562, $worksheet->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet->getCell('A1')->getFormattedValue()); + self::assertSame(44926, $worksheet->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet->getCell('A2')->getFormattedValue()); + self::assertSame(44561, $worksheet->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet->getCell('B1')->getFormattedValue()); + self::assertSame(44927, $worksheet->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet->getCell('B2')->getFormattedValue()); + } + + public function testReadExcel1904Spreadsheet(): void + { + $filename = 'tests/data/Reader/XLSX/1904_Calendar.xlsx'; + $reader = new Xlsx(); + $spreadsheet = $reader->load($filename); + + self::assertSame(Date::CALENDAR_MAC_1904, $spreadsheet->getExcelCalendar()); + + $worksheet = $spreadsheet->getActiveSheet(); + self::assertSame(43100, $worksheet->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet->getCell('A1')->getFormattedValue()); + self::assertSame(43464, $worksheet->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet->getCell('A2')->getFormattedValue()); + self::assertSame(43099, $worksheet->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet->getCell('B1')->getFormattedValue()); + self::assertSame(43465, $worksheet->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet->getCell('B2')->getFormattedValue()); + } + + public function testSwitchCalendars(): void + { + $filename1900 = 'tests/data/Reader/XLSX/1900_Calendar.xlsx'; + $reader1900 = new Xlsx(); + $spreadsheet1900 = $reader1900->load($filename1900); + $worksheet1900 = $spreadsheet1900->getActiveSheet(); + + $filename1904 = 'tests/data/Reader/XLSX/1904_Calendar.xlsx'; + $reader1904 = new Xlsx(); + $spreadsheet1904 = $reader1904->load($filename1904); + $worksheet1904 = $spreadsheet1904->getActiveSheet(); + + self::assertSame(44562, $worksheet1900->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1900->getCell('A1')->getFormattedValue()); + self::assertSame(44926, $worksheet1900->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1900->getCell('A2')->getFormattedValue()); + self::assertSame(44561, $worksheet1900->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1900->getCell('B1')->getFormattedValue()); + self::assertSame(44927, $worksheet1900->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1900->getCell('B2')->getFormattedValue()); + + self::assertSame(43100, $worksheet1904->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1904->getCell('A1')->getFormattedValue()); + self::assertSame(43464, $worksheet1904->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1904->getCell('A2')->getFormattedValue()); + self::assertSame(43099, $worksheet1904->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1904->getCell('B1')->getFormattedValue()); + self::assertSame(43465, $worksheet1904->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1904->getCell('B2')->getFormattedValue()); + + // Check that accessing date values from one spreadsheet doesn't break accessing correct values from another + self::assertSame(44561, $worksheet1900->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1900->getCell('B1')->getFormattedValue()); + self::assertSame(44927, $worksheet1900->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1900->getCell('B2')->getFormattedValue()); + self::assertSame(44562, $worksheet1900->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1900->getCell('A1')->getFormattedValue()); + self::assertSame(44926, $worksheet1900->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1900->getCell('A2')->getFormattedValue()); + + self::assertSame(43099, $worksheet1904->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1904->getCell('B1')->getFormattedValue()); + self::assertSame(43465, $worksheet1904->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1904->getCell('B2')->getFormattedValue()); + self::assertSame(43100, $worksheet1904->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1904->getCell('A1')->getFormattedValue()); + self::assertSame(43464, $worksheet1904->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1904->getCell('A2')->getFormattedValue()); + } +} diff --git a/tests/data/Reader/XLS/1900_Calendar.xls b/tests/data/Reader/XLS/1900_Calendar.xls new file mode 100644 index 0000000000000000000000000000000000000000..69027c5b181674e7c79ffb46aa62743da445aae2 GIT binary patch literal 28160 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 literal 0 HcmV?d00001 diff --git a/tests/data/Reader/XLS/1904_Calendar.xls b/tests/data/Reader/XLS/1904_Calendar.xls new file mode 100644 index 0000000000000000000000000000000000000000..8f52d97cb6fa199e46c5ddafe34891932cb6e6fc GIT binary patch literal 28160 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%0D$yI_;qx0w=!}3 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 Q01!bxGLQ+&Q~bR9fAqm=g8%>k literal 0 HcmV?d00001 From 765037e31d62979acbaa1cfa13db46510200dc45 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 17 Aug 2022 16:41:14 +0200 Subject: [PATCH 3/4] Ensure that correct calendar (read from the spreadsheet when loaded) is used for all date-related calculations and formatting --- src/PhpSpreadsheet/Cell/Cell.php | 11 ++- .../Functions/DateTime/AllSetupTeardown.php | 8 +- .../Functions/DateTime/IsoWeekNumTest.php | 2 +- .../Functions/DateTime/WeekNumTest.php | 2 +- .../Reader/Xls/DateReaderTest.php | 72 ++++++++++++++++++ .../Reader/Xlsx/DateReaderTest.php | 22 ++++++ tests/data/Reader/XLS/1900_Calendar.xls | Bin 28160 -> 28160 bytes tests/data/Reader/XLS/1904_Calendar.xls | Bin 28160 -> 28160 bytes 8 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 28c2ef2cd5..e8594506f6 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -15,7 +15,6 @@ use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; -use function _PHPStan_59fb0a3b2\RingCentral\Psr7\str; class Cell { @@ -192,7 +191,6 @@ public function getFormattedValue(): string SharedDate::setExcelCalendar($currentCalendar); return $formattedValue; - } /** @@ -407,9 +405,10 @@ private function processArrayResult( public function getCalculatedValue(bool $asArray = false, bool $resetLog = true) { if ($this->dataType === DataType::TYPE_FORMULA) { + $currentCalendar = SharedDate::getExcelCalendar(); + SharedDate::setExcelCalendar($this->getWorksheet()->getParent()->getExcelCalendar()); + try { -// $currentCalendar = SharedDate::getExcelCalendar(); -// SharedDate::setExcelCalendar($this->getWorksheet()->getParent()->getExcelCalendar()); $coordinate = $this->getCoordinate(); $worksheet = $this->getWorksheet(); $value = $this->value; @@ -437,7 +436,7 @@ public function getCalculatedValue(bool $asArray = false, bool $resetLog = true) $this->getWorksheet()->setSelectedCells($selected); $this->getWorksheet()->getParent()->setActiveSheetIndex($index); } catch (Exception $ex) { -// SharedDate::setExcelCalendar($currentCalendar); + SharedDate::setExcelCalendar($currentCalendar); if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) { return $this->calculatedValue; // Fallback for calculations referencing external files. } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) { @@ -449,7 +448,7 @@ public function getCalculatedValue(bool $asArray = false, bool $resetLog = true) ); } -// SharedDate::setExcelCalendar($currentCalendar); + SharedDate::setExcelCalendar($currentCalendar); if ($result === '#Not Yet Implemented') { return $this->calculatedValue; // Fallback if calculation engine does not support the formula. } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php index 9fb5dfa58b..0cb3b62bd2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php @@ -55,9 +55,13 @@ protected function tearDown(): void } } - protected static function setMac1904(): void + protected static function setMac1904(?Worksheet $worksheet = null): void { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + if ($worksheet === null) { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + } else { + $worksheet->getParent()->setExcelCalendar(Date::CALENDAR_MAC_1904); + } } protected static function setUnixReturn(): void diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php index a6615ff623..86b1389ba4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php @@ -35,8 +35,8 @@ public function providerISOWEEKNUM(): array public function testISOWEEKNUM1904($expectedResult, $dateValue): void { $this->mightHaveException($expectedResult); - self::setMac1904(); $sheet = $this->getSheet(); + self::setMac1904($sheet); $sheet->getCell('A1')->setValue("=ISOWEEKNUM($dateValue)"); $sheet->getCell('B1')->setValue('1954-11-23'); self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php index 65b00516e0..c1c2fe774e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php @@ -33,8 +33,8 @@ public function providerWEEKNUM(): array public function testWEEKNUM1904($expectedResult, string $formula): void { $this->mightHaveException($expectedResult); - self::setMac1904(); $sheet = $this->getSheet(); + self::setMac1904($sheet); $sheet->getCell('B1')->setValue('1954-11-23'); $sheet->getCell('A1')->setValue("=WEEKNUM($formula)"); self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); diff --git a/tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php b/tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php index 30bbb660bc..5ba83166bc 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php @@ -42,4 +42,76 @@ public function testReadExcel1904Spreadsheet(): void self::assertSame(43464, $worksheet->getCell('A2')->getValue()); self::assertSame('2022-12-31', $worksheet->getCell('A2')->getFormattedValue()); } + + public function testNewDateInLoadedExcel1900Spreadsheet(): void + { + $filename = 'tests/data/Reader/XLS/1900_Calendar.xls'; + $reader = new Xls(); + $spreadsheet = $reader->load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->getCell('A4')->setValue('=DATE(2023,1,1)'); + self::assertEquals(44927, $worksheet->getCell('A4')->getCalculatedValue()); + } + + public function testNewDateInLoadedExcel1904Spreadsheet(): void + { + $filename = 'tests/data/Reader/XLS/1904_Calendar.xls'; + $reader = new Xls(); + $spreadsheet = $reader->load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->getCell('A4')->setValue('=DATE(2023,1,1)'); + self::assertEquals(43465, $worksheet->getCell('A4')->getCalculatedValue()); + } + + public function testSwitchCalendars(): void + { + $filename1900 = 'tests/data/Reader/XLS/1900_Calendar.xls'; + $reader1900 = new Xls(); + $spreadsheet1900 = $reader1900->load($filename1900); + $worksheet1900 = $spreadsheet1900->getActiveSheet(); + + $filename1904 = 'tests/data/Reader/XLS/1904_Calendar.xls'; + $reader1904 = new Xls(); + $spreadsheet1904 = $reader1904->load($filename1904); + $worksheet1904 = $spreadsheet1904->getActiveSheet(); + + self::assertSame(44562, $worksheet1900->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1900->getCell('A1')->getFormattedValue()); + self::assertSame(44926, $worksheet1900->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1900->getCell('A2')->getFormattedValue()); + self::assertSame(44561, $worksheet1900->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1900->getCell('B1')->getFormattedValue()); + self::assertSame(44927, $worksheet1900->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1900->getCell('B2')->getFormattedValue()); + + self::assertSame(43100, $worksheet1904->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1904->getCell('A1')->getFormattedValue()); + self::assertSame(43464, $worksheet1904->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1904->getCell('A2')->getFormattedValue()); + self::assertSame(43099, $worksheet1904->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1904->getCell('B1')->getFormattedValue()); + self::assertSame(43465, $worksheet1904->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1904->getCell('B2')->getFormattedValue()); + + // Check that accessing date values from one spreadsheet doesn't break accessing correct values from another + self::assertSame(44561, $worksheet1900->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1900->getCell('B1')->getFormattedValue()); + self::assertSame(44927, $worksheet1900->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1900->getCell('B2')->getFormattedValue()); + self::assertSame(44562, $worksheet1900->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1900->getCell('A1')->getFormattedValue()); + self::assertSame(44926, $worksheet1900->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1900->getCell('A2')->getFormattedValue()); + + self::assertSame(43099, $worksheet1904->getCell('B1')->getCalculatedValue()); + self::assertSame('2021-12-31', $worksheet1904->getCell('B1')->getFormattedValue()); + self::assertSame(43465, $worksheet1904->getCell('B2')->getCalculatedValue()); + self::assertSame('2023-01-01', $worksheet1904->getCell('B2')->getFormattedValue()); + self::assertSame(43100, $worksheet1904->getCell('A1')->getValue()); + self::assertSame('2022-01-01', $worksheet1904->getCell('A1')->getFormattedValue()); + self::assertSame(43464, $worksheet1904->getCell('A2')->getValue()); + self::assertSame('2022-12-31', $worksheet1904->getCell('A2')->getFormattedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php index 51e59d3bbb..042683f9c3 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/DateReaderTest.php @@ -51,6 +51,28 @@ public function testReadExcel1904Spreadsheet(): void self::assertSame('2023-01-01', $worksheet->getCell('B2')->getFormattedValue()); } + public function testNewDateInLoadedExcel1900Spreadsheet(): void + { + $filename = 'tests/data/Reader/XLSX/1900_Calendar.xlsx'; + $reader = new Xlsx(); + $spreadsheet = $reader->load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->getCell('A4')->setValue('=DATE(2023,1,1)'); + self::assertEquals(44927, $worksheet->getCell('A4')->getCalculatedValue()); + } + + public function testNewDateInLoadedExcel1904Spreadsheet(): void + { + $filename = 'tests/data/Reader/XLSX/1904_Calendar.xlsx'; + $reader = new Xlsx(); + $spreadsheet = $reader->load($filename); + + $worksheet = $spreadsheet->getActiveSheet(); + $worksheet->getCell('A4')->setValue('=DATE(2023,1,1)'); + self::assertEquals(43465, $worksheet->getCell('A4')->getCalculatedValue()); + } + public function testSwitchCalendars(): void { $filename1900 = 'tests/data/Reader/XLSX/1900_Calendar.xlsx'; diff --git a/tests/data/Reader/XLS/1900_Calendar.xls b/tests/data/Reader/XLS/1900_Calendar.xls index 69027c5b181674e7c79ffb46aa62743da445aae2..714670a7dc10c2236f73d4e9fc33e28372a894f2 100644 GIT binary patch delta 727 zcmZ{gL1+^}6o&uVolUpd#7$xv)1q-LrI(g&do3jmA{cwhr57)P;LVc=LZvo%tT$h2 zZasvaS_^GfX}oCkBwj=iETxA~+JmPcC|E)3o2gryNZ#^h|M}kg=gn?;Pn7q>X5FN5 zGeYmI0V;oDt!(1mZEPe?p1nW#FVg-p(^2KlhJ&KsnpNQ$WXHm*!%= zNYWawDcX(s^u@{2m3UV4(PVsDu2eJY!F>FVZ%kbe4yK+t)ANWy@G~2A_1}k~h62Xv zY8#2a+7ns`7mBHkL9dJbw<79Gc%Rp0@9kj$XbxTY)r)IP8aV;^$cSn~9*x#vK^fH0 z6GM}Ua*E$<;8T^s{0tk54AYpPuf_8t%@**)PH?K$F!o!Q;KEeH!xV!28xNn(ys_UO b7bj~w!VEsn_%0>#cJ1bmlvoXp?)Hd3wPl-x delta 639 zcmZ{f!7BuD6vyB1H^12(J3BkG8$zsUdr0he=Of)!)adsiMVp8(qAorVc2cAkT?!A1;QVjs`{$&E}bK?~| zHH%OW|M@`;p0B~(8hp2!4^(}8EFM(8!{3Fr1=q`bBOK1ESru4D@G#D+3s|Q$r2^%S z$|*U~;V<)RNOIc_a3;9&mp7Jq-7hx*oTh*LjW>w;nWJ&fH%2GeMS_?MS|o&bTzweO zEE2(9t)JyrZoysBrY-t3(FOAY!OP+1$z>Vo7xxMNSOkDe>fJ)I|+ XTOzqvqn3__t?W&GM97%?Ip-(e9*BrD diff --git a/tests/data/Reader/XLS/1904_Calendar.xls b/tests/data/Reader/XLS/1904_Calendar.xls index 8f52d97cb6fa199e46c5ddafe34891932cb6e6fc..cde7fc1931da9196e01661b390f0469aef06cc6a 100644 GIT binary patch delta 265 zcmZp;!`N_#al<1UCL8z7Pi)q3+t)JiFfamPC>K8i8v_t9@qtJNCOFB#BmiYAFoGG3 z{0ty55Qk~ Date: Wed, 17 Aug 2022 17:57:06 +0200 Subject: [PATCH 4/4] Fix merge from 2.0 for the Spreadsheet Copy (I really don't like this approach, it shouldn't be necessary to save to a temporary file and then restore, a deep clone should be the correct approach) --- src/PhpSpreadsheet/Spreadsheet.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 79d4141baf..ab2196e8b1 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -3,11 +3,14 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet {