Passed
Push — master ( 0d4dfe...99ef14 )
by Vincent
07:47 queued 11s
created

LocalizedNumberTransformer   A

Complexity

Total Complexity 29

Size/Duplication

Total Lines 190
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 29
eloc 67
c 1
b 0
f 1
dl 0
loc 190
ccs 62
cts 62
cp 1
rs 10

7 Methods

Rating   Name   Duplication   Size   Complexity  
C round() 0 34 12
B transformFromHttp() 0 30 8
A getNumberFormatter() 0 12 2
A checkError() 0 4 2
A __construct() 0 6 1
A transformToHttp() 0 16 3
A cast() 0 3 1
1
<?php
2
3
namespace Bdf\Form\Leaf\Transformer;
4
5
use Bdf\Form\ElementInterface;
6
use Bdf\Form\Transformer\TransformerInterface;
7
use InvalidArgumentException;
8
use Locale;
9
use NumberFormatter;
10
11
/**
12
 * Transformer localized string number to native PHP number (int or double)
13
 *
14
 * Inspired from : https://github.com/symfony/symfony/blob/5.x/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php
15
 *
16
 * @template T as numeric
17
 */
18
class LocalizedNumberTransformer implements TransformerInterface
19
{
20
    /**
21
     * Number of digit to keep after the comma
22
     *
23
     * @var int|null
24
     */
25
    private $scale;
26
27
    /**
28
     * @var int
29
     * @psalm-var NumberFormatter::ROUND_*
30
     */
31
    private $roundingMode;
32
33
    /**
34
     * Group by thousand or not
35
     *
36
     * @var bool
37
     */
38
    private $grouping;
39
40
    /**
41
     * The locale to use
42
     * null for use the current locale
43
     *
44
     * @var string|null
45
     */
46
    private $locale;
47
48
    /**
49
     * LocalizedNumberTransformer constructor.
50
     *
51
     * @param int|null $scale Number of digit to keep after the comma. Null to keep all digits (do not round)
52
     * @param bool $grouping Group by thousand or not
53
     * @param NumberFormatter::ROUND_* $roundingMode
54
     * @param string|null $locale The locale to use. null for use the current locale
55
     */
56 204
    public function __construct(?int $scale = null, bool $grouping = false, int $roundingMode = NumberFormatter::ROUND_HALFUP, ?string $locale = null)
57
    {
58 204
        $this->scale = $scale;
59 204
        $this->grouping = $grouping;
60 204
        $this->roundingMode = $roundingMode;
61 204
        $this->locale = $locale;
62 204
    }
63
64
    /**
65
     * {@inheritdoc}
66
     *
67
     * @throws InvalidArgumentException If the given value is not numeric or cannot be formatted
68
     */
69 58
    final public function transformToHttp($value, ElementInterface $input): ?string
70
    {
71 58
        if ($value === null) {
72 7
            return null;
73
        }
74
75 52
        if (!is_numeric($value)) {
76 8
            throw new InvalidArgumentException('Expected a numeric or null.');
77
        }
78
79 44
        $formatter = $this->getNumberFormatter();
80 44
        $value = $formatter->format($value);
81
82 44
        $this->checkError($formatter);
83
84 44
        return $value;
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     *
90
     * @return T|null The numeric value
0 ignored issues
show
Bug introduced by
The type Bdf\Form\Leaf\Transformer\T was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
91
     *
92
     * @throws InvalidArgumentException If the given value is not scalar or cannot be parsed
93
     */
94 135
    final public function transformFromHttp($value, ElementInterface $input)
95
    {
96 135
        if ($value !== null && !is_scalar($value)) {
97 4
            throw new InvalidArgumentException('Expected a scalar or null.');
98
        }
99
100 131
        if ($value === null || $value === '') {
101 12
            return null;
102
        }
103
104 119
        $value = (string) $value;
105
106 119
        $formatter = $this->getNumberFormatter();
107 119
        $decSep = $formatter->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
108
109
        // Normalize "standard" decimal format to locale format
110 119
        if ($decSep !== '.') {
111 39
            $value = str_replace('.', $decSep, $value);
112
        }
113
114 119
        if (str_contains($value, $decSep)) {
115 50
            $type = NumberFormatter::TYPE_DOUBLE;
116
        } else {
117 72
            $type = PHP_INT_SIZE === 8 ? NumberFormatter::TYPE_INT64 : NumberFormatter::TYPE_INT32;
118
        }
119
120 119
        $result = $formatter->parse($value, $type);
121 119
        $this->checkError($formatter);
122
123 111
        return $this->cast($this->round($result));
124
    }
125
126
    /**
127
     * Create the NumberFormatter instance
128
     *
129
     * @return NumberFormatter
130
     */
131 149
    private function getNumberFormatter(): NumberFormatter
132
    {
133 149
        $formatter = new NumberFormatter($this->locale ?? Locale::getDefault(), NumberFormatter::DECIMAL);
134
135 149
        if (null !== $this->scale) {
136 120
            $formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $this->scale);
137 120
            $formatter->setAttribute(NumberFormatter::ROUNDING_MODE, $this->roundingMode);
138
        }
139
140 149
        $formatter->setAttribute(NumberFormatter::GROUPING_USED, $this->grouping);
0 ignored issues
show
Bug introduced by
$this->grouping of type boolean is incompatible with the type integer expected by parameter $value of NumberFormatter::setAttribute(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

140
        $formatter->setAttribute(NumberFormatter::GROUPING_USED, /** @scrutinizer ignore-type */ $this->grouping);
Loading history...
141
142 149
        return $formatter;
143
    }
144
145
    /**
146
     * Cast the number to the desired type
147
     *
148
     * @return T
149
     */
150 48
    protected function cast($value)
151
    {
152 48
        return $value;
153
    }
154
155
    /**
156
     * Rounds a number according to the configured scale and rounding mode
157
     *
158
     * @param int|float $number A number
159
     *
160
     * @return int|float The rounded number
161
     */
162 111
    private function round($number)
163
    {
164 111
        if (is_int($number) || $this->scale === null) {
165 76
            return $number;
166
        }
167
168 35
        switch ($this->roundingMode) {
169
            case NumberFormatter::ROUND_HALFEVEN:
170 6
                return round($number, $this->scale, PHP_ROUND_HALF_EVEN);
171
            case NumberFormatter::ROUND_HALFUP:
172 5
                return round($number, $this->scale, PHP_ROUND_HALF_UP);
173
            case NumberFormatter::ROUND_HALFDOWN:
174 4
                return round($number, $this->scale, PHP_ROUND_HALF_DOWN);
175
        }
176
177 20
        $coef = 10 ** $this->scale;
178 20
        $number *= $coef;
179
180 20
        switch ($this->roundingMode) {
181
            case NumberFormatter::ROUND_CEILING:
182 4
                $number = ceil($number);
183 4
                break;
184
            case NumberFormatter::ROUND_FLOOR:
185 4
                $number = floor($number);
186 4
                break;
187
            case NumberFormatter::ROUND_UP:
188 6
                $number = $number > 0 ? ceil($number) : floor($number);
189 6
                break;
190
            case NumberFormatter::ROUND_DOWN:
191 6
                $number = $number > 0 ? floor($number) : ceil($number);
192 6
                break;
193
        }
194
195 20
        return $number / $coef;
196
    }
197
198
    /**
199
     * Check if the formatter is in error state, and throw exception
200
     *
201
     * @param NumberFormatter $formatter
202
     * @throws InvalidArgumentException If the formatter has an error
203
     */
204 149
    private function checkError(NumberFormatter $formatter): void
205
    {
206 149
        if (intl_is_failure($formatter->getErrorCode())) {
207 8
            throw new InvalidArgumentException($formatter->getErrorMessage());
208
        }
209 141
    }
210
}
211