Completed
Push — master ( 9ac1c1...101804 )
by f
16s queued 10s
created

src/Russian/NounPluralization.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
namespace morphos\Russian;
3
4
use morphos\S;
5
6
/**
7
 * Rules are from http://morpher.ru/Russian/Noun.aspx
8
 */
9
class NounPluralization extends \morphos\BasePluralization implements Cases
10
{
11
    use RussianLanguage, CasesHelper;
12
13
    const ONE = 1;
14
    const TWO_FOUR = 2;
15
    const FIVE_OTHER = 3;
16
17
    /**
18
     * @var string[][]
19
     * @phpstan-var array<string, string[]>
20
     */
21
    protected static $abnormalExceptions = [
22
        'человек' => ['люди', 'человек', 'людям', 'людей', 'людьми', 'людях'],
23
    ];
24
25
    /** @var string[] */
26
    protected static $neuterExceptions = [
27
        'поле',
28
        'море',
29
    ];
30
31
    /**
32
     * @var string[]
33
     * @phpstan-var array<string, string>
34
     */
35
    protected static $genitiveExceptions = [
36
        'письмо' => 'писем',
37
        'пятно' => 'пятен',
38
        'кресло' => 'кресел',
39
        'коромысло' => 'коромысел',
40
        'ядро' => 'ядер',
41
        'блюдце' => 'блюдец',
42
        'полотенце' => 'полотенец',
43
        'гривна' => 'гривен',
44
        'год' => 'лет',
45
    ];
46
47
    /**
48
     * Склонение существительного для сочетания с числом (кол-вом предметов).
49
     *
50
     * @param string|int $word        Название предмета
51
     * @param int|float|string $count Количество предметов
52
     * @param bool       $animateness Признак одушевленности
53
     * @param string     $case        Род существительного
54
     *
55
     * @return string
56
     * @throws \Exception
57
     */
58 62
    public static function pluralize($word, $count = 2, $animateness = false, $case = null)
59
    {
60
        // меняем местами аргументы, если они переданы в старом формате
61 62 View Code Duplication
        if (is_string($count) && is_numeric($word)) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
62 1
            list($count, $word) = [$word, $count];
63
        }
64
65 62
        if ($case !== null)
66 3
            $case = static::canonizeCase($case);
67
68
        // для адъективных существительных правила склонения проще:
69
        // только две формы
70 62 View Code Duplication
        if (static::isAdjectiveNoun($word)) {
71 3
            if (static::getNumeralForm($count) == static::ONE)
72 2
                return $word;
73
            else
74 3
                return NounPluralization::getCase($word,
75 3
                    $case !== null
76
                        ? $case
77 3
                        : static::RODIT, $animateness);
78
        }
79
80 60
        if ($case === null) {
81 57
            switch (static::getNumeralForm($count)) {
82 57
                case static::ONE:
83 29
                    return $word;
84 57
                case static::TWO_FOUR:
85 36
                    return NounDeclension::getCase($word, static::RODIT, $animateness);
86 44
                case static::FIVE_OTHER:
87 44
                    return NounPluralization::getCase($word, static::RODIT, $animateness);
88
            }
89
        }
90
91 3 View Code Duplication
        if (static::getNumeralForm($count) == static::ONE)
92 3
            return NounDeclension::getCase($word, $case, $animateness);
93
        else
94 3
            return NounPluralization::getCase($word, $case, $animateness);
95
    }
96
97
    /**
98
     * @param int|float $count
99
     * @return int
100
     */
101 74
    public static function getNumeralForm($count)
102
    {
103 74
        if ($count > 100) {
104 48
            $count %= 100;
105
        }
106 74
        $ending = $count % 10;
107
108 74
        if (($count > 20 && $ending == 1) || $count == 1) {
109 38
            return static::ONE;
110 73
        } elseif (($count > 20 && in_array($ending, range(2, 4))) || in_array($count, range(2, 4))) {
111 48
            return static::TWO_FOUR;
112
        } else {
113 56
            return static::FIVE_OTHER;
114
        }
115
    }
116
117
    /**
118
     * @param string $word
119
     * @param string $case
120
     * @param bool $animateness
121
     * @return string
122
     * @throws \Exception
123
     */
124 49
    public static function getCase($word, $case, $animateness = false)
125
    {
126 49
        $case = static::canonizeCase($case);
127 49
        $forms = static::getCases($word, $animateness);
128 49
        return $forms[$case];
129
    }
130
131
    /**
132
     * @param string $word
133
     * @param bool $animateness
134
     * @return string[]
135
     * @phpstan-return array<string, string>
136
     */
137 95
    public static function getCases($word, $animateness = false)
138
    {
139 95
        $word = S::lower($word);
140
141 95 View Code Duplication
        if (in_array($word, NounDeclension::$immutableWords, true)) {
142
            return [
143 1
                static::IMENIT => $word,
144 1
                static::RODIT => $word,
145 1
                static::DAT => $word,
146 1
                static::VINIT => $word,
147 1
                static::TVORIT => $word,
148 1
                static::PREDLOJ => $word,
149
            ];
150
        }
151
152 95 View Code Duplication
        if (isset(static::$abnormalExceptions[$word])) {
153 1
            return array_combine(
154 1
                [static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ],
155 1
                static::$abnormalExceptions[$word]);
156
        }
157
158
        // Адъективное склонение (Сущ, образованные от прилагательных и причастий)
159
        // Пример: прохожий, существительное
160 94
        if (static::isAdjectiveNoun($word)) {
161 11
            return static::declinateAdjective($word, $animateness);
162
        }
163
164
        // Субстантивное склонение (существительные)
165 84
        return static::declinateSubstative($word, $animateness);
166
    }
167
168
    /**
169
     * Склонение обычных существительных.
170
     * @param string $word
171
     * @param bool $animateness
172
     * @return string[]
173
     * @phpstan-return array<string, string>
174
     */
175 84
    protected static function declinateSubstative($word, $animateness)
176
    {
177 84
        $prefix = S::slice($word, 0, -1);
178 84
        $last = S::slice($word, -1);
179
180 84
        if (($declension = NounDeclension::getDeclension($word)) == NounDeclension::SECOND_DECLENSION) {
181 55
            $soft_last = $last == 'й' || (in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true)
182
                    && ((
183 20
                        static::isConsonant(S::slice($word, -2, -1)) && !static::isHissingConsonant(S::slice($word, -2, -1)))
184 55
                        || S::slice($word, -2, -1) == 'и'));
185 55
            $prefix = NounDeclension::getPrefixOfSecondDeclension($word, $last);
186 41
        } elseif ($declension == NounDeclension::FIRST_DECLENSION) {
187 37
            $soft_last = static::checkLastConsonantSoftness($word);
188
        } else {
189 4
            $soft_last = in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь', 'дь'], true);
190
        }
191
192 84
        $forms = [];
193
194 84
        if (in_array($last, ['ч', 'г'], true)
195 82
            || in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь', 'рь', 'дь'], true)
196 84
            || (static::isVowel($last) && in_array(S::slice($word, -2, -1), ['ч', 'к'], true))) { // before ч, чь, сь, ч+vowel, к+vowel
197 39
            $forms[Cases::IMENIT] = $prefix.'и';
198 56
        } elseif (in_array($last, ['н', 'ц', 'р', 'т'], true)) {
199 19
            $forms[Cases::IMENIT] = $prefix.'ы';
200 View Code Duplication
        } else {
201 41
            $forms[Cases::IMENIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'я', $prefix.'а');
202
        }
203
204
        // RODIT
205 84
        if (isset(static::$genitiveExceptions[$word])) {
206 10
            $forms[Cases::RODIT] = static::$genitiveExceptions[$word];
207 76
        } elseif (in_array($last, ['о', 'е'], true)) {
208
            // exceptions
209 8
            if (in_array($word, static::$neuterExceptions, true)) {
210 3
                $forms[Cases::RODIT] = $prefix.'ей';
211 5 View Code Duplication
            } elseif (S::slice($word, -2, -1) == 'и') {
212 2
                $forms[Cases::RODIT] = $prefix.'й';
213
            } else {
214 8
                $forms[Cases::RODIT] = $prefix;
215
            }
216 68
        } elseif (S::slice($word, -2) == 'ка' && S::slice($word, -3, -2) !== 'и') { // words ending with -ка: чашка, вилка, ложка, тарелка, копейка, батарейка, аптека
217 13
            if (S::slice($word, -3, -2) == 'л') {
218 2
                $forms[Cases::RODIT] = S::slice($word, 0, -2).'ок';
219 11
            } elseif (in_array(S::slice($word, -3, -2), ['й', 'е'], true)) {
220 9
                $forms[Cases::RODIT] = S::slice($word, 0, -3).'ек';
221
            } else {
222 13
                $forms[Cases::RODIT] = S::slice($word, 0, -2).'ек';
223
            }
224 61
        } elseif (in_array($last, ['а'], true)) { // обида, ябеда
225 23
            $forms[Cases::RODIT] = $prefix;
226 45 View Code Duplication
        } elseif (in_array($last, ['я'], true)) { // молния
227 1
            $forms[Cases::RODIT] = $prefix.'й';
228 44
        } elseif (RussianLanguage::isHissingConsonant($last) || ($soft_last && $last != 'й') || in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь', 'дь'], true)) {
229 20
            $forms[Cases::RODIT] = $prefix.'ей';
230 27 View Code Duplication
        } elseif ($last == 'й' || S::slice($word, -2) == 'яц') { // месяц
231 7
            $forms[Cases::RODIT] = $prefix.'ев';
232
        } else { // (static::isConsonant($last) && !RussianLanguage::isHissingConsonant($last))
233 20
            $forms[Cases::RODIT] = $prefix.'ов';
234
        }
235
236
        // DAT
237 84
        $forms[Cases::DAT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ям', $prefix.'ам');
238
239
        // VINIT
240 84
        $forms[Cases::VINIT] = NounDeclension::getVinitCaseByAnimateness($forms, $animateness);
241
242
        // TVORIT
243
        // my personal rule
244 84
        if ($last == 'ь' && $declension == NounDeclension::THIRD_DECLENSION && !in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь', 'дь'], true)) {
245
            $forms[Cases::TVORIT] = $prefix.'ми';
246
        } else {
247 84
            $forms[Cases::TVORIT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ями', $prefix.'ами');
248
        }
249
250
        // PREDLOJ
251 84
        $forms[Cases::PREDLOJ] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ях', $prefix.'ах');
252 84
        return $forms;
253
    }
254
255
    /**
256
     * Склонение существительных, образованных от прилагательных и причастий.
257
     * Rules are from http://rusgram.narod.ru/1216-1231.html
258
     * @param string $word
259
     * @param bool $animateness
260
     * @return string[]
261
     * @phpstan-return array<string, string>
262
     */
263 11
    protected static function declinateAdjective($word, $animateness)
264
    {
265 11
        $prefix = S::slice($word, 0, -2);
266 11
        $vowel = static::isHissingConsonant(S::slice($prefix, -1)) ? 'и' : 'ы';
267
        return [
268 11
            Cases::IMENIT => $prefix.$vowel.'е',
269 11
            Cases::RODIT => $prefix.$vowel.'х',
270 11
            Cases::DAT => $prefix.$vowel.'м',
271 11
            Cases::VINIT => $prefix.$vowel.($animateness ? 'х' : 'е'),
272 11
            Cases::TVORIT => $prefix.$vowel.'ми',
273 11
            Cases::PREDLOJ => $prefix.$vowel.'х',
274
        ];
275
    }
276
}
277