Skip to content

Commit 43589bc

Browse files
committed
Make Base Date a Property of Spreadsheet
This change is extracted from PR PHPOffice#2787 by @MarkBaker. That change mostly deals with array functions, and that part will be superseded by PR PHPOffice#3962. However, this part of 2787 is not included in 3962. Fix PHPOffice#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.
1 parent 318a82e commit 43589bc

File tree

13 files changed

+312
-13
lines changed

13 files changed

+312
-13
lines changed

src/PhpSpreadsheet/Cell/Cell.php

+10-1
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,15 @@ public function getValueString(): string
187187
*/
188188
public function getFormattedValue(): string
189189
{
190-
return (string) NumberFormat::toFormattedString(
190+
$currentCalendar = SharedDate::getExcelCalendar();
191+
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
192+
$formattedValue = (string) NumberFormat::toFormattedString(
191193
$this->getCalculatedValue(),
192194
(string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
193195
);
196+
SharedDate::setExcelCalendar($currentCalendar);
197+
198+
return $formattedValue;
194199
}
195200

196201
protected static function updateIfCellIsTableHeader(?Worksheet $workSheet, self $cell, mixed $oldValue, mixed $newValue): void
@@ -364,6 +369,8 @@ public function getCalculatedValue(bool $resetLog = true): mixed
364369
{
365370
if ($this->dataType === DataType::TYPE_FORMULA) {
366371
try {
372+
$currentCalendar = SharedDate::getExcelCalendar();
373+
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
367374
$index = $this->getWorksheet()->getParentOrThrow()->getActiveSheetIndex();
368375
$selected = $this->getWorksheet()->getSelectedCells();
369376
$result = Calculation::getInstance(
@@ -379,6 +386,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
379386
}
380387
}
381388
} catch (SpreadsheetException $ex) {
389+
SharedDate::setExcelCalendar($currentCalendar);
382390
if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) {
383391
return $this->calculatedValue; // Fallback for calculations referencing external files.
384392
} elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) {
@@ -391,6 +399,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
391399
$ex
392400
);
393401
}
402+
SharedDate::setExcelCalendar($currentCalendar);
394403

395404
if ($result === '#Not Yet Implemented') {
396405
return $this->calculatedValue; // Fallback if calculation engine does not support the formula.

src/PhpSpreadsheet/Reader/Xls.php

+2
Original file line numberDiff line numberDiff line change
@@ -1927,8 +1927,10 @@ private function readDateMode(): void
19271927

19281928
// offset: 0; size: 2; 0 = base 1900, 1 = base 1904
19291929
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
1930+
$this->spreadsheet->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
19301931
if (ord($recordData[0]) == 1) {
19311932
Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
1933+
$this->spreadsheet->setExcelCalendar(Date::CALENDAR_MAC_1904);
19321934
}
19331935
}
19341936

src/PhpSpreadsheet/Reader/Xlsx.php

+2
Original file line numberDiff line numberDiff line change
@@ -709,12 +709,14 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
709709
$xmlWorkbookNS = $this->loadZip($relTarget, $mainNS);
710710

711711
// Set base date
712+
$excel->setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
712713
if ($xmlWorkbookNS->workbookPr) {
713714
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
714715
$attrs1904 = self::getAttributes($xmlWorkbookNS->workbookPr);
715716
if (isset($attrs1904['date1904'])) {
716717
if (self::boolean((string) $attrs1904['date1904'])) {
717718
Date::setExcelCalendar(Date::CALENDAR_MAC_1904);
719+
$excel->setExcelCalendar(Date::CALENDAR_MAC_1904);
718720
}
719721
}
720722
}

src/PhpSpreadsheet/Shared/Date.php

+5-6
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
use PhpOffice\PhpSpreadsheet\Cell\Cell;
1111
use PhpOffice\PhpSpreadsheet\Exception;
1212
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
13-
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate;
1413
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
1514

1615
class Date
@@ -64,15 +63,15 @@ class Date
6463
/**
6564
* Set the Excel calendar (Windows 1900 or Mac 1904).
6665
*
67-
* @param int $baseYear Excel base date (1900 or 1904)
66+
* @param ?int $baseYear Excel base date (1900 or 1904)
6867
*
6968
* @return bool Success or failure
7069
*/
71-
public static function setExcelCalendar(int $baseYear): bool
70+
public static function setExcelCalendar(?int $baseYear): bool
7271
{
7372
if (
74-
($baseYear == self::CALENDAR_WINDOWS_1900)
75-
|| ($baseYear == self::CALENDAR_MAC_1904)
73+
($baseYear === self::CALENDAR_WINDOWS_1900)
74+
|| ($baseYear === self::CALENDAR_MAC_1904)
7675
) {
7776
self::$excelCalendar = $baseYear;
7877

@@ -173,7 +172,7 @@ public static function convertIsoDate(mixed $value): float|int
173172
throw new Exception("Invalid string $value supplied for datatype Date");
174173
}
175174

176-
$newValue = SharedDate::PHPToExcel($date);
175+
$newValue = self::PHPToExcel($date);
177176
if ($newValue === false) {
178177
throw new Exception("Invalid string $value supplied for datatype Date");
179178
}

src/PhpSpreadsheet/Spreadsheet.php

+25
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PhpOffice\PhpSpreadsheet\Document\Properties;
88
use PhpOffice\PhpSpreadsheet\Document\Security;
99
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
10+
use PhpOffice\PhpSpreadsheet\Shared\Date;
1011
use PhpOffice\PhpSpreadsheet\Shared\File;
1112
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
1213
use PhpOffice\PhpSpreadsheet\Style\Style;
@@ -31,6 +32,8 @@ class Spreadsheet implements JsonSerializable
3132
self::VISIBILITY_VERY_HIDDEN,
3233
];
3334

35+
protected int $excelCalendar = Date::CALENDAR_WINDOWS_1900;
36+
3437
/**
3538
* Unique ID.
3639
*/
@@ -1553,4 +1556,26 @@ public function getTableByName(string $tableName): ?Table
15531556

15541557
return $table;
15551558
}
1559+
1560+
/**
1561+
* @return bool Success or failure
1562+
*/
1563+
public function setExcelCalendar(int $baseYear): bool
1564+
{
1565+
if (($baseYear === Date::CALENDAR_WINDOWS_1900) || ($baseYear === Date::CALENDAR_MAC_1904)) {
1566+
$this->excelCalendar = $baseYear;
1567+
1568+
return true;
1569+
}
1570+
1571+
return false;
1572+
}
1573+
1574+
/**
1575+
* @return int Excel base date (1900 or 1904)
1576+
*/
1577+
public function getExcelCalendar(): int
1578+
{
1579+
return $this->excelCalendar;
1580+
}
15561581
}

src/PhpSpreadsheet/Writer/Xls/Workbook.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -910,9 +910,9 @@ private function writeDateMode(): void
910910
$record = 0x0022; // Record identifier
911911
$length = 0x0002; // Bytes to follow
912912

913-
$f1904 = (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904)
914-
? 1
915-
: 0; // Flag for 1904 date system
913+
$f1904 = ($this->spreadsheet->getExcelCalendar() === Date::CALENDAR_MAC_1904)
914+
? 1 // Flag for 1904 date system
915+
: 0; // Flag for 1900 date system
916916

917917
$header = pack('vv', $record, $length);
918918
$data = pack('v', $f1904);

src/PhpSpreadsheet/Writer/Xlsx/Workbook.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $recalcRequired = f
4040
$this->writeFileVersion($objWriter);
4141

4242
// workbookPr
43-
$this->writeWorkbookPr($objWriter);
43+
$this->writeWorkbookPr($objWriter, $spreadsheet);
4444

4545
// workbookProtection
4646
$this->writeWorkbookProtection($objWriter, $spreadsheet);
@@ -81,11 +81,11 @@ private function writeFileVersion(XMLWriter $objWriter): void
8181
/**
8282
* Write WorkbookPr.
8383
*/
84-
private function writeWorkbookPr(XMLWriter $objWriter): void
84+
private function writeWorkbookPr(XMLWriter $objWriter, Spreadsheet $spreadsheet): void
8585
{
8686
$objWriter->startElement('workbookPr');
8787

88-
if (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904) {
88+
if ($spreadsheet->getExcelCalendar() === Date::CALENDAR_MAC_1904) {
8989
$objWriter->writeAttribute('date1904', '1');
9090
}
9191

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xls;
4+
5+
use PhpOffice\PhpSpreadsheet\Reader\Xls;
6+
use PhpOffice\PhpSpreadsheet\Shared\Date;
7+
use PHPUnit\Framework\TestCase;
8+
9+
class DateReaderTest extends TestCase
10+
{
11+
protected function tearDown(): void
12+
{
13+
Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900);
14+
}
15+
16+
public function testReadExcel1900Spreadsheet(): void
17+
{
18+
$filename = 'tests/data/Reader/XLS/1900_Calendar.xls';
19+
$reader = new Xls();
20+
$spreadsheet = $reader->load($filename);
21+
22+
self::assertSame(Date::CALENDAR_WINDOWS_1900, $spreadsheet->getExcelCalendar());
23+
24+
$worksheet = $spreadsheet->getActiveSheet();
25+
self::assertSame(44562, $worksheet->getCell('A1')->getValue());
26+
self::assertSame('2022-01-01', $worksheet->getCell('A1')->getFormattedValue());
27+
self::assertSame(44926, $worksheet->getCell('A2')->getValue());
28+
self::assertSame('2022-12-31', $worksheet->getCell('A2')->getFormattedValue());
29+
$spreadsheet->disconnectWorksheets();
30+
}
31+
32+
public function testReadExcel1904Spreadsheet(): void
33+
{
34+
$filename = 'tests/data/Reader/XLS/1904_Calendar.xls';
35+
$reader = new Xls();
36+
$spreadsheet = $reader->load($filename);
37+
38+
self::assertSame(Date::CALENDAR_MAC_1904, $spreadsheet->getExcelCalendar());
39+
40+
$worksheet = $spreadsheet->getActiveSheet();
41+
self::assertSame(43100, $worksheet->getCell('A1')->getValue());
42+
self::assertSame('2022-01-01', $worksheet->getCell('A1')->getFormattedValue());
43+
self::assertSame(43464, $worksheet->getCell('A2')->getValue());
44+
self::assertSame('2022-12-31', $worksheet->getCell('A2')->getFormattedValue());
45+
$spreadsheet->disconnectWorksheets();
46+
}
47+
48+
public function testNewDateInLoadedExcel1900Spreadsheet(): void
49+
{
50+
$filename = 'tests/data/Reader/XLS/1900_Calendar.xls';
51+
$reader = new Xls();
52+
$spreadsheet = $reader->load($filename);
53+
54+
$worksheet = $spreadsheet->getActiveSheet();
55+
$worksheet->getCell('A4')->setValue('=DATE(2023,1,1)');
56+
self::assertEquals(44927, $worksheet->getCell('A4')->getCalculatedValue());
57+
$spreadsheet->disconnectWorksheets();
58+
}
59+
60+
public function testNewDateInLoadedExcel1904Spreadsheet(): void
61+
{
62+
$filename = 'tests/data/Reader/XLS/1904_Calendar.xls';
63+
$reader = new Xls();
64+
$spreadsheet = $reader->load($filename);
65+
66+
$worksheet = $spreadsheet->getActiveSheet();
67+
$worksheet->getCell('A4')->setValue('=DATE(2023,1,1)');
68+
self::assertEquals(43465, $worksheet->getCell('A4')->getCalculatedValue());
69+
$spreadsheet->disconnectWorksheets();
70+
}
71+
72+
public function testSwitchCalendars(): void
73+
{
74+
$filename1904 = 'tests/data/Reader/XLS/1904_Calendar.xls';
75+
$reader1904 = new Xls();
76+
$spreadsheet1904 = $reader1904->load($filename1904);
77+
$worksheet1904 = $spreadsheet1904->getActiveSheet();
78+
$date1 = Date::convertIsoDate('2022-01-01');
79+
self::assertSame(43100.0, $date1);
80+
81+
$filename1900 = 'tests/data/Reader/XLS/1900_Calendar.xls';
82+
$reader1900 = new Xls();
83+
$spreadsheet1900 = $reader1900->load($filename1900);
84+
$worksheet1900 = $spreadsheet1900->getActiveSheet();
85+
$date2 = Date::convertIsoDate('2022-01-01');
86+
self::assertSame(44562.0, $date2);
87+
88+
self::assertSame(44562, $worksheet1900->getCell('A1')->getValue());
89+
self::assertSame('2022-01-01', $worksheet1900->getCell('A1')->getFormattedValue());
90+
self::assertSame(44926, $worksheet1900->getCell('A2')->getValue());
91+
self::assertSame('2022-12-31', $worksheet1900->getCell('A2')->getFormattedValue());
92+
self::assertSame(44561, $worksheet1900->getCell('B1')->getCalculatedValue());
93+
self::assertSame('2021-12-31', $worksheet1900->getCell('B1')->getFormattedValue());
94+
self::assertSame(44927, $worksheet1900->getCell('B2')->getCalculatedValue());
95+
self::assertSame('2023-01-01', $worksheet1900->getCell('B2')->getFormattedValue());
96+
97+
self::assertSame(43100, $worksheet1904->getCell('A1')->getValue());
98+
self::assertSame('2022-01-01', $worksheet1904->getCell('A1')->getFormattedValue());
99+
self::assertSame(43464, $worksheet1904->getCell('A2')->getValue());
100+
self::assertSame('2022-12-31', $worksheet1904->getCell('A2')->getFormattedValue());
101+
self::assertSame(43099, $worksheet1904->getCell('B1')->getCalculatedValue());
102+
self::assertSame('2021-12-31', $worksheet1904->getCell('B1')->getFormattedValue());
103+
self::assertSame(43465, $worksheet1904->getCell('B2')->getCalculatedValue());
104+
self::assertSame('2023-01-01', $worksheet1904->getCell('B2')->getFormattedValue());
105+
106+
// Check that accessing date values from one spreadsheet doesn't break accessing correct values from another
107+
self::assertSame(44561, $worksheet1900->getCell('B1')->getCalculatedValue());
108+
self::assertSame('2021-12-31', $worksheet1900->getCell('B1')->getFormattedValue());
109+
self::assertSame(44927, $worksheet1900->getCell('B2')->getCalculatedValue());
110+
self::assertSame('2023-01-01', $worksheet1900->getCell('B2')->getFormattedValue());
111+
self::assertSame(44562, $worksheet1900->getCell('A1')->getValue());
112+
self::assertSame('2022-01-01', $worksheet1900->getCell('A1')->getFormattedValue());
113+
self::assertSame(44926, $worksheet1900->getCell('A2')->getValue());
114+
self::assertSame('2022-12-31', $worksheet1900->getCell('A2')->getFormattedValue());
115+
116+
self::assertSame(43099, $worksheet1904->getCell('B1')->getCalculatedValue());
117+
self::assertSame('2021-12-31', $worksheet1904->getCell('B1')->getFormattedValue());
118+
self::assertSame(43465, $worksheet1904->getCell('B2')->getCalculatedValue());
119+
self::assertSame('2023-01-01', $worksheet1904->getCell('B2')->getFormattedValue());
120+
self::assertSame(43100, $worksheet1904->getCell('A1')->getValue());
121+
self::assertSame('2022-01-01', $worksheet1904->getCell('A1')->getFormattedValue());
122+
self::assertSame(43464, $worksheet1904->getCell('A2')->getValue());
123+
self::assertSame('2022-12-31', $worksheet1904->getCell('A2')->getFormattedValue());
124+
$spreadsheet1900->disconnectWorksheets();
125+
$spreadsheet1904->disconnectWorksheets();
126+
}
127+
}

0 commit comments

Comments
 (0)