Completed
Push — master ( 622740...e78b73 )
by f
01:58
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\NounPluralization implements Cases
10
{
11
    use RussianLanguage, CasesHelper;
12
13
    const ONE = 1;
14
    const TWO_FOUR = 2;
15
    const FIVE_OTHER = 3;
16
17
    protected static $neuterExceptions = [
18
        'поле',
19
        'море',
20
    ];
21
22
    protected static $genitiveExceptions = [
23
        'письмо' => 'писем',
24
        'пятно' => 'пятен',
25
        'кресло' => 'кресел',
26
        'коромысло' => 'коромысел',
27
        'ядро' => 'ядер',
28
        'блюдце' => 'блюдец',
29
        'полотенце' => 'полотенец',
30
        'гривна' => 'гривен',
31
        'год' => 'лет',
32
    ];
33
34
    protected static $immutableWords = [
35
        'евро',
36
        'пенни',
37
    ];
38
39
    protected static $runawayVowelsExceptions = [
40
        'писе*ц',
41
        'песе*ц',
42
        'глото*к',
43
    ];
44
45
    protected static $runawayVowelsNormalized = false;
46
47
    /**
48
     * @return array|bool
49
     */
50 67
    protected static function getRunAwayVowelsList()
51
    {
52 67
        if (self::$runawayVowelsNormalized === false) {
53 1
            self::$runawayVowelsNormalized = [];
0 ignored issues
show
Documentation Bug introduced by
It seems like array() of type array is incompatible with the declared type boolean of property $runawayVowelsNormalized.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

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