Make Base Date a Property of Spreadsheet

This change is extracted from PR #2787 by @MarkBaker. That change mostly deals with array functions, and that part will be superseded by PR #3962. However, this part of 2787 is not included in 3962.

Fix #1036 (closed as stale in 2019 and just reopened). Excel spreadsheets can have either of 2 base dates, 1900 or 1904, and the numeric value of any date cells will vary depending on which base date is in use. PhpSpreadsheet has, till now, handled that as a static property of Shared/Date. This does not work well if two spreadsheets with different base dates are open simultaneously. The code is changed to store the base date as a property of the spreadsheet when an Xls/Xlsx spreadsheet is loaded, and use that property when saving an Xls/Xlsx spreadsheet. Any call to `getCalculatedValue` or `getFormattedValue` will temporarily set the Shared/Date value to that of the spreadsheet, and restore it at completion. In order to avoid a BC break, the Xls and Xlsx readers will continue to populate the Shared/Date value as before.
This commit is contained in:
oleibman
2024-06-22 22:09:22 -07:00
parent 318a82e0f9
commit 43589bc9b6
13 changed files with 312 additions and 13 deletions
+10 -1
View File
@@ -187,10 +187,15 @@ class Cell implements Stringable
*/
public function getFormattedValue(): string
{
return (string) NumberFormat::toFormattedString(
$currentCalendar = SharedDate::getExcelCalendar();
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
$formattedValue = (string) NumberFormat::toFormattedString(
$this->getCalculatedValue(),
(string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
);
SharedDate::setExcelCalendar($currentCalendar);
return $formattedValue;
}
protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void
@@ -364,6 +369,8 @@ class Cell implements Stringable
{
if ($this->dataType === DataType::TYPE_FORMULA) {
try {
$currentCalendar = SharedDate::getExcelCalendar();
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
$index = $this->getWorksheet()->getParentOrThrow()->getActiveSheetIndex();
$selected = $this->getWorksheet()->getSelectedCells();
$result = Calculation::getInstance(
@@ -379,6 +386,7 @@ class Cell implements Stringable
}
}
} catch (SpreadsheetException $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) {
@@ -391,6 +399,7 @@ class Cell implements Stringable
$ex
);
}
SharedDate::setExcelCalendar($currentCalendar);
if ($result === '#Not Yet Implemented') {
return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
+2
View File
@@ -1927,8 +1927,10 @@ class Xls extends BaseReader
// 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);
}
}
+2
View File
@@ -709,12 +709,14 @@ class Xlsx extends BaseReader
$xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
// Set base date
$excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
if ($xmlWorkbookNS->workbookPr) {
Date::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);
}
}
}
+5 -6
View File
@@ -10,7 +10,6 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Cell\Cell;
use PhpOffice\PhpSpreadsheet\Exception;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
class Date
@@ -64,15 +63,15 @@ class Date
/**
* Set the Excel calendar (Windows 1900 or Mac 1904).
*
* @param int $baseYear Excel base date (1900 or 1904)
* @param ?int $baseYear Excel base date (1900 or 1904)
*
* @return bool Success or failure
*/
public static function setExcelCalendar(int $baseYear): bool
public static function setExcelCalendar(?int $baseYear): bool
{
if (
($baseYear == self::CALENDAR_WINDOWS_1900)
|| ($baseYear == self::CALENDAR_MAC_1904)
($baseYear === self::CALENDAR_WINDOWS_1900)
|| ($baseYear === self::CALENDAR_MAC_1904)
) {
self::$excelCalendar = $baseYear;
@@ -173,7 +172,7 @@ class Date
throw new Exception("Invalid string $value supplied for datatype Date");
}
$newValue = SharedDate::PHPToExcel($date);
$newValue = self::PHPToExcel($date);
if ($newValue === false) {
throw new Exception("Invalid string $value supplied for datatype Date");
}
+25
View File
@@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
use PhpOffice\PhpSpreadsheet\Document\Properties;
use PhpOffice\PhpSpreadsheet\Document\Security;
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;
@@ -31,6 +32,8 @@ class Spreadsheet implements JsonSerializable
self::VISIBILITY_VERY_HIDDEN,
];
protected int $excelCalendar = Date::CALENDAR_WINDOWS_1900;
/**
* Unique ID.
*/
@@ -1553,4 +1556,26 @@ class Spreadsheet implements JsonSerializable
return $table;
}
/**
* @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 int Excel base date (1900 or 1904)
*/
public function getExcelCalendar(): int
{
return $this->excelCalendar;
}
}
+3 -3
View File
@@ -910,9 +910,9 @@ class Workbook extends BIFFwriter
$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);
+3 -3
View File
@@ -40,7 +40,7 @@ class Workbook extends WriterPart
$this->writeFileVersion($objWriter);
// workbookPr
$this->writeWorkbookPr($objWriter);
$this->writeWorkbookPr($objWriter, $spreadsheet);
// workbookProtection
$this->writeWorkbookProtection($objWriter, $spreadsheet);
@@ -81,11 +81,11 @@ class Workbook extends WriterPart
/**
* 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');
}
@@ -0,0 +1,127 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Reader\Xls;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PHPUnit\Framework\TestCase;
class DateReaderTest extends TestCase
{
protected function tearDown(): void
{
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
}
public function testReadExcel1900Spreadsheet(): void
{
$filename = 'tests/data/Reader/XLS/1900_Calendar.xls';
$reader = new Xls();
$spreadsheet = $reader->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());
$spreadsheet->disconnectWorksheets();
}
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());
$spreadsheet->disconnectWorksheets();
}
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());
$spreadsheet->disconnectWorksheets();
}
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());
$spreadsheet->disconnectWorksheets();
}
public function testSwitchCalendars(): void
{
$filename1904 = 'tests/data/Reader/XLS/1904_Calendar.xls';
$reader1904 = new Xls();
$spreadsheet1904 = $reader1904->load($filename1904);
$worksheet1904 = $spreadsheet1904->getActiveSheet();
$date1 = Date::convertIsoDate('2022-01-01');
self::assertSame(43100.0, $date1);
$filename1900 = 'tests/data/Reader/XLS/1900_Calendar.xls';
$reader1900 = new Xls();
$spreadsheet1900 = $reader1900->load($filename1900);
$worksheet1900 = $spreadsheet1900->getActiveSheet();
$date2 = Date::convertIsoDate('2022-01-01');
self::assertSame(44562.0, $date2);
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());
$spreadsheet1900->disconnectWorksheets();
$spreadsheet1904->disconnectWorksheets();
}
}
@@ -0,0 +1,135 @@
<?php
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Shared\Date;
use PHPUnit\Framework\TestCase;
class DateReaderTest extends TestCase
{
protected function tearDown(): void
{
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
}
public function testReadExcel1900Spreadsheet(): void
{
$filename = 'tests/data/Reader/XLSX/1900_Calendar.xlsx';
$reader = new Xlsx();
$spreadsheet = $reader->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());
$spreadsheet->disconnectWorksheets();
}
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());
$spreadsheet->disconnectWorksheets();
}
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());
$spreadsheet->disconnectWorksheets();
}
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());
$spreadsheet->disconnectWorksheets();
}
public function testSwitchCalendars(): void
{
$filename1904 = 'tests/data/Reader/XLSX/1904_Calendar.xlsx';
$reader1904 = new Xlsx();
$spreadsheet1904 = $reader1904->load($filename1904);
$worksheet1904 = $spreadsheet1904->getActiveSheet();
$date1 = Date::convertIsoDate('2022-01-01');
self::assertSame(43100.0, $date1);
$filename1900 = 'tests/data/Reader/XLSX/1900_Calendar.xlsx';
$reader1900 = new Xlsx();
$spreadsheet1900 = $reader1900->load($filename1900);
$worksheet1900 = $spreadsheet1900->getActiveSheet();
$date2 = Date::convertIsoDate('2022-01-01');
self::assertSame(44562.0, $date2);
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());
$spreadsheet1900->disconnectWorksheets();
$spreadsheet1904->disconnectWorksheets();
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.