In PHP 8.4+, how does round() handle halves by default, and what does the RoundingMode enum add?
answer
- default: half away from zero
- always returns float
- HalfEven is banker's rounding
- four new modes exist only as enum cases
- invalid mode: ValueError since 8.4
basics
~20 sround() rounds halves away from zero by default, so round(2.5) is 3.0 and round(-2.5) is -3.0, and it always returns a float. PHP 8.4 added the RoundingMode enum: the four half modes plus TowardsZero, AwayFromZero, PositiveInfinity and NegativeInfinity.
solid answer
~40 s`round(int|float $num, int $precision = 0, int|RoundingMode $mode = RoundingMode::HalfAwayFromZero): float`. The default rounds a tie **away from zero**: `round(2.5)` is `3.0`, `round(-2.5)` is `-3.0`. The result is always a `float`, so `(int)` is needed for an integer. A negative precision rounds to tens or hundreds: `round(345, -2)` is `300.0`. **PHP 8.4** introduced the `RoundingMode` enum: `HalfAwayFromZero`, `HalfTowardsZero`, `HalfEven` and `HalfOdd` match the old `PHP_ROUND_HALF_UP`, `_DOWN`, `_EVEN` and `_ODD` constants, and four new modes — `TowardsZero`, `AwayFromZero`, `PositiveInfinity`, `NegativeInfinity` — exist only as enum cases. `HalfEven` is banker's rounding, which avoids an upward bias when many halves are rounded. 8.4 also made an invalid mode throw `ValueError` and rewrote the rounding internals, so a few edge-case results changed.
code
php · 13 lines<?php
var_dump(round(2.5)); // float(3)
var_dump(round(-2.5)); // float(-3)
var_dump(round(2.5, 0, RoundingMode::HalfEven)); // float(2)
var_dump(round(3.5, 0, RoundingMode::HalfEven)); // float(4)
var_dump(round(1.21, 1, RoundingMode::PositiveInfinity)); // float(1.3)
var_dump(round(345, -2)); // float(300)
try {
round(1.5, 0, 42);
} catch (ValueError $e) {
echo $e->getMessage(), PHP_EOL; // invalid mode, PHP 8.4+
}go deeper
Recall that round(2.5) is 3.0, round(-2.5) is -3.0, and that round() always returns a float.
Explain the four tie modes and their legacy constants, the naming trap in PHP_ROUND_HALF_UP, negative precision, and the 8.4 direction modes.
Choose HalfEven or a direction mode deliberately for aggregates and limits, and review pinned float outputs when upgrading across the 8.4 rounding rewrite.
Set explicit rounding policy per domain — which mode, at which step — and require it in code rather than relying on defaults.
## The signature In PHP 8.4 and later, `round()` is declared as: ```php function round(int|float $num, int $precision = 0, int|RoundingMode $mode = RoundingMode::HalfAwayFromZero): float ``` Three facts follow from it: 1. The result is **always a `float`**, even for `round(3)`. Cast with `(int)` when an integer is required, and remember that a function declared `: int` cannot return `round()`'s result directly in a strict file. 2. `$precision` counts decimal places; a **negative** precision rounds left of the point: `round(345, -2)` is `300.0`, `round(678, -2)` is `700.0`. 3. `$mode` accepts either a legacy integer constant or a `RoundingMode` enum case. ## How ties are resolved A **tie** is a value exactly halfway between two candidates, like `2.5` at precision 0. The mode decides which way it goes: | Mode (enum case) | Legacy constant | `2.5` | `3.5` | `-2.5` | |---|---|---|---|---| | `HalfAwayFromZero` (default) | `PHP_ROUND_HALF_UP` | `3` | `4` | `-3` | | `HalfTowardsZero` | `PHP_ROUND_HALF_DOWN` | `2` | `3` | `-2` | | `HalfEven` | `PHP_ROUND_HALF_EVEN` | `2` | `4` | `-2` | | `HalfOdd` | `PHP_ROUND_HALF_ODD` | `3` | `3` | `-3` | Note the naming trap: PHP's `PHP_ROUND_HALF_UP` means away from zero, so `-1.5` becomes `-2`, not `-1`. The enum names say what they do. **`HalfEven`**, also called banker's rounding, sends ties to the even neighbour. Across many values, half of the ties go up and half go down, so totals are not biased upward. That is why it is common for aggregating money and statistics. ## The four new modes in PHP 8.4 These are not about ties; they pick a direction for **every** value, and they exist **only** as enum cases: - `RoundingMode::TowardsZero` — truncate: `round(1.29, 1, RoundingMode::TowardsZero)` is `1.2`, `-1.29` gives `-1.2`. - `RoundingMode::AwayFromZero` — always away: `1.21` gives `1.3`. - `RoundingMode::PositiveInfinity` — ceiling at the given precision: `1.21` gives `1.3`, `-1.29` gives `-1.2`. - `RoundingMode::NegativeInfinity` — floor at the given precision: `1.29` gives `1.2`. Before 8.4 you needed `floor($x * 10) / 10`, which reintroduces float error through the multiplication. ## Other PHP 8.4 changes - An **invalid mode** now throws `ValueError`. Earlier versions silently fell back to `PHP_ROUND_HALF_UP`. - The integer-rounding code was **rewritten**. For example, `round(0.49999999999999994)` used to return `1.0` and now correctly returns `0.0`. The old "pre-rounding" step was also removed and replaced by a different comparison, so a few inputs round differently from PHP 8.3. Tests that pinned exact float outputs may need review after an upgrade. ## Rounding a binary float to decimal places `round()` works on binary floats, yet it is designed to round the decimal value a person wrote. The manual's examples show `round(5.045, 2)` as `5.05` and `round(5.055, 2)` as `5.06`, even though neither literal is exactly representable. That makes `round()` convenient for display and for converting a float to minor units, `(int) round($price * 100)`, but it does not make floats exact: each `round()` result is still a float, and summing rounded floats can drift again. ## Where each mode fits - **Display and ordinary business rules**: the default, `HalfAwayFromZero`, matches what most people learned at school. - **Aggregating many values**: `HalfEven`, to avoid systematic upward bias. - **Limits and quotas**: `PositiveInfinity` or `NegativeInfinity`, so a rounded value never crosses a threshold in the wrong direction. - **Exact money calculations**: do the arithmetic in integers or bcmath, and round once at a defined step, with the mode written explicitly rather than relying on a default. ## Common mistakes - **Expecting an `int`.** `round(3.7)` is `float(4)`; comparing it with `===` to `4` is `false`. - **Misreading `PHP_ROUND_HALF_UP`.** It means away from zero for negative numbers too. - **Rounding repeatedly.** Rounding each intermediate result to two places and then rounding the total again can move the result by a unit in the last place; round once. - **Assuming output stability across upgrades.** The 8.4 rewrite changed a few edge-case results, so snapshot tests of rounded floats deserve a look after upgrading from 8.3 or earlier.
- In PHP, what does round(-1.5) return with the default mode, and why do people get it wrong?`-2.0`. The default is half away from zero, and the legacy constant for it is named `PHP_ROUND_HALF_UP`, which suggests rounding toward positive infinity and `-1`. The 8.4 enum name `RoundingMode::HalfAwayFromZero` describes the real behaviour.
- In PHP 8.4+, how do you round 1.21 up to one decimal place without multiplying and dividing floats?Call `round(1.21, 1, RoundingMode::PositiveInfinity)`, which returns `1.3`. Before PHP 8.4 the usual idiom was `ceil($x * 10) / 10`, which adds its own float error through the multiplication. The direction modes exist only as `RoundingMode` cases, with no legacy integer constants.
saying these in an interview costs you the question
- Says round() returns an int when the precision is 0.
- Thinks PHP_ROUND_HALF_UP rounds -1.5 to -1.
- Believes round(2.5) is 2 by default, like banker's rounding.
- Expects an invalid mode to be ignored silently on PHP 8.4+.
- Looks for a PHP_ROUND_* constant for TowardsZero or PositiveInfinity.