Skip to content

Make Base Date a Property of Spreadsheet #4071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Changed

- On read, Xlsx Reader had been breaking up union ranges into separate individual ranges. It will now try to preserve range as it was read in. [PR #4042](https://github.com/PHPOffice/PhpSpreadsheet/pull/4042)
- Xlsx/Xls spreadsheet calculation and formatting of dates will use base date of spreadsheet even when spreadsheets with different base dates are simultaneously open. [Issue #1036](https://github.com/PHPOffice/PhpSpreadsheet/issues/1036) [Issue #1635](https://github.com/PHPOffice/PhpSpreadsheet/issues/1635) [PR #4071](https://github.com/PHPOffice/PhpSpreadsheet/pull/4071)

### Deprecated

Expand Down
13 changes: 11 additions & 2 deletions src/PhpSpreadsheet/Cell/Cell.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,15 @@ public function getValueString(): string
*/
public function getFormattedValue(): string
{
return (string) NumberFormat::toFormattedString(
$this->getCalculatedValueString(),
$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
Expand Down Expand Up @@ -372,6 +377,8 @@ public function getCalculatedValue(bool $resetLog = true): mixed
{
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(
Expand All @@ -387,6 +394,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
}
}
} 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) {
Expand All @@ -399,6 +407,7 @@ public function getCalculatedValue(bool $resetLog = true): mixed
$ex
);
}
SharedDate::setExcelCalendar($currentCalendar);

if ($result === '#Not Yet Implemented') {
return $this->calculatedValue; // Fallback if calculation engine does not support the formula.
Expand Down
2 changes: 2 additions & 0 deletions src/PhpSpreadsheet/Reader/Xls.php
Original file line number Diff line number Diff line change
Expand Up @@ -1927,8 +1927,10 @@ private function readDateMode(): void

// 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);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/PhpSpreadsheet/Reader/Xlsx.php
Original file line number Diff line number Diff line change
Expand Up @@ -712,12 +712,14 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
$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);
}
}
}
Expand Down
11 changes: 5 additions & 6 deletions src/PhpSpreadsheet/Shared/Date.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -173,7 +172,7 @@ public static function convertIsoDate(mixed $value): float|int
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");
}
Expand Down
25 changes: 25 additions & 0 deletions src/PhpSpreadsheet/Spreadsheet.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand All @@ -31,6 +32,8 @@ class Spreadsheet implements JsonSerializable
self::VISIBILITY_VERY_HIDDEN,
];

protected int $excelCalendar = Date::CALENDAR_WINDOWS_1900;

/**
* Unique ID.
*/
Expand Down Expand Up @@ -1553,4 +1556,26 @@ public function getTableByName(string $tableName): ?Table

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;
}
}
6 changes: 3 additions & 3 deletions src/PhpSpreadsheet/Writer/Xls/Workbook.php
Original file line number Diff line number Diff line change
Expand Up @@ -910,9 +910,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);
Expand Down
6 changes: 3 additions & 3 deletions src/PhpSpreadsheet/Writer/Xlsx/Workbook.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function writeWorkbook(Spreadsheet $spreadsheet, bool $recalcRequired = f
$this->writeFileVersion($objWriter);

// workbookPr
$this->writeWorkbookPr($objWriter);
$this->writeWorkbookPr($objWriter, $spreadsheet);

// workbookProtection
$this->writeWorkbookProtection($objWriter, $spreadsheet);
Expand Down Expand Up @@ -81,11 +81,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');
}

Expand Down
127 changes: 127 additions & 0 deletions tests/PhpSpreadsheetTests/Reader/Xls/DateReaderTest.php
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading
Loading