fix: parse fractional seconds in ChronosTime as a fraction of a second

ChronosTime::parseString() passed the fractional-seconds digits straight to
(int), so a short fraction such as .5 was read as 5 microseconds instead of
500000 (half a second), diverging from DateTime. Right-pad the fractional digits
to microseconds before converting.
This commit is contained in:
dualfroz committed 2026-09-04 09:47:39 +02:00
1 parent 7c2a7810d0
commit 08305c1b95
3 files changed
+83 -1

No files matched your search

+66
View File
@@ -0,0 +1,66 @@
# fix: parse fractional seconds in ChronosTime as a fraction of a second
## Problem
`ChronosTime` misinterprets a time string whose fractional-seconds part has fewer
than six digits. The fractional part is treated as a literal microsecond count
instead of a fraction of a second:
```php
(new Cake\Chronos\ChronosTime('12:00:00.5'))->format('H:i:s.u');
// "12:00:00.000005" (5 microseconds)
// expected "12:00:00.500000" (half a second, matching DateTime)
(new DateTime('12:00:00.5'))->format('u'); // "500000"
```
So `12:00:00.5` becomes 5 microseconds rather than 500000, diverging from PHP's
own `DateTime` and from the round-trip used when constructing a `ChronosTime`
from a `DateTimeInterface` (which formats to six digits).
## Root cause
`src/ChronosTime.php`, `parseString()`:
```php
$microseconds = (int)substr($matches[4] ?? '', 0, 6);
```
`$matches[4]` is the digit run after the decimal point. Passing it straight to
`(int)` reads `"5"` as `5`, ignoring that these are the most-significant digits of
a fractional second. Only six-digit input (for example `.500000`) happened to
work, which is why the issue was not caught: every existing test uses a full
six-digit fraction (or a seven-digit one that is truncated to six).
## The fix
Right-pad the (at most six) fractional digits to microseconds before converting:
```php
$microseconds = (int)str_pad(substr($matches[4] ?? '', 0, 6), 6, '0', STR_PAD_RIGHT);
```
- `.5` -> `500000`
- `.05` -> `50000`
- `.500000` -> `500000` (unchanged)
- `.000005` -> `5` (unchanged)
- `.9999991`-> truncated to `999999` (unchanged; existing behaviour preserved)
This matches how `DateTime` interprets fractional seconds.
## Test
Added `ChronosTimeTest::testConstructFromStringWithFractionalSeconds`, asserting
that `12:00:00.5`, `12:00:00.05` and `12:00:00.000005` produce
`500000`, `50000` and `5` microseconds respectively.
## Gate
```
composer install && vendor/bin/phpunit
```
Result: exit code `0` - `OK (981 tests, 2496 assertions)`.
Counterfactual: reverting `parseString()` to `(int)substr(...)` makes the new test
fail - `12:00:00.5` yields `12:00:00.000005` instead of `12:00:00.500000`.