diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php index b669eb0a1e..7bb03c9da8 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php @@ -47,6 +47,7 @@ public static function day($dateValue) // Execute function $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + SharedDateHelper::roundMicroseconds($PHPDateObject); return (int) $PHPDateObject->format('j'); } @@ -85,6 +86,7 @@ public static function month($dateValue) // Execute function $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + SharedDateHelper::roundMicroseconds($PHPDateObject); return (int) $PHPDateObject->format('n'); } @@ -123,6 +125,7 @@ public static function year($dateValue) } // Execute function $PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue); + SharedDateHelper::roundMicroseconds($PHPDateObject); return (int) $PHPDateObject->format('Y'); } diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php index d9b99f3c36..e1331b0109 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php @@ -46,6 +46,7 @@ public static function hour($timeValue) // Execute function $timeValue = fmod($timeValue, 1); $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); + SharedDateHelper::roundMicroseconds($timeValue); return (int) $timeValue->format('H'); } @@ -86,6 +87,7 @@ public static function minute($timeValue) // Execute function $timeValue = fmod($timeValue, 1); $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); + SharedDateHelper::roundMicroseconds($timeValue); return (int) $timeValue->format('i'); } @@ -126,6 +128,7 @@ public static function second($timeValue) // Execute function $timeValue = fmod($timeValue, 1); $timeValue = SharedDateHelper::excelToDateTimeObject($timeValue); + SharedDateHelper::roundMicroseconds($timeValue); return (int) $timeValue->format('s'); } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 4f19673113..9f5abe34d9 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -223,11 +223,13 @@ public static function excelToDateTimeObject($excelTimestamp, $timeZone = null) $days = floor($excelTimestamp); $partDay = $excelTimestamp - $days; - $hours = floor($partDay * 24); - $partDay = $partDay * 24 - $hours; - $minutes = floor($partDay * 60); - $partDay = $partDay * 60 - $minutes; - $seconds = round($partDay * 60); + $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; if ($days >= 0) { $days = '+' . $days; @@ -235,7 +237,7 @@ public static function excelToDateTimeObject($excelTimestamp, $timeZone = null) $interval = $days . ' days'; return $baseDate->modify($interval) - ->setTime((int) $hours, (int) $minutes, (int) $seconds); + ->setTime((int) $hours, (int) $minutes, (int) $seconds, (int) $microseconds); } /** @@ -252,8 +254,10 @@ public static function excelToDateTimeObject($excelTimestamp, $timeZone = null) */ public static function excelToTimestamp($excelTimestamp, $timeZone = null) { - return (int) self::excelToDateTimeObject($excelTimestamp, $timeZone) - ->format('U'); + $dto = self::excelToDateTimeObject($excelTimestamp, $timeZone); + self::roundMicroseconds($dto); + + return (int) $dto->format('U'); } /** @@ -287,13 +291,15 @@ public static function PHPToExcel($dateValue) */ public static function dateTimeToExcel(DateTimeInterface $dateValue) { + $seconds = (float) sprintf('%d.%06d', $dateValue->format('s'), $dateValue->format('u')); + return self::formattedPHPToExcel( (int) $dateValue->format('Y'), (int) $dateValue->format('m'), (int) $dateValue->format('d'), (int) $dateValue->format('H'), (int) $dateValue->format('i'), - (int) $dateValue->format('s') + $seconds ); } @@ -323,7 +329,7 @@ public static function timestampToExcel($unixTimestamp) * @param int $day * @param int $hours * @param int $minutes - * @param int $seconds + * @param float|int $seconds * * @return float Excel date/time value */ @@ -553,4 +559,12 @@ public static function formattedDateTimeFromTimestamp(string $date, string $form return $dtobj->format($format); } + + public static function roundMicroseconds(DateTime $dti): void + { + $microseconds = (int) $dti->format('u'); + if ($microseconds >= 500000) { + $dti->modify('+1 second'); + } + } } diff --git a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php index ba54b53593..d1826ea77b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php @@ -166,6 +166,37 @@ public static function format($value, string $format): string // Excel 2003 XML formats, m will not have been changed to i above. // Change it now. $format = (string) \preg_replace('/\\\\:m/', ':i', $format); + $microseconds = (int) $dateObj->format('u'); + if (strpos($format, ':s.000') !== false) { + $milliseconds = (int) round($microseconds / 1000.0); + if ($milliseconds === 1000) { + $milliseconds = 0; + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + $format = str_replace(':s.000', ':s.' . sprintf('%03d', $milliseconds), $format); + } elseif (strpos($format, ':s.00') !== false) { + $centiseconds = (int) round($microseconds / 10000.0); + if ($centiseconds === 100) { + $centiseconds = 0; + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + $format = str_replace(':s.00', ':s.' . sprintf('%02d', $centiseconds), $format); + } elseif (strpos($format, ':s.0') !== false) { + $deciseconds = (int) round($microseconds / 100000.0); + if ($deciseconds === 10) { + $deciseconds = 0; + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + $format = str_replace(':s.0', ':s.' . sprintf('%1d', $deciseconds), $format); + } else { // no fractional second + if ($microseconds >= 500000) { + $dateObj->modify('+1 second'); + } + $dateObj->modify("-$microseconds microseconds"); + } return $dateObj->format($format); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php index 23af522269..4f13264ffd 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/ExplicitDateTest.php @@ -35,7 +35,7 @@ public static function testExplicitDate(): void $value = $sheet->getCell('A3')->getValue(); $formatted = $sheet->getCell('A3')->getFormattedValue(); self::assertEqualsWithDelta(44561.98948, $value, 0.00001); - self::assertSame('2021-12-31 23:44:51', $formatted); + self::assertSame('2021-12-31 23:44:52', $formatted); // Date only $value = $sheet->getCell('B3')->getValue(); $formatted = $sheet->getCell('B3')->getFormattedValue(); @@ -45,7 +45,7 @@ public static function testExplicitDate(): void $value = $sheet->getCell('C3')->getValue(); $formatted = $sheet->getCell('C3')->getFormattedValue(); self::assertEqualsWithDelta(0.98948, $value, 0.00001); - self::assertSame('23:44:51', $formatted); + self::assertSame('23:44:52', $formatted); $spreadsheet->disconnectWorksheets(); } diff --git a/tests/data/Style/NumberFormatDates.php b/tests/data/Style/NumberFormatDates.php index 42fbc4ea09..632b967e03 100644 --- a/tests/data/Style/NumberFormatDates.php +++ b/tests/data/Style/NumberFormatDates.php @@ -43,10 +43,25 @@ 'yyyy/mm/dd\ h:mm:ss.000', ], [ - '2023/02/28 07:35:02.000', + '2023/02/28 07:35:02.400', 44985.316, 'yyyy/mm/dd\ hh:mm:ss.000', ], + [ + '2023/02/28 07:35:13.067', + 44985.316123456, + 'yyyy/mm/dd\ hh:mm:ss.000', + ], + [ + '2023/02/28 07:35:13.07', + 44985.316123456, + 'yyyy/mm/dd\ hh:mm:ss.00', + ], + [ + '2023/02/28 07:35:13.1', + 44985.316123456, + 'yyyy/mm/dd\ hh:mm:ss.0', + ], [ '07:35:00 AM', 43270.315972222,