Repository navigation
Fix CarbonInterval::spec() when the fraction holds a second or more - #3366
Merged
kylekatarnls merged 1 commit intoSep 23, 2026
Merged
kylekatarnls merged 1 commit into
kylekatarnls merged 1 commit into
Conversation
getDateIntervalSpec() built the seconds component by concatenation:
\sprintf('%d.%06d', $seconds, abs($interval->f) * 1000000)
%06d pads but never carries, so an interval whose DateInterval::$f holds
one second or more was rendered as a fraction of its own magnitude.
CarbonInterval::milliseconds() and ::microseconds() store the whole value
in $f, so:
CarbonInterval::milliseconds(10500)->spec(true); // PT0.10500000S
The result is well-formed ISO 8601, so every conforming parser accepts it
and silently reads back 0.105 rather than 10.5, and totalSeconds disagrees
with spec() on the same object.
Use the addition already applied by the $withNegatives branch of the same
expression on both paths. Output is byte-identical for intervals whose
fraction is already below one second.
Fixes briannesbitt#3365
kylekatarnls
approved these changes
Sep 23, 2026
Collaborator
|
Thanks 🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3365.
The bug
getDateIntervalSpec()built the seconds component by concatenation:%06dpads but never carries, so an interval whoseDateInterval::$fholds one second or more is rendered as a fraction of its own magnitude.CarbonInterval::milliseconds()and::microseconds()store the whole value in$f, so:The output is well-formed ISO 8601, so every conforming parser accepts it and silently reads back the wrong number — I cross-checked against two independent
xs:durationimplementations (.NET'sXmlConvert, and luxon), and both return0.105.totalSecondsandspec(true)therefore describe different durations on the same object.The fraction width is unstable for the same reason — 6, 7 or 8 digits depending on the magnitude of
$f.The fix
The
$withNegativesbranch of the same ternary already computes this correctly by addition rather than concatenation, which is whyspec(true, true)returnsPT10.500000Stoday wherespec(true)does not. This applies that form on both paths.Verification
s0–59 ×f0.000–0.999, zero differences.testSetLocaleToAutoneedingfr_FR.UTF-8, which are identical on unmodifiedmasterin the same container.tests/CarbonInterval/alone: 443 tests, 1219 assertions, green.master(5 of 8 assertions) and passes with the fix.php-cs-fixerreports no issues in either changed file (the two files it does flag,Traits/Date.phpandTraits/Units.php, are flagged identically on unmodifiedmaster).PT0.012300SandPT-15.321654Sboth still pass.One behaviour change worth flagging
Beyond 6 decimal places,
sprintf's integer cast truncates wherenumber_formatrounds —f = 0.1234565gives0.123456before and0.123457after. That is belowDateInterval's own resolution, but it is a real difference and I would rather name it than leave it to be discovered.Alternative worth considering
This fixes the formatter defensively. The arguably deeper issue is that
milliseconds()/microseconds()leave$foutside the0 <= f < 1range the property represents —->cascade()normalises it andspec(true)is then correct either way. Happy to fix it in the factories instead if you prefer that; this change is the smaller one and does not alter any already-normalised interval.