Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit d97565d

Browse filesBrowse files
VincentLangletfabpot
authored andcommitted
[Form] Correctly round model with PercentType and add a rounding_mode option
1 parent f46ab58 commit d97565d
Copy full SHA for d97565d

File tree

Expand file treeCollapse file tree

4 files changed

+229
-4
lines changed
Filter options
Expand file treeCollapse file tree

4 files changed

+229
-4
lines changed

‎src/Symfony/Component/Form/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
is deprecated. The method will be added to the interface in 6.0.
1212
* Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method
1313
is deprecated. The method will be added to the interface in 6.0.
14+
* Added a `rounding_mode` option for the PercentType and correctly round the value when submitted
1415

1516
5.0.0
1617
-----

‎src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Core/DataTransformer/PercentToLocalizedStringTransformer.php
+109-2Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,55 @@
2323
*/
2424
class PercentToLocalizedStringTransformer implements DataTransformerInterface
2525
{
26+
/**
27+
* Rounds a number towards positive infinity.
28+
*
29+
* Rounds 1.4 to 2 and -1.4 to -1.
30+
*/
31+
const ROUND_CEILING = \NumberFormatter::ROUND_CEILING;
32+
33+
/**
34+
* Rounds a number towards negative infinity.
35+
*
36+
* Rounds 1.4 to 1 and -1.4 to -2.
37+
*/
38+
const ROUND_FLOOR = \NumberFormatter::ROUND_FLOOR;
39+
40+
/**
41+
* Rounds a number away from zero.
42+
*
43+
* Rounds 1.4 to 2 and -1.4 to -2.
44+
*/
45+
const ROUND_UP = \NumberFormatter::ROUND_UP;
46+
47+
/**
48+
* Rounds a number towards zero.
49+
*
50+
* Rounds 1.4 to 1 and -1.4 to -1.
51+
*/
52+
const ROUND_DOWN = \NumberFormatter::ROUND_DOWN;
53+
54+
/**
55+
* Rounds to the nearest number and halves to the next even number.
56+
*
57+
* Rounds 2.5, 1.6 and 1.5 to 2 and 1.4 to 1.
58+
*/
59+
const ROUND_HALF_EVEN = \NumberFormatter::ROUND_HALFEVEN;
60+
61+
/**
62+
* Rounds to the nearest number and halves away from zero.
63+
*
64+
* Rounds 2.5 to 3, 1.6 and 1.5 to 2 and 1.4 to 1.
65+
*/
66+
const ROUND_HALF_UP = \NumberFormatter::ROUND_HALFUP;
67+
68+
/**
69+
* Rounds to the nearest number and halves towards zero.
70+
*
71+
* Rounds 2.5 and 1.6 to 2, 1.5 and 1.4 to 1.
72+
*/
73+
const ROUND_HALF_DOWN = \NumberFormatter::ROUND_HALFDOWN;
74+
2675
const FRACTIONAL = 'fractional';
2776
const INTEGER = 'integer';
2877

@@ -31,6 +80,8 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface
3180
self::INTEGER,
3281
];
3382

83+
protected $roundingMode;
84+
3485
private $type;
3586
private $scale;
3687

@@ -42,7 +93,7 @@ class PercentToLocalizedStringTransformer implements DataTransformerInterface
4293
*
4394
* @throws UnexpectedTypeException if the given value of type is unknown
4495
*/
45-
public function __construct(int $scale = null, string $type = null)
96+
public function __construct(int $scale = null, string $type = null, ?int $roundingMode = self::ROUND_HALF_UP)
4697
{
4798
if (null === $scale) {
4899
$scale = 0;
@@ -52,12 +103,17 @@ public function __construct(int $scale = null, string $type = null)
52103
$type = self::FRACTIONAL;
53104
}
54105

106+
if (null === $roundingMode) {
107+
$roundingMode = self::ROUND_HALF_UP;
108+
}
109+
55110
if (!\in_array($type, self::$types, true)) {
56111
throw new UnexpectedTypeException($type, implode('", "', self::$types));
57112
}
58113

59114
$this->type = $type;
60115
$this->scale = $scale;
116+
$this->roundingMode = $roundingMode;
61117
}
62118

63119
/**
@@ -166,7 +222,7 @@ public function reverseTransform($value)
166222
}
167223
}
168224

169-
return $result;
225+
return $this->round($result);
170226
}
171227

172228
/**
@@ -179,7 +235,58 @@ protected function getNumberFormatter()
179235
$formatter = new \NumberFormatter(\Locale::getDefault(), \NumberFormatter::DECIMAL);
180236

181237
$formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->scale);
238+
$formatter->setAttribute(\NumberFormatter::ROUNDING_MODE, $this->roundingMode);
182239

183240
return $formatter;
184241
}
242+
243+
/**
244+
* Rounds a number according to the configured scale and rounding mode.
245+
*
246+
* @param int|float $number A number
247+
*
248+
* @return int|float The rounded number
249+
*/
250+
private function round($number)
251+
{
252+
if (null !== $this->scale && null !== $this->roundingMode) {
253+
// shift number to maintain the correct scale during rounding
254+
$roundingCoef = pow(10, $this->scale);
255+
256+
if (self::FRACTIONAL == $this->type) {
257+
$roundingCoef *= 100;
258+
}
259+
260+
// string representation to avoid rounding errors, similar to bcmul()
261+
$number = (string) ($number * $roundingCoef);
262+
263+
switch ($this->roundingMode) {
264+
case self::ROUND_CEILING:
265+
$number = ceil($number);
266+
break;
267+
case self::ROUND_FLOOR:
268+
$number = floor($number);
269+
break;
270+
case self::ROUND_UP:
271+
$number = $number > 0 ? ceil($number) : floor($number);
272+
break;
273+
case self::ROUND_DOWN:
274+
$number = $number > 0 ? floor($number) : ceil($number);
275+
break;
276+
case self::ROUND_HALF_EVEN:
277+
$number = round($number, 0, PHP_ROUND_HALF_EVEN);
278+
break;
279+
case self::ROUND_HALF_UP:
280+
$number = round($number, 0, PHP_ROUND_HALF_UP);
281+
break;
282+
case self::ROUND_HALF_DOWN:
283+
$number = round($number, 0, PHP_ROUND_HALF_DOWN);
284+
break;
285+
}
286+
287+
$number = 1 === $roundingCoef ? (int) $number : $number / $roundingCoef;
288+
}
289+
290+
return $number;
291+
}
185292
}

‎src/Symfony/Component/Form/Extension/Core/Type/PercentType.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Extension/Core/Type/PercentType.php
+16-2Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Form\Extension\Core\Type;
1313

1414
use Symfony\Component\Form\AbstractType;
15+
use Symfony\Component\Form\Extension\Core\DataTransformer\NumberToLocalizedStringTransformer;
1516
use Symfony\Component\Form\Extension\Core\DataTransformer\PercentToLocalizedStringTransformer;
1617
use Symfony\Component\Form\FormBuilderInterface;
1718
use Symfony\Component\Form\FormInterface;
@@ -25,7 +26,11 @@ class PercentType extends AbstractType
2526
*/
2627
public function buildForm(FormBuilderInterface $builder, array $options)
2728
{
28-
$builder->addViewTransformer(new PercentToLocalizedStringTransformer($options['scale'], $options['type']));
29+
$builder->addViewTransformer(new PercentToLocalizedStringTransformer(
30+
$options['scale'],
31+
$options['type'],
32+
$options['rounding_mode']
33+
));
2934
}
3035

3136
/**
@@ -43,6 +48,7 @@ public function configureOptions(OptionsResolver $resolver)
4348
{
4449
$resolver->setDefaults([
4550
'scale' => 0,
51+
'rounding_mode' => NumberToLocalizedStringTransformer::ROUND_HALF_UP,
4652
'symbol' => '%',
4753
'type' => 'fractional',
4854
'compound' => false,
@@ -52,7 +58,15 @@ public function configureOptions(OptionsResolver $resolver)
5258
'fractional',
5359
'integer',
5460
]);
55-
61+
$resolver->setAllowedValues('rounding_mode', [
62+
NumberToLocalizedStringTransformer::ROUND_FLOOR,
63+
NumberToLocalizedStringTransformer::ROUND_DOWN,
64+
NumberToLocalizedStringTransformer::ROUND_HALF_DOWN,
65+
NumberToLocalizedStringTransformer::ROUND_HALF_EVEN,
66+
NumberToLocalizedStringTransformer::ROUND_HALF_UP,
67+
NumberToLocalizedStringTransformer::ROUND_UP,
68+
NumberToLocalizedStringTransformer::ROUND_CEILING,
69+
]);
5670
$resolver->setAllowedTypes('scale', 'int');
5771
$resolver->setAllowedTypes('symbol', ['bool', 'string']);
5872
}

‎src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/PercentToLocalizedStringTransformerTest.php
+103Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,109 @@ public function testReverseTransform()
7979
$this->assertEquals(2, $transformer->reverseTransform('200'));
8080
}
8181

82+
public function reverseTransformWithRoundingProvider()
83+
{
84+
return [
85+
// towards positive infinity (1.6 -> 2, -1.6 -> -1)
86+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.5', 35, PercentToLocalizedStringTransformer::ROUND_CEILING],
87+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.4', 35, PercentToLocalizedStringTransformer::ROUND_CEILING],
88+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.45', 3.5, PercentToLocalizedStringTransformer::ROUND_CEILING],
89+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.44', 3.5, PercentToLocalizedStringTransformer::ROUND_CEILING],
90+
[null, 0, '34.5', 0.35, PercentToLocalizedStringTransformer::ROUND_CEILING],
91+
[null, 0, '34.4', 0.35, PercentToLocalizedStringTransformer::ROUND_CEILING],
92+
[null, 1, '3.45', 0.035, PercentToLocalizedStringTransformer::ROUND_CEILING],
93+
[null, 1, '3.44', 0.035, PercentToLocalizedStringTransformer::ROUND_CEILING],
94+
// towards negative infinity (1.6 -> 1, -1.6 -> -2)
95+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.5', 34, PercentToLocalizedStringTransformer::ROUND_FLOOR],
96+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.4', 34, PercentToLocalizedStringTransformer::ROUND_FLOOR],
97+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.45', 3.4, PercentToLocalizedStringTransformer::ROUND_FLOOR],
98+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.44', 3.4, PercentToLocalizedStringTransformer::ROUND_FLOOR],
99+
[null, 0, '34.5', 0.34, PercentToLocalizedStringTransformer::ROUND_FLOOR],
100+
[null, 0, '34.4', 0.34, PercentToLocalizedStringTransformer::ROUND_FLOOR],
101+
[null, 1, '3.45', 0.034, PercentToLocalizedStringTransformer::ROUND_FLOOR],
102+
[null, 1, '3.44', 0.034, PercentToLocalizedStringTransformer::ROUND_FLOOR],
103+
// away from zero (1.6 -> 2, -1.6 -> 2)
104+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.5', 35, PercentToLocalizedStringTransformer::ROUND_UP],
105+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.4', 35, PercentToLocalizedStringTransformer::ROUND_UP],
106+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.45', 3.5, PercentToLocalizedStringTransformer::ROUND_UP],
107+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.44', 3.5, PercentToLocalizedStringTransformer::ROUND_UP],
108+
[null, 0, '34.5', 0.35, PercentToLocalizedStringTransformer::ROUND_UP],
109+
[null, 0, '34.4', 0.35, PercentToLocalizedStringTransformer::ROUND_UP],
110+
[null, 1, '3.45', 0.035, PercentToLocalizedStringTransformer::ROUND_UP],
111+
[null, 1, '3.44', 0.035, PercentToLocalizedStringTransformer::ROUND_UP],
112+
// towards zero (1.6 -> 1, -1.6 -> -1)
113+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.5', 34, PercentToLocalizedStringTransformer::ROUND_DOWN],
114+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.4', 34, PercentToLocalizedStringTransformer::ROUND_DOWN],
115+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.45', 3.4, PercentToLocalizedStringTransformer::ROUND_DOWN],
116+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.44', 3.4, PercentToLocalizedStringTransformer::ROUND_DOWN],
117+
[PercentToLocalizedStringTransformer::INTEGER, 2, '37.37', 37.37, PercentToLocalizedStringTransformer::ROUND_DOWN],
118+
[PercentToLocalizedStringTransformer::INTEGER, 2, '2.01', 2.01, PercentToLocalizedStringTransformer::ROUND_DOWN],
119+
[null, 0, '34.5', 0.34, PercentToLocalizedStringTransformer::ROUND_DOWN],
120+
[null, 0, '34.4', 0.34, PercentToLocalizedStringTransformer::ROUND_DOWN],
121+
[null, 1, '3.45', 0.034, PercentToLocalizedStringTransformer::ROUND_DOWN],
122+
[null, 1, '3.44', 0.034, PercentToLocalizedStringTransformer::ROUND_DOWN],
123+
[null, 2, '37.37', 0.3737, PercentToLocalizedStringTransformer::ROUND_DOWN],
124+
[null, 2, '2.01', 0.0201, PercentToLocalizedStringTransformer::ROUND_DOWN],
125+
// round halves (.5) to the next even number
126+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.6', 35, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
127+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.5', 34, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
128+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.4', 34, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
129+
[PercentToLocalizedStringTransformer::INTEGER, 0, '33.5', 34, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
130+
[PercentToLocalizedStringTransformer::INTEGER, 0, '32.5', 32, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
131+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.46', 3.5, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
132+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.45', 3.4, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
133+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.44', 3.4, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
134+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.35', 3.4, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
135+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.25', 3.2, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
136+
[null, 0, '34.6', 0.35, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
137+
[null, 0, '34.5', 0.34, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
138+
[null, 0, '34.4', 0.34, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
139+
[null, 0, '33.5', 0.34, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
140+
[null, 0, '32.5', 0.32, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
141+
[null, 1, '3.46', 0.035, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
142+
[null, 1, '3.45', 0.034, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
143+
[null, 1, '3.44', 0.034, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
144+
[null, 1, '3.35', 0.034, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
145+
[null, 1, '3.25', 0.032, PercentToLocalizedStringTransformer::ROUND_HALF_EVEN],
146+
// round halves (.5) away from zero
147+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.6', 35, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
148+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.5', 35, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
149+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.4', 34, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
150+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.46', 3.5, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
151+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.45', 3.5, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
152+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.44', 3.4, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
153+
[null, 0, '34.6', 0.35, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
154+
[null, 0, '34.5', 0.35, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
155+
[null, 0, '34.4', 0.34, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
156+
[null, 1, '3.46', 0.035, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
157+
[null, 1, '3.45', 0.035, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
158+
[null, 1, '3.44', 0.034, PercentToLocalizedStringTransformer::ROUND_HALF_UP],
159+
// round halves (.5) towards zero
160+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.6', 35, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
161+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.5', 34, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
162+
[PercentToLocalizedStringTransformer::INTEGER, 0, '34.4', 34, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
163+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.46', 3.5, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
164+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.45', 3.4, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
165+
[PercentToLocalizedStringTransformer::INTEGER, 1, '3.44', 3.4, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
166+
[null, 0, '34.6', 0.35, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
167+
[null, 0, '34.5', 0.34, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
168+
[null, 0, '34.4', 0.34, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
169+
[null, 1, '3.46', 0.035, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
170+
[null, 1, '3.45', 0.034, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
171+
[null, 1, '3.44', 0.034, PercentToLocalizedStringTransformer::ROUND_HALF_DOWN],
172+
];
173+
}
174+
175+
/**
176+
* @dataProvider reverseTransformWithRoundingProvider
177+
*/
178+
public function testReverseTransformWithRounding($type, $scale, $input, $output, $roundingMode)
179+
{
180+
$transformer = new PercentToLocalizedStringTransformer($scale, $type, $roundingMode);
181+
182+
$this->assertSame($output, $transformer->reverseTransform($input));
183+
}
184+
82185
public function testReverseTransformEmpty()
83186
{
84187
$transformer = new PercentToLocalizedStringTransformer();

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.