Passed
Push — master ( eada73...ab684d )
by Alexander
07:23
created

Locale   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 450
Duplicated Lines 0 %

Test Coverage

Coverage 74.51%

Importance

Changes 0
Metric Value
wmc 56
eloc 142
dl 0
loc 450
ccs 114
cts 153
cp 0.7451
rs 5.5199
c 0
b 0
f 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
F asString() 0 56 14
A numbers() 0 3 1
A variant() 0 3 1
A withExtendedLanguage() 0 6 1
A __toString() 0 3 1
A calendar() 0 3 1
A withPrivate() 0 6 1
A region() 0 3 1
A collation() 0 3 1
A withNumbers() 0 5 1
F __construct() 0 56 16
A withCollation() 0 5 1
A withLanguage() 0 5 1
A private() 0 3 1
A script() 0 3 1
A withCalendar() 0 5 1
A withRegion() 0 5 1
A extendedLanguage() 0 3 1
A getBCP47Regex() 0 16 1
A currency() 0 3 1
A fallbackLocale() 0 23 4
A withVariant() 0 5 1
A language() 0 3 1
A withScript() 0 5 1
A withCurrency() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Locale often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Locale, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\I18n;
6
7
/**
8
 * Locale stores locale information created from BCP 47 formatted string
9
 * https://tools.ietf.org/html/bcp47
10
 */
11
final class Locale
12
{
13
    /**
14
     * @var string|null Two-letter ISO-639-2 language code
15
     * @see http://www.loc.gov/standards/iso639-2/
16
     */
17
    private $language;
18
19
    /**
20
     * @var string|null extended language subtags
21
     */
22
    private $extendedLanguage;
23
24
    /**
25
     * @var string|null
26
     */
27
    private $extension;
28
29
    /**
30
     * @var string|null Four-letter ISO 15924 script code
31
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
32
     */
33
    private $script;
34
35
    /**
36
     * @var string|null Two-letter ISO 3166-1 country code
37
     * @see https://www.iso.org/iso-3166-country-codes.html
38
     */
39
    private $region;
40
41
    /**
42
     * @var string|null variant of language conventions to use
43
     */
44
    private $variant;
45
46
    /**
47
     * @var string|null ICU currency
48
     */
49
    private $currency;
50
51
    /**
52
     * @var string|null ICU calendar
53
     */
54
    private $calendar;
55
56
    /**
57
     * @var string ICU collation
58
     */
59
    private $collation;
60
61
    /**
62
     * @var string|null ICU numbers
63
     */
64
    private $numbers;
65
66
    /**
67
     * @var string|null
68
     */
69
    private $grandfathered;
70
71
    /**
72
     * @var string|null
73
     */
74
    private $private;
75
76
    /**
77
     * Locale constructor.
78
     * @param string $localeString BCP 47 formatted locale string
79
     * @see https://tools.ietf.org/html/bcp47
80
     * @throws \InvalidArgumentException
81
     */
82 10
    public function __construct(string $localeString)
83
    {
84 10
        if (!preg_match(static::getBCP47Regex(), $localeString, $matches)) {
85 1
            throw new \InvalidArgumentException($localeString . ' is not valid BCP 47 formatted locale string');
86
        }
87
88 9
        if (!empty($matches['language'])) {
89 8
            $this->language = strtolower($matches['language']);
90
        }
91
92 9
        if (!empty($matches['region'])) {
93 6
            $this->region = strtoupper($matches['region']);
94
        }
95
96 9
        if (!empty($matches['variant'])) {
97 4
            $this->variant = $matches['variant'];
98
        }
99
100 9
        if (!empty($matches['extendedLanguage'])) {
101
            $this->extendedLanguage = $matches['extendedLanguage'];
102
        }
103
104 9
        if (!empty($matches['extension'])) {
105 1
            $this->extension = $matches['extension'];
106
        }
107
108 9
        if (!empty($matches['script'])) {
109 1
            $this->script = ucfirst(strtolower($matches['script']));
110
        }
111
112 9
        if (!empty($matches['grandfathered'])) {
113
            $this->grandfathered = $matches['grandfathered'];
114
        }
115
116 9
        if (!empty($matches['private'])) {
117 4
            $this->private = preg_replace('~^x-~', '', $matches['private']);
118
        }
119
120 9
        if (!empty($matches['keywords'])) {
121
            foreach (explode(';', $matches['keywords']) as $pair) {
122
                [$key, $value] = explode('=', $pair);
123
124
                if ($key === 'calendar') {
125
                    $this->calendar = $value;
126
                }
127
128
                if ($key === 'collation') {
129
                    $this->collation = $value;
130
                }
131
132
                if ($key === 'currency') {
133
                    $this->currency = $value;
134
                }
135
136
                if ($key === 'numbers') {
137
                    $this->numbers = $value;
138
                }
139
            }
140
        }
141 9
    }
142
143
    /**
144
     * @return string Four-letter ISO 15924 script code
145
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
146
     */
147 1
    public function script(): ?string
148
    {
149 1
        return $this->script;
150
    }
151
152
    /**
153
     * @param null|string $script Four-letter ISO 15924 script code
154
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
155
     * @return self
156
     */
157
    public function withScript(?string $script): self
158
    {
159
        $new = clone $this;
160
        $new->script = $script;
161
        return $new;
162
    }
163
164
165
    /**
166
     * @return string variant of language conventions to use
167
     */
168 2
    public function variant(): ?string
169
    {
170 2
        return $this->variant;
171
    }
172
173
    /**
174
     * @param null|string $variant variant of language conventions to use
175
     * @return self
176
     */
177 1
    public function withVariant(?string $variant): self
178
    {
179 1
        $new = clone $this;
180 1
        $new->variant = $variant;
181 1
        return $new;
182
    }
183
184
    /**
185
     * @return string|null Two-letter ISO-639-2 language code
186
     * @see http://www.loc.gov/standards/iso639-2/
187
     */
188 5
    public function language(): string
189
    {
190 5
        return $this->language;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->language could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
191
    }
192
193
    /**
194
     * @param null|string $language Two-letter ISO-639-2 language code
195
     * @see http://www.loc.gov/standards/iso639-2/
196
     * @return self
197
     */
198 1
    public function withLanguage(?string $language): self
199
    {
200 1
        $new = clone $this;
201 1
        $new->language = $language;
202 1
        return $new;
203
    }
204
205
    /**
206
     * @return null|string ICU calendar
207
     */
208
    public function calendar(): ?string
209
    {
210
        return $this->calendar;
211
    }
212
213
    /**
214
     * @param null|string $calendar ICU calendar
215
     * @return self
216
     */
217 1
    public function withCalendar(?string $calendar): self
218
    {
219 1
        $new = clone $this;
220 1
        $new->calendar = $calendar;
221 1
        return $new;
222
    }
223
224
225
    /**
226
     * @return null|string ICU collation
227
     */
228
    public function collation(): ?string
229
    {
230
        return $this->collation;
231
    }
232
233
    /**
234
     * @param null|string $collation ICU collation
235
     * @return self
236
     */
237 1
    public function withCollation(?string $collation): self
238
    {
239 1
        $new = clone $this;
240 1
        $new->collation = $collation;
241 1
        return $new;
242
    }
243
244
    /**
245
     * @return null|string ICU numbers
246
     */
247
    public function numbers(): ?string
248
    {
249
        return $this->numbers;
250
    }
251
252
    /**
253
     * @param null|string $numbers ICU numbers
254
     * @return self
255
     */
256 1
    public function withNumbers(?string $numbers): self
257
    {
258 1
        $new = clone $this;
259 1
        $new->numbers = $numbers;
260 1
        return $new;
261
    }
262
263
    /**
264
     * @return string Two-letter ISO 3166-1 country code
265
     * @see https://www.iso.org/iso-3166-country-codes.html
266
     */
267 3
    public function region(): ?string
268
    {
269 3
        return $this->region;
270
    }
271
272
    /**
273
     * @param null|string $region Two-letter ISO 3166-1 country code
274
     * @see https://www.iso.org/iso-3166-country-codes.html
275
     * @return self
276
     */
277 1
    public function withRegion(?string $region): self
278
    {
279 1
        $new = clone $this;
280 1
        $new->region = $region;
281 1
        return $new;
282
    }
283
284
    /**
285
     * @return string ICU currency
286
     */
287
    public function currency(): ?string
288
    {
289
        return $this->currency;
290
    }
291
292
    /**
293
     * @param null|string $currency ICU currency
294
     * @return self
295
     */
296 1
    public function withCurrency(?string $currency): self
297
    {
298 1
        $new = clone $this;
299 1
        $new->currency = $currency;
300
301 1
        return $new;
302
    }
303
304
    /**
305
     * @return null|string extended language subtags
306
     */
307
    public function extendedLanguage(): ?string
308
    {
309
        return $this->extendedLanguage;
310
    }
311
312
    /**
313
     * @param null|string $extendedLanguage extended language subtags
314
     * @return self
315
     */
316 1
    public function withExtendedLanguage(?string $extendedLanguage): self
317
    {
318 1
        $new = clone $this;
319 1
        $new->extendedLanguage = $extendedLanguage;
320
321 1
        return $new;
322
    }
323
324
325
    /**
326
     * @return null|string
327
     */
328 2
    public function private(): ?string
329
    {
330 2
        return $this->private;
331
    }
332
333
    /**
334
     * @param null|string $private
335
     * @return self
336
     */
337 2
    public function withPrivate(?string $private): self
338
    {
339 2
        $new = clone $this;
340 2
        $new->private = $private;
341
342 2
        return $new;
343
    }
344
345
    /**
346
     * @return string regular expression for parsing BCP 47
347
     * @see https://tools.ietf.org/html/bcp47
348
     */
349 10
    private static function getBCP47Regex(): string
350
    {
351 10
        $regular = '(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)';
352 10
        $irregular = '(?:en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)';
353 10
        $grandfathered = '(?<grandfathered>' . $irregular . '|' . $regular . ')';
354 10
        $private = '(?<private>x(?:-[A-Za-z0-9]{1,8})+)';
355 10
        $singleton = '[0-9A-WY-Za-wy-z]';
356 10
        $extension = '(?<extension>' . $singleton . '(?:-[A-Za-z0-9]{2,8})+)';
357 10
        $variant = '(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})';
358 10
        $region = '(?<region>[A-Za-z]{2}|[0-9]{3})';
359 10
        $script = '(?<script>[A-Za-z]{4})';
360 10
        $extendedLanguage = '(?<extendedLanguage>[A-Za-z]{3}(?:-[A-Za-z]{3}){0,2})';
361 10
        $language = '(?<language>[A-Za-z]{4,8})|(?<language>[A-Za-z]{2,3})(?:-' . $extendedLanguage . ')?';
362 10
        $icuKeywords = '(?:@(?<keywords>.*?))?';
363 10
        $languageTag = '(?:' . $language . '(?:-' . $script . ')?' . '(?:-' . $region . ')?' . '(?:-' . $variant . ')*' . '(?:-' . $extension . ')*' . '(?:-' . $private . ')?' . ')';
364 10
        return '/^(?J:' . $grandfathered . '|' . $languageTag . '|' . $private . ')' . $icuKeywords . '$/';
365
    }
366
367
    public function __toString(): string
368
    {
369
        return $this->asString();
370
    }
371
372
    /**
373
     * @return string
374
     */
375 2
    public function asString(): string
376
    {
377 2
        if ($this->grandfathered !== null) {
378
            return $this->grandfathered;
379
        }
380
381 2
        $result = [];
382 2
        if ($this->language !== null) {
383 2
            $result[] = $this->language;
384
385 2
            if ($this->extendedLanguage !== null) {
386
                $result[] = $this->extendedLanguage;
387
            }
388
389 2
            if ($this->script !== null) {
390
                $result[] = $this->script;
391
            }
392
393 2
            if ($this->region !== null) {
394 2
                $result[] = $this->region;
395
            }
396
397 2
            if ($this->variant !== null) {
398 1
                $result[] = $this->variant;
399
            }
400
401 2
            if ($this->extension !== null) {
402 1
                $result[] = $this->extension;
403
            }
404
        }
405
406 2
        if ($this->private !== null) {
407 1
            $result[] = 'x-' . $this->private;
408
        }
409
410 2
        $keywords = [];
411 2
        if ($this->currency !== null) {
412
            $keywords[] = 'currency=' . $this->currency;
413
        }
414 2
        if ($this->collation !== null) {
415
            $keywords[] = 'collation=' . $this->collation;
416
        }
417 2
        if ($this->calendar !== null) {
418
            $keywords[] = 'calendar=' . $this->calendar;
419
        }
420 2
        if ($this->numbers !== null) {
421
            $keywords[] = 'numbers=' . $this->numbers;
422
        }
423
424 2
        $string = implode('-', $result);
425
426 2
        if ($keywords !== []) {
427
            $string .= '@' . implode(';', $keywords);
428
        }
429
430 2
        return $string;
431
    }
432
433
    /**
434
     * Returns fallback locale
435
     *
436
     * @return self fallback locale
437
     */
438 1
    public function fallbackLocale(): self
439
    {
440
        $fallback = $this
441 1
            ->withCalendar(null)
442 1
            ->withCollation(null)
443 1
            ->withCurrency(null)
444 1
            ->withExtendedLanguage(null)
445 1
            ->withNumbers(null)
446 1
            ->withPrivate(null);
447
448 1
        if ($fallback->variant() !== null) {
449 1
            return $fallback->withVariant(null);
450
        }
451
452 1
        if ($fallback->region() !== null) {
453 1
            return $fallback->withRegion(null);
454
        }
455
456
        if ($fallback->script() !== null) {
457
            return $fallback->withScript(null);
458
        }
459
460
        return $fallback;
461
    }
462
}
463