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