Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Plurals
100.00% covered (success)
100.00%
45 / 45
100.00% covered (success)
100.00%
3 / 3
36
100.00% covered (success)
100.00%
1 / 1
 rule
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 forms
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 entry
100.00% covered (success)
100.00%
43 / 43
100.00% covered (success)
100.00%
1 / 1
34
1<?php
2
3declare(strict_types=1);
4
5namespace Celemas\Verba;
6
7use Closure;
8
9/**
10 * Plural rules keyed by language, using the classic gettext formulas.
11 *
12 * Only the exceptions are listed; every other language falls through to the
13 * two-form `n !== 1` default. Region subtags are honored where they change the
14 * rule (e.g. `pt_BR`). A catalog may override its rule via the `plural` key.
15 *
16 * @api
17 */
18final class Plurals
19{
20    /**
21     * Returns the rule mapping a count to its zero-based plural form index.
22     *
23     * @return Closure(int): int
24     */
25    public static function rule(string $locale): Closure
26    {
27        return self::entry($locale)[1];
28    }
29
30    /**
31     * Number of plural forms (nplurals) for the given locale.
32     */
33    public static function forms(string $locale): int
34    {
35        return self::entry($locale)[0];
36    }
37
38    /**
39     * @return array{int, Closure(int): int}
40     *
41     * @psalm-suppress UnusedClosureParam One-form languages ignore the count.
42     */
43    private static function entry(string $locale): array
44    {
45        $norm = str_replace('-', '_', strtolower($locale));
46        $lang = explode('_', $norm)[0];
47
48        return match (true) {
49            $norm === 'pt_br', $lang === 'fr' => [2, static fn(int $n): int => $n > 1 ? 1 : 0],
50            in_array($lang, ['ja', 'ko', 'zh', 'vi', 'th', 'id', 'fa'], true) => [
51                1,
52                static fn(int $n): int => 0,
53            ],
54            in_array($lang, ['ru', 'uk', 'be'], true) => [
55                3,
56                static fn(int $n): int => match (true) {
57                    ($n % 10) === 1 && ($n % 100) !== 11 => 0,
58                    ($n % 10) >= 2 && ($n % 10) <= 4 && !(($n % 100) >= 12 && ($n % 100) <= 14) => 1,
59                    default => 2,
60                },
61            ],
62            $lang === 'pl' => [
63                3,
64                static fn(int $n): int => match (true) {
65                    $n === 1 => 0,
66                    ($n % 10) >= 2 && ($n % 10) <= 4 && !(($n % 100) >= 12 && ($n % 100) <= 14) => 1,
67                    default => 2,
68                },
69            ],
70            in_array($lang, ['cs', 'sk'], true) => [
71                3,
72                static fn(int $n): int => match (true) {
73                    $n === 1 => 0,
74                    $n >= 2 && $n <= 4 => 1,
75                    default => 2,
76                },
77            ],
78            $lang === 'ar' => [
79                6,
80                static fn(int $n): int => match (true) {
81                    $n === 0 => 0,
82                    $n === 1 => 1,
83                    $n === 2 => 2,
84                    ($n % 100) >= 3 && ($n % 100) <= 10 => 3,
85                    ($n % 100) >= 11 => 4,
86                    default => 5,
87                },
88            ],
89            default => [2, static fn(int $n): int => $n === 1 ? 0 : 1],
90        };
91    }
92}