Completed
Push — master ( 7b0904...670e33 )
by f
14:18
created

NounPluralization::getRunAwayVowelsList()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 8
Ratio 100 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 8
loc 8
ccs 5
cts 5
cp 1
crap 2
rs 10
c 0
b 0
f 0
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 84
     */
58
    public static function pluralize($word, $count = 2, $animateness = false, $case = null)
59 84
    {
60 84
        // меняем местами аргументы, если они переданы в старом формате
61 84 View Code Duplication
        if (is_string($count) && is_numeric($word)) {
0 ignored issues
show
Duplication introduced by
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
            list($count, $word) = [$word, $count];
63 84
        }
64
65
        if ($case !== null)
66
            $case = static::canonizeCase($case);
67
68
        // для адъективных существительных правила склонения проще:
69
        // только две формы
70 View Code Duplication
        if (static::isAdjectiveNoun($word)) {
0 ignored issues
show
Duplication introduced by
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...
71
            if (static::getNumeralForm($count) == static::ONE)
72
                return $word;
73
            else
74
                return NounPluralization::getCase($word,
75
                    $case !== null
76
                        ? $case
77 62
                        : static::RODIT, $animateness);
78
        }
79
80 62
        if ($case === null) {
81 1
            switch (static::getNumeralForm($count)) {
82
                case static::ONE:
83
                    return $word;
84 62
                case static::TWO_FOUR:
85 3
                    return NounDeclension::getCase($word, static::RODIT, $animateness);
86
                case static::FIVE_OTHER:
87
                    return NounPluralization::getCase($word, static::RODIT, $animateness);
88
            }
89 62
        }
90 3
91 2 View Code Duplication
        if (static::getNumeralForm($count) == static::ONE)
0 ignored issues
show
Duplication introduced by
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...
92
            return NounDeclension::getCase($word, $case, $animateness);
93 3
        else
94 3
            return NounPluralization::getCase($word, $case, $animateness);
95
    }
96 3
97
    /**
98
     * @param int|float $count
99 60
     * @return int
100 57
     */
101 57
    public static function getNumeralForm($count)
102 29
    {
103 57
        if ($count > 100) {
104 36
            $count %= 100;
105 44
        }
106 44
        $ending = $count % 10;
107
108
        if (($count > 20 && $ending == 1) || $count == 1) {
109
            return static::ONE;
110 3
        } elseif (($count > 20 && in_array($ending, range(2, 4))) || in_array($count, range(2, 4))) {
111 3
            return static::TWO_FOUR;
112
        } else {
113 3
            return static::FIVE_OTHER;
114
        }
115
    }
116
117
    /**
118
     * @param string $word
119
     * @param string $case
120 74
     * @param bool $animateness
121
     * @return string
122 74
     * @throws \Exception
123 48
     */
124
    public static function getCase($word, $case, $animateness = false)
125 74
    {
126
        $case = static::canonizeCase($case);
127 74
        $forms = static::getCases($word, $animateness);
128 38
        return $forms[$case];
129 73
    }
130 48
131
    /**
132 56
     * @param string $word
133
     * @param bool $animateness
134
     * @return string[]
135
     * @phpstan-return array<string, string>
136
     */
137
    public static function getCases($word, $animateness = false)
138
    {
139
        $word = S::lower($word);
140
141 View Code Duplication
        if (in_array($word, NounDeclension::$immutableWords, true)) {
0 ignored issues
show
Duplication introduced by
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...
142
            return [
143 49
                static::IMENIT => $word,
144
                static::RODIT => $word,
145 49
                static::DAT => $word,
146 49
                static::VINIT => $word,
147 49
                static::TVORIT => $word,
148
                static::PREDLOJ => $word,
149
            ];
150
        }
151
152 View Code Duplication
        if (isset(static::$abnormalExceptions[$word])) {
0 ignored issues
show
Duplication introduced by
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...
153
            return array_combine(
154
                [static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ],
155
                static::$abnormalExceptions[$word]);
156 95
        }
157
158 95
        // Адъективное склонение (Сущ, образованные от прилагательных и причастий)
159
        // Пример: прохожий, существительное
160 95
        if (static::isAdjectiveNoun($word)) {
161
            return static::declinateAdjective($word, $animateness);
162 1
        }
163 1
164 1
        // Субстантивное склонение (существительные)
165 1
        return static::declinateSubstative($word, $animateness);
166 1
    }
167 1
168
    /**
169
     * Склонение обычных существительных.
170
     * @param string $word
171 95
     * @param bool $animateness
172 1
     * @return string[]
173 1
     * @phpstan-return array<string, string>
174 1
     */
175
    protected static function declinateSubstative($word, $animateness)
176
    {
177
        $prefix = S::slice($word, 0, -1);
178
        $last = S::slice($word, -1);
179 94
180 11
        $runaway_vowels_list = NounDeclension::getRunAwayVowelsList();
181
        if (isset($runaway_vowels_list[$word])) {
182
            $vowel_offset = $runaway_vowels_list[$word];
183
            $word = S::slice($word, 0, $vowel_offset) . S::slice($word, $vowel_offset + 1);
0 ignored issues
show
Security Bug introduced by
It seems like $vowel_offset defined by $runaway_vowels_list[$word] on line 182 can also be of type false; however, morphos\S::slice() does only seem to accept integer|null, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
184 84
        }
185
186
        if (($declension = NounDeclension::getDeclension($word)) == NounDeclension::SECOND_DECLENSION) {
187
            $soft_last = $last == 'й' || (in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true)
188
                    && ((
189
                        static::isConsonant(S::slice($word, -2, -1)) && !static::isHissingConsonant(S::slice($word, -2, -1)))
190
                        || S::slice($word, -2, -1) == 'и'));
191
            $prefix = NounDeclension::getPrefixOfSecondDeclension($word, $last);
192
        } elseif ($declension == NounDeclension::FIRST_DECLENSION) {
193
            $soft_last = static::checkLastConsonantSoftness($word);
194 84
        } else {
195
            $soft_last = in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь'], true);
196 84
        }
197 84
198
        $forms = [];
199 84
200 84
        if (in_array($last, ['ч', 'г'], true)
201 1
            || in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь', 'рь'], true)
202 1
            || (static::isVowel($last) && in_array(S::slice($word, -2, -1), ['ч', 'к'], true))) { // before ч, чь, сь, ч+vowel, к+vowel
203
            $forms[Cases::IMENIT] = $prefix.'и';
204
        } elseif (in_array($last, ['н', 'ц', 'р', 'т'], true)) {
205 84
            $forms[Cases::IMENIT] = $prefix.'ы';
206 55 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
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...
207
            $forms[Cases::IMENIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'я', $prefix.'а');
208 20
        }
209 55
210 55
        // RODIT
211 41
        if (isset(static::$genitiveExceptions[$word])) {
212 37
            $forms[Cases::RODIT] = static::$genitiveExceptions[$word];
213
        } elseif (in_array($last, ['о', 'е'], true)) {
214 4
            // exceptions
215
            if (in_array($word, static::$neuterExceptions, true)) {
216
                $forms[Cases::RODIT] = $prefix.'ей';
217 84 View Code Duplication
            } elseif (S::slice($word, -2, -1) == 'и') {
0 ignored issues
show
Duplication introduced by
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...
218
                $forms[Cases::RODIT] = $prefix.'й';
219 84
            } else {
220 82
                $forms[Cases::RODIT] = $prefix;
221 84
            }
222 39
        } elseif (S::slice($word, -2) == 'ка' && S::slice($word, -3, -2) !== 'и') { // words ending with -ка: чашка, вилка, ложка, тарелка, копейка, батарейка
223 56
            if (S::slice($word, -3, -2) == 'л') {
224 19
                $forms[Cases::RODIT] = S::slice($word, 0, -2).'ок';
225 View Code Duplication
            } elseif (S::slice($word, -3, -2) == 'й') {
0 ignored issues
show
Duplication introduced by
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...
226 41
                $forms[Cases::RODIT] = S::slice($word, 0, -3).'ек';
227
            } else {
228
                $forms[Cases::RODIT] = S::slice($word, 0, -2).'ек';
229
            }
230 84
        } elseif (in_array($last, ['а'], true)) { // обида, ябеда
231 10
            $forms[Cases::RODIT] = $prefix;
232 76 View Code Duplication
        } elseif (in_array($last, ['я'], true)) { // молния
0 ignored issues
show
Duplication introduced by
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...
233
            $forms[Cases::RODIT] = $prefix.'й';
234 8
        } elseif (RussianLanguage::isHissingConsonant($last) || ($soft_last && $last != 'й') || in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь'], true)) {
235 3
            $forms[Cases::RODIT] = $prefix.'ей';
236 5 View Code Duplication
        } elseif ($last == 'й' || S::slice($word, -2) == 'яц') { // месяц
0 ignored issues
show
Duplication introduced by
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...
237 2
            $forms[Cases::RODIT] = $prefix.'ев';
238
        } else { // (static::isConsonant($last) && !RussianLanguage::isHissingConsonant($last))
239 8
            $forms[Cases::RODIT] = $prefix.'ов';
240
        }
241 68
242 13
        // DAT
243 2
        $forms[Cases::DAT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ям', $prefix.'ам');
244 11
245 9
        // VINIT
246
        $forms[Cases::VINIT] = NounDeclension::getVinitCaseByAnimateness($forms, $animateness);
247 13
248
        // TVORIT
249 61
        // my personal rule
250 23
        if ($last == 'ь' && $declension == NounDeclension::THIRD_DECLENSION && !in_array(S::slice($word, -2), ['чь', 'сь', 'ть', 'нь'], true)) {
251 45
            $forms[Cases::TVORIT] = $prefix.'ми';
252 1
        } else {
253 44
            $forms[Cases::TVORIT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ями', $prefix.'ами');
254 20
        }
255 27
256 7
        // PREDLOJ
257
        $forms[Cases::PREDLOJ] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ях', $prefix.'ах');
258 20
        return $forms;
259
    }
260
261
    /**
262 84
     * Склонение существительных, образованных от прилагательных и причастий.
263
     * Rules are from http://rusgram.narod.ru/1216-1231.html
264
     * @param string $word
265 84
     * @param bool $animateness
266
     * @return string[]
267
     * @phpstan-return array<string, string>
268
     */
269 84
    protected static function declinateAdjective($word, $animateness)
270
    {
271
        $prefix = S::slice($word, 0, -2);
272 84
        $vowel = static::isHissingConsonant(S::slice($prefix, -1)) ? 'и' : 'ы';
273
        return [
274
            Cases::IMENIT => $prefix.$vowel.'е',
275
            Cases::RODIT => $prefix.$vowel.'х',
276 84
            Cases::DAT => $prefix.$vowel.'м',
277 84
            Cases::VINIT => $prefix.$vowel.($animateness ? 'х' : 'е'),
278
            Cases::TVORIT => $prefix.$vowel.'ми',
279
            Cases::PREDLOJ => $prefix.$vowel.'х',
280
        ];
281
    }
282
}
283