8000 [Form] Correctly round model with PercentType and add a rounding_mode… · symfony/symfony@d97565d · GitHub
[go: up one dir, main page]

Skip to content

Commit d97565d

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

File tree

4 files changed

+229
-4
lines changed

4 files changed

+229
-4
lines changed

src/Symfony/Component/Form/CHANGELOG.md

Lines 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

Lines 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

Lines 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

Lines 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