Passed
Push — master ( 4c4c5d...6cc026 )
by Alexander
07:21 queued 04:58
created

Locale::hours()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\I18n;
6
7
use InvalidArgumentException;
8
9
/**
10
 * Locale stores locale information created from BCP 47 formatted string.
11
 *
12
 * @see https://tools.ietf.org/html/bcp47
13
 */
14
final class Locale
15
{
16
    /**
17
     * @var string|null Two-letter ISO-639-2 language code.
18
     *
19
     * @see http://www.loc.gov/standards/iso639-2/
20
     */
21
    private ?string $language = null;
22
23
    /**
24
     * @var string|null Extended language subtags.
25
     */
26
    private ?string $extendedLanguage = null;
27
28
    /**
29
     * @var string|null
30
     */
31
    private ?string $extension = null;
32
33
    /**
34
     * @var string|null Four-letter ISO 15924 script code.
35
     *
36
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
37
     */
38
    private ?string $script = null;
39
40
    /**
41
     * @var string|null Two-letter ISO 3166-1 country code.
42
     *
43
     * @see https://www.iso.org/iso-3166-country-codes.html
44
     */
45
    private ?string $region = null;
46
47
    /**
48
     * @var string|null variant of language conventions to use.
49
     */
50
    private ?string $variant = null;
51
52
    /**
53
     * @var string|null ICU currency.
54
     */
55
    private ?string $currency = null;
56
57
    /**
58
     * @var string|null ICU calendar.
59
     */
60
    private ?string $calendar = null;
61
62
    /**
63
     * @var string|null ICU case-first collation.
64
     *
65
     * @see https://unicode-org.github.io/icu/userguide/collation/customization/#casefirst
66
     * @see https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
67
     */
68
    private ?string $colcasefirst = null;
69
70
    /**
71
     * @var string|null ICU collation.
72
     */
73
    private ?string $collation = null;
74
75
    /**
76
     * @var string|null ICU numeric collation.
77
     *
78
     * @see https://unicode-org.github.io/icu/userguide/collation/customization/#numericordering
79
     * @see https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
80
     */
81
    private ?string $colnumeric = null;
82
83
    /**
84
     * @var string|null ICU numbers.
85
     */
86
    private ?string $numbers = null;
87
88
    /**
89
     * @var string|null Unicode hour cycle identifier.
90
     *
91
     * @see https://www.unicode.org/reports/tr35/#UnicodeHourCycleIdentifier
92
     */
93
    private ?string $hours = null;
94
95
    /**
96
     * @var string|null
97
     */
98
    private ?string $grandfathered = null;
99
100
    /**
101
     * @var string|null
102
     */
103
    private ?string $private = null;
104
105
    /**
106
     * Locale constructor.
107
     *
108
     * @param string $localeString BCP 47 formatted locale string.
109
     *
110
     * @see https://tools.ietf.org/html/bcp47
111
     *
112
     * @throws InvalidArgumentException
113
     */
114 34
    public function __construct(string $localeString)
115
    {
116 34
        if (!preg_match(self::getBCP47Regex(), $localeString, $matches)) {
117 1
            throw new InvalidArgumentException($localeString . ' is not valid BCP 47 formatted locale string.');
118
        }
119
120 33
        if (!empty($matches['language'])) {
121 32
            $this->language = strtolower($matches['language']);
122
        }
123
124 33
        if (!empty($matches['region'])) {
125 19
            $this->region = strtoupper($matches['region']);
126
        }
127
128 33
        if (!empty($matches['variant'])) {
129 5
            $this->variant = $matches['variant'];
130
        }
131
132 33
        if (!empty($matches['extendedLanguage'])) {
133 2
            $this->extendedLanguage = $matches['extendedLanguage'];
134
        }
135
136 33
        if (!empty($matches['extension'])) {
137 1
            $this->extension = $matches['extension'];
138
        }
139
140 33
        if (!empty($matches['script'])) {
141 7
            $this->script = ucfirst(strtolower($matches['script']));
142
        }
143
144 33
        if (!empty($matches['grandfathered'])) {
145 1
            $this->grandfathered = $matches['grandfathered'];
146
        }
147
148 33
        if (!empty($matches['private'])) {
149 5
            $this->private = preg_replace('~^x-~', '', $matches['private']);
150
        }
151
152 33
        if (!empty($matches['keywords'])) {
153 8
            foreach (explode(';', $matches['keywords']) as $pair) {
154 8
                [$key, $value] = explode('=', $pair);
155
156 8
                if ($key === 'calendar') {
157 2
                    $this->calendar = $value;
158
                }
159
160 8
                if ($key === 'colcasefirst') {
161 2
                    $this->colcasefirst = $value;
162
                }
163
164 8
                if ($key === 'collation') {
165 2
                    $this->collation = $value;
166
                }
167
168 8
                if ($key === 'colnumeric') {
169 2
                    $this->colnumeric = $value;
170
                }
171
172 8
                if ($key === 'currency') {
173 2
                    $this->currency = $value;
174
                }
175
176 8
                if ($key === 'numbers') {
177 2
                    $this->numbers = $value;
178
                }
179
180 8
                if ($key === 'hours') {
181 2
                    $this->hours = $value;
182
                }
183
            }
184
        }
185 33
    }
186
187
    /**
188
     * @return string Four-letter ISO 15924 script code.
189
     *
190
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
191
     */
192 5
    public function script(): ?string
193
    {
194 5
        return $this->script;
195
    }
196
197
    /**
198
     * @param string|null $script Four-letter ISO 15924 script code.
199
     *
200
     * @see http://www.unicode.org/iso15924/iso15924-codes.html
201
     *
202
     * @return self
203
     */
204 2
    public function withScript(?string $script): self
205
    {
206 2
        $new = clone $this;
207 2
        $new->script = $script;
208 2
        return $new;
209
    }
210
211
    /**
212
     * @return string Variant of language conventions to use.
213
     */
214 4
    public function variant(): ?string
215
    {
216 4
        return $this->variant;
217
    }
218
219
    /**
220
     * @param string|null $variant Variant of language conventions to use.
221
     *
222
     * @return self
223
     */
224 2
    public function withVariant(?string $variant): self
225
    {
226 2
        $new = clone $this;
227 2
        $new->variant = $variant;
228 2
        return $new;
229
    }
230
231
    /**
232
     * @return string|null Two-letter ISO-639-2 language code.
233
     *
234
     * @see http://www.loc.gov/standards/iso639-2/
235
     */
236 3
    public function language(): ?string
237
    {
238 3
        return $this->language;
239
    }
240
241
    /**
242
     * @param string|null $language Two-letter ISO-639-2 language code.
243
     *
244
     * @see http://www.loc.gov/standards/iso639-2/
245
     *
246
     * @return self
247
     */
248 1
    public function withLanguage(?string $language): self
249
    {
250 1
        $new = clone $this;
251 1
        $new->language = $language;
252 1
        return $new;
253
    }
254
255
    /**
256
     * @return string|null ICU calendar.
257
     */
258 2
    public function calendar(): ?string
259
    {
260 2
        return $this->calendar;
261
    }
262
263
    /**
264
     * @param string|null $calendar ICU calendar.
265
     *
266
     * @return self
267
     */
268 3
    public function withCalendar(?string $calendar): self
269
    {
270 3
        $new = clone $this;
271 3
        $new->calendar = $calendar;
272 3
        return $new;
273
    }
274
275
    /**
276
     * @return string|null ICU case-first collation.
277
     *
278
     * @see https://unicode-org.github.io/icu/userguide/collation/customization/#casefirst
279
     * @see https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
280
     */
281 2
    public function colcasefirst(): ?string
282
    {
283 2
        return $this->colcasefirst;
284
    }
285
286
    /**
287
     * @param string|null $colcasefirst ICU case-first collation.
288
     *
289
     * @see https://unicode-org.github.io/icu/userguide/collation/customization/#casefirst
290
     * @see https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
291
     *
292
     * @return self
293
     */
294 3
    public function withColcasefirst(?string $colcasefirst): self
295
    {
296 3
        $new = clone $this;
297 3
        $new->colcasefirst = $colcasefirst;
298 3
        return $new;
299
    }
300
301
    /**
302
     * @return string|null ICU collation.
303
     */
304 2
    public function collation(): ?string
305
    {
306 2
        return $this->collation;
307
    }
308
309
    /**
310
     * @param string|null $collation ICU collation.
311
     *
312
     * @return self
313
     */
314 3
    public function withCollation(?string $collation): self
315
    {
316 3
        $new = clone $this;
317 3
        $new->collation = $collation;
318 3
        return $new;
319
    }
320
321
    /**
322
     * @return string|null ICU numeric collation.
323
     *
324
     * @see https://unicode-org.github.io/icu/userguide/collation/customization/#numericordering
325
     * @see https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
326
     */
327 2
    public function colnumeric(): ?string
328
    {
329 2
        return $this->colnumeric;
330
    }
331
332
    /**
333
     * @param string|null $colnumeric ICU numeric collation.
334
     *
335
     * @see https://unicode-org.github.io/icu/userguide/collation/customization/#numericordering
336
     * @see https://www.unicode.org/reports/tr35/tr35-61/tr35-collation.html#Collation_Settings
337
     *
338
     * @return self
339
     */
340 3
    public function withColnumeric(?string $colnumeric): self
341
    {
342 3
        $new = clone $this;
343 3
        $new->colnumeric = $colnumeric;
344 3
        return $new;
345
    }
346
347
    /**
348
     * @return string|null ICU numbers.
349
     */
350 2
    public function numbers(): ?string
351
    {
352 2
        return $this->numbers;
353
    }
354
355
    /**
356
     * @param string|null $numbers ICU numbers.
357
     *
358
     * @return self
359
     */
360 3
    public function withNumbers(?string $numbers): self
361
    {
362 3
        $new = clone $this;
363 3
        $new->numbers = $numbers;
364 3
        return $new;
365
    }
366
367
    /**
368
     * @return string|null Unicode hour cycle identifier.
369
     *
370
     * @see https://www.unicode.org/reports/tr35/#UnicodeHourCycleIdentifier
371
     */
372 2
    public function hours(): ?string
373
    {
374 2
        return $this->hours;
375
    }
376
377
    /**
378
     * @param string|null $hours Unicode hour cycle identifier.
379
     *
380
     * @see https://www.unicode.org/reports/tr35/#UnicodeHourCycleIdentifier
381
     *
382
     * @return self
383
     */
384 3
    public function withHours(?string $hours): self
385
    {
386 3
        $new = clone $this;
387 3
        $new->hours = $hours;
388 3
        return $new;
389
    }
390
391
    /**
392
     * @return string Two-letter ISO 3166-1 country code.
393
     *
394
     * @see https://www.iso.org/iso-3166-country-codes.html
395
     */
396 5
    public function region(): ?string
397
    {
398 5
        return $this->region;
399
    }
400
401
    /**
402
     * @param string|null $region Two-letter ISO 3166-1 country code.
403
     *
404
     * @see https://www.iso.org/iso-3166-country-codes.html
405
     *
406
     * @return self
407
     */
408 2
    public function withRegion(?string $region): self
409
    {
410 2
        $new = clone $this;
411 2
        $new->region = $region;
412 2
        return $new;
413
    }
414
415
    /**
416
     * @return string ICU currency.
417
     */
418 2
    public function currency(): ?string
419
    {
420 2
        return $this->currency;
421
    }
422
423
    /**
424
     * @param string|null $currency ICU currency.
425
     *
426
     * @return self
427
     */
428 3
    public function withCurrency(?string $currency): self
429
    {
430 3
        $new = clone $this;
431 3
        $new->currency = $currency;
432
433 3
        return $new;
434
    }
435
436
    /**
437
     * @return string|null Extended language subtags.
438
     */
439 2
    public function extendedLanguage(): ?string
440
    {
441 2
        return $this->extendedLanguage;
442
    }
443
444
    /**
445
     * @param string|null $extendedLanguage Extended language subtags.
446
     *
447
     * @return self
448
     */
449 3
    public function withExtendedLanguage(?string $extendedLanguage): self
450
    {
451 3
        $new = clone $this;
452 3
        $new->extendedLanguage = $extendedLanguage;
453
454 3
        return $new;
455
    }
456
457
    /**
458
     * @return string|null
459
     */
460 3
    public function private(): ?string
461
    {
462 3
        return $this->private;
463
    }
464
465
    /**
466
     * @param string|null $private
467
     *
468
     * @return self
469
     */
470 3
    public function withPrivate(?string $private): self
471
    {
472 3
        $new = clone $this;
473 3
        $new->private = $private;
474
475 3
        return $new;
476
    }
477
478
    /**
479
     * @return string Regular expression for parsing BCP 47.
480
     *
481
     * @see https://tools.ietf.org/html/bcp47
482
     */
483 34
    private static function getBCP47Regex(): string
484
    {
485 34
        $regular = '(?:art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)';
486 34
        $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)';
487 34
        $grandfathered = '(?<grandfathered>' . $irregular . '|' . $regular . ')';
488 34
        $private = '(?<private>x(?:-[A-Za-z0-9]{1,8})+)';
489 34
        $singleton = '[0-9A-WY-Za-wy-z]';
490 34
        $extension = '(?<extension>' . $singleton . '(?:-[A-Za-z0-9]{2,8})+)';
491 34
        $variant = '(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3})';
492 34
        $region = '(?<region>[A-Za-z]{2}|[0-9]{3})';
493 34
        $script = '(?<script>[A-Za-z]{4})';
494 34
        $extendedLanguage = '(?<extendedLanguage>[A-Za-z]{3}(?:-[A-Za-z]{3}){0,2})';
495 34
        $language = '(?<language>[A-Za-z]{4,8})|(?<language>[A-Za-z]{2,3})(?:-' . $extendedLanguage . ')?';
496 34
        $icuKeywords = '(?:@(?<keywords>.*?))?';
497 34
        $languageTag = '(?:' . $language . '(?:-' . $script . ')?' . '(?:-' . $region . ')?' . '(?:-' . $variant . ')*' . '(?:-' . $extension . ')*' . '(?:-' . $private . ')?' . ')';
498 34
        return '/^(?J:' . $grandfathered . '|' . $languageTag . '|' . $private . ')' . $icuKeywords . '$/';
499
    }
500
501 1
    public function __toString(): string
502
    {
503 1
        return $this->asString();
504
    }
505
506
    /**
507
     * @return string Locale string.
508
     */
509 3
    public function asString(): string
510
    {
511 3
        if ($this->grandfathered !== null) {
512 1
            return $this->grandfathered;
513
        }
514
515 3
        $result = [];
516 3
        if ($this->language !== null) {
517 3
            $result[] = $this->language;
518
519 3
            if ($this->extendedLanguage !== null) {
520 1
                $result[] = $this->extendedLanguage;
521
            }
522
523 3
            if ($this->script !== null) {
524 1
                $result[] = $this->script;
525
            }
526
527 3
            if ($this->region !== null) {
528 2
                $result[] = $this->region;
529
            }
530
531 3
            if ($this->variant !== null) {
532 1
                $result[] = $this->variant;
533
            }
534
535 3
            if ($this->extension !== null) {
536 1
                $result[] = $this->extension;
537
            }
538
        }
539
540 3
        if ($this->private !== null) {
541 1
            $result[] = 'x-' . $this->private;
542
        }
543
544 3
        $keywords = [];
545 3
        if ($this->currency !== null) {
546 1
            $keywords[] = 'currency=' . $this->currency;
547
        }
548 3
        if ($this->colcasefirst !== null) {
549 1
            $keywords[] = 'colcasefirst=' . $this->colcasefirst;
550
        }
551 3
        if ($this->collation !== null) {
552 1
            $keywords[] = 'collation=' . $this->collation;
553
        }
554 3
        if ($this->colnumeric !== null) {
555 1
            $keywords[] = 'colnumeric=' . $this->colnumeric;
556
        }
557 3
        if ($this->calendar !== null) {
558 1
            $keywords[] = 'calendar=' . $this->calendar;
559
        }
560 3
        if ($this->numbers !== null) {
561 1
            $keywords[] = 'numbers=' . $this->numbers;
562
        }
563 3
        if ($this->hours !== null) {
564 1
            $keywords[] = 'hours=' . $this->hours;
565
        }
566
567 3
        $string = implode('-', $result);
568
569 3
        if ($keywords !== []) {
570 1
            $string .= '@' . implode(';', $keywords);
571
        }
572
573 3
        return $string;
574
    }
575
576
    /**
577
     * Returns fallback locale.
578
     *
579
     * @return self Fallback locale.
580
     */
581 2
    public function fallbackLocale(): self
582
    {
583
        $fallback = $this
584 2
            ->withCalendar(null)
585 2
            ->withColcasefirst(null)
586 2
            ->withCollation(null)
587 2
            ->withColnumeric(null)
588 2
            ->withCurrency(null)
589 2
            ->withExtendedLanguage(null)
590 2
            ->withNumbers(null)
591 2
            ->withHours(null)
592 2
            ->withPrivate(null);
593
594 2
        if ($fallback->variant() !== null) {
595 1
            return $fallback->withVariant(null);
596
        }
597
598 2
        if ($fallback->region() !== null) {
599 1
            return $fallback->withRegion(null);
600
        }
601
602 2
        if ($fallback->script() !== null) {
603 1
            return $fallback->withScript(null);
604
        }
605
606 1
        return $fallback;
607
    }
608
}
609