@@ -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 *
0 commit comments