CountryCurrency::isNumberValid()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
nc 2
nop 1
dl 0
loc 7
rs 10
c 1
b 0
f 0
1
<?php
2
/**
3
 * @package Localization
4
 * @subpackage Currencies
5
 */
6
7
declare(strict_types=1);
8
9
namespace AppLocalize\Localization\Countries;
10
11
use AppLocalize\Localization\Currencies\CurrencyInterface;
12
use AppLocalize\Localization\Currencies\CurrencyNumberInfo;
13
use AppLocalize\Localization_Exception;
14
15
/**
16
 * @package Localization
17
 * @subpackage Currencies
18
 */
19
class CountryCurrency implements CurrencyInterface
20
{
21
    private CurrencyInterface $currency;
22
    private CountryInterface $country;
23
24
    public function __construct(CurrencyInterface $currency, CountryInterface $country)
25
    {
26
        $this->currency = $currency;
27
        $this->country = $country;
28
    }
29
30
    public function getCountry(): CountryInterface
31
    {
32
        return $this->country;
33
    }
34
35
    public function getID(): string
36
    {
37
        return $this->currency->getID();
38
    }
39
40
    public function getCountries(): array
41
    {
42
        return $this->currency->getCountries();
43
    }
44
45
    public function getSingular(): string
46
    {
47
        return $this->currency->getSingular();
48
    }
49
50
    public function getPlural(): string
51
    {
52
        return $this->currency->getPlural();
53
    }
54
55
    public function getSymbol(): string
56
    {
57
        return $this->currency->getSymbol();
58
    }
59
60
    public function getPreferredSymbol() : string
61
    {
62
        return $this->currency->getPreferredSymbol();
63
    }
64
65
    public function isNamePreferred(): bool
66
    {
67
        return $this->currency->isNamePreferred();
68
    }
69
70
    public function getStructuralTemplate(?CountryInterface $country=null): string
71
    {
72
        return $this->currency->getStructuralTemplate($this->country);
73
    }
74
75
    /**
76
     * Checks if the specified number string is a valid
77
     * numeric notation for this currency.
78
     * @param string|number $number
79
     * @return bool
80
     */
81
    public function isNumberValid($number) : bool
82
    {
83
        if (empty($number)) {
84
            return true;
85
        }
86
87
        return preg_match($this->getRegex(), (string)$number) !== false;
88
    }
89
90
    protected $regex = '/\A([0-9%1$s]+)\z|([0-9%1$s]+),-\z|([0-9%1$s]+)[%2$s]([0-9]+)\z/s';
91
    protected $regexGBP = '/\A([0-9%1$s]+)\z|([0-9%1$s]+)[%2$s]([0-9]+)\z/s';
92
    protected $regexUSD = '/\A([0-9%1$s]+)\z|([0-9%1$s]+)[%2$s]([0-9]+)\z/s';
93
94
    /**
95
     * @return string
96
     * @throws Localization_Exception
97
     */
98
    protected function getRegex() : string
99
    {
100
        if (!isset($this->regex)) {
101
            throw new Localization_Exception(
102
                'No regex defined',
103
                sprintf(
104
                    'To use this method, set the regex class property for currency %1$s.',
105
                    $this->getID()
106
                )
107
            );
108
        }
109
110
        if (!isset($this->cachedRegex)) {
111
            $this->cachedRegex = sprintf(
0 ignored issues
show
Bug Best Practice introduced by
The property cachedRegex does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
112
                $this->regex,
113
                $this->getThousandsSeparator(),
114
                $this->getDecimalsSeparator()
115
            );
116
        }
117
118
        return $this->cachedRegex;
119
    }
120
121
    public function getISO(): string
122
    {
123
        return $this->currency->getISO();
124
    }
125
126
    public function getFormatHint(): ?string
127
    {
128
        return null;
129
    }
130
131
    /**
132
     * Returns examples of the currency's numeric notation, as
133
     * an indexed array with examples which are used in forms
134
     * as input help for users.
135
     *
136
     * The optional parameter sets how many decimal positions
137
     * should be included in the examples.
138
     *
139
     * @param int $decimalPositions
140
     * @return string[]
141
     */
142
    public function getExamples(int $decimalPositions = 0) : array
143
    {
144
        $dSep = $this->getDecimalsSeparator();
145
        $tSep = $this->getThousandsSeparator();
146
147
        $decimals = '25874125486589953255847851252585';
148
        $examples = array();
149
        $examples[] = '50';
150
        $examples[] = sprintf('50%1$s-', $dSep);
151
        $examples[] = '1500';
152
        $examples[] = sprintf('1%1$s500', $tSep);
153
154
        if ($decimalPositions > 0) {
155
            $examples[] = sprintf(
156
                '50%1$s%2$s',
157
                $dSep,
158
                substr($decimals, 0, $decimalPositions)
159
            );
160
            $examples[] = sprintf(
161
                '1%1$s500%2$s%3$s',
162
                $tSep,
163
                $dSep,
164
                substr($decimals, 0, $decimalPositions)
165
            );
166
        }
167
168
        return $examples;
169
    }
170
171
    public function tryParseNumber($number)
172
    {
173
        $parts = explode('.', $this->normalizeNumber($number));
174
175
        if (count($parts) > 1)
176
        {
177
            $decimals = array_pop($parts);
178
            $thousands = implode('', $parts);
179
            if ($decimals === '-') {
180
                $decimals = 0;
181
            }
182
        }
183
        else
184
        {
185
            $decimals = 0;
186
            $thousands = implode('', $parts);
187
        }
188
189
        return new CurrencyNumberInfo((int)$thousands, (int)$decimals);
190
    }
191
192
    /**
193
     * @param int|float|string|NULL $number
194
     * @return string
195
     */
196
    public function normalizeNumber($number) : string
197
    {
198
        if($number === '' || $number === null) {
199
            return '';
200
        }
201
202
        $normalized = str_replace(' ', '', (string)$number);
203
        $dSep = $this->getDecimalsSeparator();
204
        $tSep = $this->getThousandsSeparator();
205
206
        // Handle the case where both classical separators are used,
207
        // independently of the country's specific separators.
208
        if(strpos($normalized, '.') !== false && strpos($normalized, ',') !== false) {
209
            $normalized = str_replace(array('.', ','), '.', $normalized);
210
            $parts = explode('.', $normalized);
211
            $decimals = array_pop($parts);
212
            $normalized = implode('', $parts).$dSep.$decimals;
213
        }
214
215
        // number uses the full notation
216
        if (strpos($normalized, $tSep) !== false && strpos($normalized, $dSep) !== false) {
217
            return str_replace(array($tSep, $dSep), array('', '.'), $normalized);
218
        }
219
220
        return str_replace(',', '.', $normalized);
221
    }
222
223
    public function parseNumber($number) : CurrencyNumberInfo
224
    {
225
        $parsed = $this->tryParseNumber($number);
226
227
        if ($parsed instanceof CurrencyNumberInfo) {
0 ignored issues
show
introduced by
$parsed is always a sub-type of AppLocalize\Localization...cies\CurrencyNumberInfo.
Loading history...
228
            return $parsed;
229
        }
230
231
        throw new Localization_Exception(
232
            'Could not parse number',
233
            sprintf(
234
                'The number [%1$s] did not yield a currency number object.',
235
                $number
236
            )
237
        );
238
    }
239
240
    public function isSymbolOnFront(): bool
241
    {
242
        return $this->currency->isSymbolOnFront();
243
    }
244
245
    public function formatNumber($number, int $decimalPositions = 2) : string
246
    {
247
        return number_format(
248
            (float)$number,
249
            $decimalPositions,
250
            $this->getDecimalsSeparator(),
251
            $this->getThousandsSeparator()
252
        );
253
    }
254
255
    public function getThousandsSeparator(): string
256
    {
257
        return $this->country->getNumberThousandsSeparator();
258
    }
259
260
    public function getDecimalsSeparator(): string
261
    {
262
        return $this->country->getNumberDecimalsSeparator();
263
    }
264
265
    public function makeReadable($number, int $decimalPositions = 2, bool $addSymbol=true) : string
266
    {
267
        if ($number === null || $number === '') {
268
            return '';
269
        }
270
271
        $parsed = $this->parseNumber($number);
272
273
        $number = $this->formatNumber(
274
            str_replace('-', '', $parsed->getString()),
275
            $decimalPositions
276
        );
277
278
        if(!$addSymbol) {
279
            return $number;
280
        }
281
282
        $sign = '';
283
        if($parsed->isNegative()) {
284
            $sign = '-';
285
        }
286
287
        $replace = array(
288
            '{symbol}' => $this->getPreferredSymbol(),
289
            '{amount}' => $number,
290
            '-' => $sign
291
        );
292
293
        return trim(str_replace(
294
            array_keys($replace),
295
            array_values($replace),
296
            $this->getStructuralTemplate()
297
        ));
298
    }
299
300
    public function __toString()
301
    {
302
        return (string)$this->currency;
303
    }
304
}
305