Description
Symfony version(s) affected
7.1, 6.4, 5.4
Description
According to the official documentation on how NumberType
fields round float numbers, the default rounding mode is \NumberFormatter::ROUND_HALFUP
.
When no scale is defined though, I'm not experiencing the advertised behavior and the following happens:
- Numbers typed-in by users do not get rounded at all: they are provided as is to the application when submitting the form.
- When loaded back into the form, they get rounded "half even", not "half up" (e.g. number
1.2345
would get rounded to1.234
instead of1.235
).
Maybe I'm missing something, but I would expect the typed-in numbers to be rounded "half up" as advertised by the documentation, in both cases above, whether I set a scale or not.
Initially, I thought it might have been a documentation issue (cf symfony/symfony-docs#20134), but I'm leaning towards a core Symfony issue instead now.
How to reproduce
Here is a reproducer repository that illustrates the described behavior:
https://github.com/pacproduct/SymfonyNumberTypeRoudingIssue
Possible Solution
I'm not sure I'd know how to cleanly fix this.
But as a starting lead: I believe the following functions may be the source of these 2 behaviors:
\Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer::round
:
Seems like this function is the one that applies the rounding mode before numbers are returned by the form. It only applies the rounding mode if a scale is defined:
private function round(int|float $number): int|float
{
if (null !== $this->scale) {
[...]
$number = match ($this->roundingMode) {
\NumberFormatter::ROUND_CEILING => ceil($number),
[...]
\NumberFormatter::ROUND_HALFDOWN => round($number, 0, \PHP_ROUND_HALF_DOWN),
};
[...]
}
return $number;
}
\Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer::getNumberFormatter
:
Seems like this function is the one that applies the rounding mode before numbers are injected in the form. It only applies the rounding mode if a scale is defined:
protected function getNumberFormatter(): \NumberFormatter
{
$formatter = new \NumberFormatter($this->locale ?? \Locale::getDefault(), \NumberFormatter::DECIMAL);
if (null !== $this->scale) {
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
}
$formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->grouping);
return $formatter;
}
Additional Context
For information, the rounding mode observed here (\NumberFormatter::ROUND_HALFEVEN
) is the default behavior of the native PHP library \NumberFormatter
, which explains why not setting a rounding mode to it makes it behave the way it does in NumberToLocalizedStringTransformer::getNumberFormatter
.
Illustration:
$formatter = new \NumberFormatter('en', \NumberFormatter::DECIMAL);
var_dump($formatter->format(485.3485)); // Outputs: string(7) "485.348"