Skip to content

Commit 2a25899

Browse files
committed
Add support for reading dates
1 parent 55163f6 commit 2a25899

File tree

4 files changed

+96
-2
lines changed

4 files changed

+96
-2
lines changed

src/XlsxFastEditor.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,67 @@ private function getDomFromPath(string $path): \DOMDocument
171171
return $this->getXPathFromPath($path)->document;
172172
}
173173

174+
/**
175+
* Excel can either use a base date from year 1900 (Microsoft Windows) or from year 1904 (old Apple MacOS).
176+
* https://support.microsoft.com/en-us/office/date-systems-in-excel-e7fe7167-48a9-4b96-bb53-5612a800b487
177+
* @phpstan-return 1900|1904
178+
*/
179+
public function getWorkbookDateSystem(): int
180+
{
181+
static $baseYear = 0;
182+
if ($baseYear == 0) {
183+
$xpath = $this->getXPathFromPath(self::WORKBOOK_PATH);
184+
$date1904 = $xpath->evaluate('normalize-space(/o:workbook/o:workbookPr/@date1904)');
185+
if (is_string($date1904) && in_array(strtolower(trim($date1904)), ['true', '1'], true)) {
186+
$baseYear = 1904;
187+
} else {
188+
$baseYear = 1900;
189+
}
190+
}
191+
return $baseYear;
192+
}
193+
194+
public static function excelDateToDateTime(float $excelDateTime, int $workbookDateSystem = 1900): \DateTimeImmutable
195+
{
196+
static $baseDate1900 = null;
197+
static $baseDate1904 = null;
198+
if ($workbookDateSystem === 1900) {
199+
if ($excelDateTime < 1) {
200+
// Make cells with only time (no date) to start on 1900-01-01
201+
$excelDateTime++;
202+
}
203+
if ($excelDateTime < 60) {
204+
// https://learn.microsoft.com/en-us/office/troubleshoot/excel/wrongly-assumes-1900-is-leap-year
205+
$excelDateTime++;
206+
}
207+
// 1 January 1900 as serial number 1 in the 1900 Date System, accounting for leap year problem
208+
if ($baseDate1900 === null) {
209+
$baseDate1900 = new \DateTimeImmutable('1899-12-30');
210+
}
211+
$excelBaseDate = $baseDate1900;
212+
} elseif ($workbookDateSystem === 1904) {
213+
// 1 January 1904 as serial number 0 in the 1904 Date System
214+
if ($baseDate1904 === null) {
215+
$baseDate1904 = new \DateTimeImmutable('1904-01-01');
216+
}
217+
$excelBaseDate = $baseDate1904;
218+
} else {
219+
throw new \InvalidArgumentException('Invalid Excel workbook date system! Supported values: 1900, 1904');
220+
}
221+
222+
$daysOffset = floor($excelDateTime);
223+
$iso8601 = "P{$daysOffset}D";
224+
225+
$timeFraction = $excelDateTime - $daysOffset;
226+
if ($timeFraction > 0) {
227+
// Convert days to seconds with no more than milliseconds precision
228+
$seconds = floor($timeFraction * 86400000) / 1000;
229+
$iso8601 .= "T{$seconds}S";
230+
}
231+
232+
return $excelBaseDate->add(new \DateInterval($iso8601));
233+
}
234+
174235
/**
175236
* Count the number of worksheets in the workbook.
176237
*/
@@ -532,6 +593,18 @@ public function readFloat(int $sheetNumber, string $cellName): ?float
532593
return $cell === null ? null : $cell->readFloat();
533594
}
534595

596+
/**
597+
* Read a date/time in the given worksheet at the given cell location.
598+
*
599+
* @param int $sheetNumber Worksheet number (base 1)
600+
* @param $cellName Cell name such as `B4`
601+
*/
602+
public function readDate(int $sheetNumber, string $cellName): ?\DateTimeImmutable
603+
{
604+
$cell = $this->getCell($sheetNumber, $cellName, XlsxFastEditor::ACCESS_MODE_NULL);
605+
return $cell === null ? null : $cell->readDateTime();
606+
}
607+
535608
/**
536609
* Read an integer in the given worksheet at the given cell location.
537610
*

src/XlsxFastEditorCell.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,18 @@ public function readFloat(): ?float
139139
return (float)$value;
140140
}
141141

142+
/**
143+
* Read the date/time value of the cell, if any.
144+
*/
145+
public function readDateTime(): ?\DateTimeImmutable
146+
{
147+
$value = $this->readFloat();
148+
if ($value === null) {
149+
return null;
150+
}
151+
return XlsxFastEditor::excelDateToDateTime($value, $this->editor->getWorkbookDateSystem());
152+
}
153+
142154
/**
143155
* Read the integer value of the cell.
144156
*/

tests/test.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@
1717

1818
assert($xlsxFastEditor->getWorksheetCount() === 2);
1919

20+
assert($xlsxFastEditor->getWorkbookDateSystem() === 1900);
21+
assert(XlsxFastEditor::excelDateToDateTime(0.5, 1900)->format('c') === '1900-01-01T12:00:00+01:00');
22+
assert(XlsxFastEditor::excelDateToDateTime(32, 1900)->format('c') === '1900-02-01T00:00:00+01:00');
23+
assert(XlsxFastEditor::excelDateToDateTime(44865, 1904)->format('c') === '2026-11-01T00:00:00+01:00');
24+
2025
$sheet1 = $xlsxFastEditor->getWorksheetNumber('Sheet1');
2126
assert($sheet1 === 1);
2227

@@ -42,14 +47,18 @@
4247
assert($xlsxFastEditor->readInt($sheet2, 'c3') === -5 * 2);
4348
assert($xlsxFastEditor->readString($sheet2, 'B3') === 'déjà-vu');
4449

50+
assert($xlsxFastEditor->readDate($sheet1, 'F2')?->format('c') === '1980-11-24T00:00:00+01:00');
51+
assert($xlsxFastEditor->readDate($sheet1, 'F3')?->format('c') === '1980-11-24T10:20:30+01:00');
52+
assert($xlsxFastEditor->readDate($sheet1, 'F4')?->format('c') === '1900-01-01T10:20:30+01:00');
53+
4554
assert($xlsxFastEditor->readArray($sheet1)['B'][2] === 'Hello');
4655
assert($xlsxFastEditor->readArrayWithHeaders($sheet1)['Strings'][2] === 'Hello');
4756

4857
// Navigation
4958
assert($xlsxFastEditor->getFirstRow($sheet1)?->number() === 1);
5059
assert($xlsxFastEditor->getRow($sheet1, 1)?->getFirstCell()?->name() === 'A1');
5160
assert($xlsxFastEditor->getRow($sheet1, 2)?->number() === 2);
52-
assert($xlsxFastEditor->getRow($sheet1, 3)?->getLastCell()?->name() === 'E3');
61+
assert($xlsxFastEditor->getRow($sheet1, 3)?->getLastCell()?->name() === 'F3');
5362
assert($xlsxFastEditor->getRow($sheet1, 4)?->getCell('D4')?->name() === 'D4');
5463
assert($xlsxFastEditor->getLastRow($sheet1)?->number() === 4);
5564

@@ -70,7 +79,7 @@
7079
$nb++;
7180
}
7281
}
73-
assert($nb === 20);
82+
assert($nb === 24);
7483

7584
// Writing existing cells
7685
$xlsxFastEditor->writeFormula($sheet1, 'c2', '=2*3');

tests/test.xlsx

242 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)