VulCode
Blog

Carbon: round(3600) rounds to the full hour instead of only zeroing seconds

A numeric precision in round(), floor() and ceil() means a number of seconds, but from 60 upwards it never carried into minutes, hours and days. round(3600) only zeroed the seconds.

DualFroz - VulCode CEODualFroz - VulCode CEO1 min read
Carbon: round(3600) rounds to the full hour instead of only zeroing seconds - DualFroz - VulCode CEO
Title in the repository
fix: numeric round()/floor()/ceil() precision >= 60 seconds does not carry into minute/hour/day
Merged
25 Sep 2026
Written up here
6 Oct 2026

The note was written 11 days after the change was merged.

Carbon
Technologies:PHPdates and time

What was broken

Carbon is a PHP date library used by Laravel, among many others. Its round(), floor() and ceil() methods take a precision either as a unit name ('hour') or as a number, which the documentation and tests describe as a number of seconds. Below 60 everything worked, for example round(5) rounded to 5 seconds. From 60 upwards the result stayed locked inside the seconds field:

Carbon::parse('2026-01-01 14:21:37')->round(3600)   -> 14:21:00 (instead of 14:00:00)
Carbon::parse('2026-01-01 14:30:00')->ceil(3600)    -> 14:30:00 (instead of 15:00:00)

For a numeric precision the code set the unit to second and rounded only the seconds value, which stays in the 0-59 range. With a precision of 3600 the result was always zero, and minutes and hours were never touched. The same bug affected the string form '3600 seconds'.

What we changed

In roundUnit(), a numeric precision that spans the whole next unit is now carried upwards: 3600 seconds become 60 minutes, and 60 minutes become an hour. The list of steps only contains units of a fixed size, so a precision in days is never carried into months, which have a varying number of days.

How we tested it

A new test compares numeric precisions with their string forms: round(3600) with round('hour'), round(120) with round('2 minutes'), round(86400) with round('day'), plus floor(3600) just before a full hour and ceil(3600) at half past.

The result

The change was merged on 25 September 2026. A numeric precision in Carbon now behaves the same as the unit name it stands for.

A PHP DateTime extension for working with dates, standard in the Laravel ecosystem.

Language
PHP
GitHub stars
16,591

The same calls before and after the change

The cases of the note and of the test in pull request #3359: what should come out, what came out before the fix and what comes out now.

CallShould giveBefore the changeAfter the changeResult
Callround(3600)on 2026-01-01 14:21:37
Should give14:00:00
Before the change14:21:00
After the change14:00:00
As expected
Callceil(3600)on 2026-01-01 14:30:00, half past
Should give15:00:00
Before the change14:30:00
After the change15:00:00
As expected
Callround(120)next to the string form in the test
Should giveround('2 minutes')
After the changeround('2 minutes')
As expected
Callround(86400)next to the string form in the test
Should giveround('day')
After the changeround('day')
As expected
Callfloor(3600)just before a full hour
Should givefloor('hour')
After the changefloor('hour')
As expected

5 of 5 cases as expected after the change

What we changed

In roundUnit(), a numeric precision that spans the whole next unit is carried upwards: 3600 seconds become 60 minutes, and 60 minutes an hour.

What it leaves alone

The list of steps only holds units of a fixed size, so days are never carried into months.

What it taught us

Rounding time has to carry into larger units, just like addition. An hour rounded to a full hour must not end at 60 minutes.

Questions about change #3359

Where the change is, how we tested it and who merged it.