diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index e8f23164f3..04896edf5e 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -223,13 +223,27 @@ public static function excelToDateTimeObject(float|int $excelTimestamp, null|Dat } $days = floor($excelTimestamp); $partDay = $excelTimestamp - $days; - $hms = 86400 * $partDay; - $microseconds = (int) round(fmod($hms, 1) * 1000000); - $hms = (int) floor($hms); - $hours = intdiv($hms, 3600); - $hms -= $hours * 3600; - $minutes = intdiv($hms, 60); - $seconds = $hms % 60; + $hoursInMs = 86400 * $partDay; + $wholeSeconds = (int) floor($hoursInMs); + + // flooring here might lose data due to precision issues, hence round it + $microseconds = (int) round(($hoursInMs - $wholeSeconds) * 1_000_000); + $microseconds = (int) round($microseconds, -2); + + // rounding may lead to edge cases + if ($microseconds === 1_000_000) { + $microseconds = 0; + ++$wholeSeconds; + } + if ($wholeSeconds >= 86400) { + $wholeSeconds = 0; + ++$days; + } + + $hours = intdiv($wholeSeconds, 3600); + $remainingSeconds = $wholeSeconds % 3600; + $minutes = intdiv($remainingSeconds, 60); + $seconds = $remainingSeconds % 60; if ($days >= 0) { $days = '+' . $days; diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php index adad99aff9..46a357e2c7 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php @@ -4,7 +4,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx; +use DateTime; use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; class ExplicitDateTest extends \PHPUnit\Framework\TestCase { @@ -40,12 +44,55 @@ public static function testExplicitDate(): void $formatted = $sheet->getCell('B3')->getFormattedValue(); self::assertEquals(44561, $value); self::assertSame('2021-12-31', $formatted); - // Time only + // Time only, with seconds $value = $sheet->getCell('C3')->getValue(); $formatted = $sheet->getCell('C3')->getFormattedValue(); self::assertEqualsWithDelta(0.98948, $value, 0.00001); self::assertSame('23:44:52', $formatted); + // Time only, full minute + $value = $sheet->getCell('F3')->getValue(); + $formatted = $sheet->getCell('F3')->getFormattedValue(); + self::assertEqualsWithDelta(0.5673611, $value, 0.00001); + self::assertSame('13:37', $formatted); $spreadsheet->disconnectWorksheets(); } + + public function testThatDateTimesCanBePersistedAndReread(): void + { + $originalDateTime = new DateTime('2020-10-21T14:55:31'); + + $dateTimeFromSpreadsheet = $this->getDateTimeFrom($this->excelSheetWithDateTime($originalDateTime)); + $dateTimeFromSpreadsheetAfterPersistAndReread = $this->getDateTimeFrom($this->persistAndReread($this->excelSheetWithDateTime($originalDateTime))); + + self::assertEquals($originalDateTime, $dateTimeFromSpreadsheet); + self::assertEquals($originalDateTime, $dateTimeFromSpreadsheetAfterPersistAndReread); + } + + private function excelSheetWithDateTime(DateTime $dateTime): Spreadsheet + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->getActiveSheet()->setCellValue('A1', Date::dateTimeToExcel($dateTime)); + + return $spreadsheet; + } + + public function getDateTimeFrom(Spreadsheet $spreadsheet): DateTime + { + $value = $spreadsheet->getSheet(0)->getCell('A1')->getCalculatedValue(); + self::assertIsNumeric($value); + $value = (float) $value; + + return Date::excelToDateTimeObject($value); + } + + private function persistAndReread(Spreadsheet $spreadsheet): Spreadsheet + { + $tempPointer = tmpfile(); + $tempFileName = stream_get_meta_data($tempPointer)['uri'] ?? null; + self::assertNotNull($tempFileName, 'Temp file not created'); + (new Xlsx($spreadsheet))->save($tempFileName); + + return (new \PhpOffice\PhpSpreadsheet\Reader\Xlsx())->load($tempFileName); + } } diff --git a/tests/data/Reader/XLSX/explicitdate.xlsx b/tests/data/Reader/XLSX/explicitdate.xlsx index c4017b8780..874b4b8f04 100644 Binary files a/tests/data/Reader/XLSX/explicitdate.xlsx and b/tests/data/Reader/XLSX/explicitdate.xlsx differ