@@ -171,6 +171,67 @@ private function getDomFromPath(string $path): \DOMDocument
171
171
return $ this ->getXPathFromPath ($ path )->document ;
172
172
}
173
173
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
+
174
235
/**
175
236
* Count the number of worksheets in the workbook.
176
237
*/
@@ -532,6 +593,18 @@ public function readFloat(int $sheetNumber, string $cellName): ?float
532
593
return $ cell === null ? null : $ cell ->readFloat ();
533
594
}
534
595
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
+
535
608
/**
536
609
* Read an integer in the given worksheet at the given cell location.
537
610
*
0 commit comments