NounDeclension::detectGender()   A
last analyzed

Complexity

Conditions 6
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
nc 3
nop 1
dl 0
loc 14
ccs 9
cts 9
cp 1
crap 6
rs 9.2222
c 0
b 0
f 0
1
<?php
2
namespace morphos\Russian;
3
4
use morphos\BaseInflection;
5
use morphos\Gender;
6
use morphos\S;
7
use RuntimeException;
8
9
/**
10
 * Rules are from http://morpher.ru/Russian/Noun.aspx
11
 */
12
class NounDeclension extends BaseInflection implements Cases, Gender
13
{
14
    use RussianLanguage, CasesHelper;
15
16
    const FIRST_DECLENSION = 1;
17
    const SECOND_DECLENSION = 2;
18
    const THIRD_DECLENSION = 3;
19
20
    /** @var string[] */
21
    public static $immutableWords = [
22
        // валюты
23
        'евро', 'пенни', 'песо', 'сентаво',
24
25
        // на а
26
        'боа', 'бра', 'фейхоа', 'амплуа', 'буржуа',
27
        // на о
28
        'манго', 'какао', 'кино', 'трюмо', 'пальто', 'бюро', 'танго', 'вето', 'бунгало', 'сабо', 'авокадо', 'депо', 'панно',
29
        // на у
30
        'зебу', 'кенгуру', 'рагу', 'какаду', 'шоу',
31
        // на е
32
        'шимпанзе', 'конферансье', 'атташе', 'колье', 'резюме', 'пенсне', 'кашне', 'протеже', 'коммюнике', 'драже', 'суфле', 'пюре', 'купе', 'фойе', 'шоссе', 'крупье',
33
        // на и
34
        'такси', 'жалюзи', 'шасси', 'алиби', 'киви', 'иваси', 'регби', 'конфетти', 'колибри', 'жюри', 'пенальти', 'рефери', 'кольраби',
35
        // на э
36
        'каноэ', 'алоэ',
37
        // на ю
38
        'меню', 'парвеню', 'авеню', 'дежавю', 'инженю', 'барбекю', 'интервью',
39
    ];
40
41
    /**
42
     * These words has 2 declension type.
43
     * @var string[]|string[][]
44
     */
45
    protected static $abnormalExceptions = [
46
        'бремя',
47
        'вымя',
48
        'темя',
49
        'пламя',
50
        'стремя',
51
        'пламя',
52
        'время',
53
        'знамя',
54
        'имя',
55
        'племя',
56
        'семя',
57
        'путь' => ['путь', 'пути', 'пути', 'путь', 'путем', 'пути'],
58
        'дитя' => ['дитя', 'дитяти', 'дитяти', 'дитя', 'дитятей', 'дитяти'],
59
    ];
60
61
    /** @var string[]  */
62
    protected static $masculineWithSoft = [
63
        'автослесарь',
64
        'библиотекарь',
65
        'водитель',
66
        'воспитатель',
67
        'врач',
68
        'выхухоль',
69
        'гвоздь',
70
        'делопроизводитель',
71
        'день',
72
        'дождь',
73
        'заместитель',
74
        'зверь',
75
        'любитель',
76
        'камень',
77
        'конь',
78
        'конь',
79
        'корень',
80
        'лось',
81
        'медведь',
82
        'модуль',
83
        'олень',
84
        'парень',
85
        'пекарь',
86
        'пельмень',
87
        'пень',
88
        'председатель',
89
        'представитель',
90
        'преподаватель',
91
        'продавец',
92
        'производитель',
93
        'путь',
94
        'рояль',
95
        'рубль',
96
        'руководитель',
97
        'секретарь',
98
        'слесарь',
99
        'строитель',
100
        'табель',
101
        'токарь',
102
        'трутень',
103
        'тюлень',
104
        'учитель',
105
        'циркуль',
106
        'шампунь',
107
        'шкворень',
108
        'юань',
109
        'ячмень',
110
    ];
111
112
    /** @var string[] */
113
    public static $runawayVowelsExceptions = [
114
        'глото*к',
115
        'де*нь',
116
        'каме*нь',
117
        'коре*нь',
118
        'паре*нь',
119
        'пе*нь',
120
        'песе*ц',
121
        'писе*ц',
122
        'санузе*л',
123
        'труте*нь',
124
    ];
125
126
    /**
127
     * Проверка, изменяемое ли слово.
128
     * @param string $word Слово для проверки
129
     * @param bool $animateness Признак одушевленности
130
     * @return bool
131
     */
132 43
    public static function isMutable($word, $animateness = false)
0 ignored issues
show
Unused Code introduced by
The parameter $animateness is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
133
    {
134 43
        $word = S::lower($word);
135 43
        if (in_array(S::slice($word, -1), ['у', 'и', 'е', 'о', 'ю'], true) || in_array($word, static::$immutableWords, true)) {
136 43
            return false;
137
        }
138
        return true;
139
    }
140
141
    /**
142
     * Определение рода существительного.
143
     * @param string $word
144
     * @return string
145
     */
146 8
    public static function detectGender($word)
147
    {
148 8
    	$word = S::lower($word);
149 8
    	$last = S::slice($word, -1);
150
		// пытаемся угадать род объекта, хотя бы примерно, чтобы правильно склонять
151 8
		if (S::slice($word, -2) == 'мя' || in_array($last, ['о', 'е', 'и', 'у'], true))
152 2
			return static::NEUTER;
153
154 6
		if (in_array($last, ['а', 'я'], true) ||
155 6
			($last == 'ь' && !in_array($word, static::$masculineWithSoft, true)))
156 3
			return static::FEMALE;
157
158 3
		return static::MALE;
159
    }
160
161
    /**
162
     * Определение склонения (по школьной программе) существительного.
163
     * @param string $word
164
     * @param bool $animateness
165
     * @return int
166
     */
167 207
    public static function getDeclension($word, $animateness = false)
0 ignored issues
show
Unused Code introduced by
The parameter $animateness is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
168
    {
169 207
        $word = S::lower($word);
170 207
        $last = S::slice($word, -1);
171 207
        if (isset(static::$abnormalExceptions[$word]) || in_array($word, static::$abnormalExceptions, true)) {
172 3
            return 2;
173
        }
174
175 204
        if (in_array($last, ['а', 'я'], true) && S::slice($word, -2) != 'мя') {
176 64
            return 1;
177 155
        } elseif (static::isConsonant($last) || in_array($last, ['о', 'е', 'ё'], true)
178 45
            || ($last == 'ь' && static::isConsonant(S::slice($word, -2, -1)) && !static::isHissingConsonant(S::slice($word, -2, -1))
179 155
                && (in_array($word, static::$masculineWithSoft, true)) /*|| in_array($word, static::$masculineWithSoftAndRunAwayVowels, true)*/)) {
180 145
            return 2;
181
        } else {
182 10
            return 3;
183
        }
184
    }
185
186
    /**
187
     * Получение слова во всех 6 падежах.
188
     * @param string $word
189
     * @param bool $animateness Признак одушевлённости
190
     * @return string[]
191
     * @phpstan-return array<string, string>
192
     */
193 112
    public static function getCases($word, $animateness = false)
194
    {
195 112
        $word = S::lower($word);
196
197
        // Адъективное склонение (Сущ, образованные от прилагательных и причастий) - прохожий, существительное
198 112
        if (static::isAdjectiveNoun($word)) {
199 8
            return static::declinateAdjective($word, $animateness);
200
        }
201
202
        // Субстантивное склонение (существительные)
203 104 View Code Duplication
        if (in_array($word, static::$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...
204
            return [
205 3
                static::IMENIT => $word,
206 3
                static::RODIT => $word,
207 3
                static::DAT => $word,
208 3
                static::VINIT => $word,
209 3
                static::TVORIT => $word,
210 3
                static::PREDLOJ => $word,
211
            ];
212
        }
213
214 101 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...
215 2
            return array_combine([static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ], static::$abnormalExceptions[$word]);
216
        }
217
218 99
        if (in_array($word, static::$abnormalExceptions, true)) {
219 1
            $prefix = S::slice($word, 0, -1);
220
            return [
221 1
                static::IMENIT => $word,
222 1
                static::RODIT => $prefix.'ени',
223 1
                static::DAT => $prefix.'ени',
224 1
                static::VINIT => $word,
225 1
                static::TVORIT => $prefix.'енем',
226 1
                static::PREDLOJ => $prefix.'ени',
227
            ];
228
        }
229
230 98
        switch (static::getDeclension($word)) {
231 98
            case static::FIRST_DECLENSION:
232 25
                return static::declinateFirstDeclension($word);
233 75
            case static::SECOND_DECLENSION:
234 71
                return static::declinateSecondDeclension($word, $animateness);
235 4
            case static::THIRD_DECLENSION:
236 4
                return static::declinateThirdDeclension($word);
237
238
            default: throw new RuntimeException('Unreachable');
239
        }
240
    }
241
242
    /**
243
     * Получение всех форм слова первого склонения.
244
     * @param string $word
245
     * @return string[]
246
     * @phpstan-return array<string, string>
247
     */
248 25
    public static function declinateFirstDeclension($word)
249
    {
250 25
        $word = S::lower($word);
251 25
        $prefix = S::slice($word, 0, -1);
252 25
        $last = S::slice($word, -1);
253 25
        $soft_last = static::checkLastConsonantSoftness($word);
254
        $forms =  [
255 25
            Cases::IMENIT => $word,
256
        ];
257
258
        // RODIT
259 25
        $forms[Cases::RODIT] = static::chooseVowelAfterConsonant($last, $soft_last || (in_array(S::slice($word, -2, -1), ['г', 'к', 'х'], true)), $prefix.'и', $prefix.'ы');
260
261
        // DAT
262 25
        $forms[Cases::DAT] = static::getPredCaseOf12Declensions($word, $last, $prefix);
263
264
        // VINIT
265 25
        $forms[Cases::VINIT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) !== 'ч', $prefix.'ю', $prefix.'у');
266
267
        // TVORIT
268 25
        if ($last === 'ь') {
269
            $forms[Cases::TVORIT] = $prefix.'ой';
270 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...
271 25
            $forms[Cases::TVORIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'ей', $prefix.'ой');
272
        }
273
274
        // 	if ($last == 'й' || (static::isConsonant($last) && !static::isHissingConsonant($last)) || static::checkLastConsonantSoftness($word))
275
        // 	$forms[Cases::TVORIT] = $prefix.'ей';
276
        // else
277
        // 	$forms[Cases::TVORIT] = $prefix.'ой'; # http://morpher.ru/Russian/Spelling.aspx#sibilant
278
279
        // PREDLOJ the same as DAT
280 25
        $forms[Cases::PREDLOJ] = $forms[Cases::DAT];
281 25
        return $forms;
282
    }
283
284
    /**
285
     * Получение всех форм слова второго склонения.
286
     * @param string $word
287
     * @param bool $animateness
288
     * @return string[]
289
     * @phpstan-return array<string, string>
290
     */
291 71
    public static function declinateSecondDeclension($word, $animateness = false)
292
    {
293 71
        $word = S::lower($word);
294 71
        $last = S::slice($word, -1);
295 71
        $soft_last = $last === 'й'
296
            || (
297 68
                in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true)
298
                && (
299
                        (
300 24
                        static::isConsonant(S::slice($word, -2, -1))
301 20
                        && !static::isHissingConsonant(S::slice($word, -2, -1))
302
                        )
303 71
                    || S::slice($word, -2, -1) === 'и')
304
               );
305 71
        $prefix = static::getPrefixOfSecondDeclension($word, $last);
306
        $forms =  [
307 71
            Cases::IMENIT => $word,
308
        ];
309
310
        // RODIT
311 71
        $forms[Cases::RODIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'я', $prefix.'а');
312
313
        // DAT
314 71
        $forms[Cases::DAT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'ю', $prefix.'у');
315
316
        // VINIT
317 71
        if (in_array($last, ['о', 'е', 'ё'], true)) {
318 13
            $forms[Cases::VINIT] = $word;
319
        } else {
320 58
            $forms[Cases::VINIT] = static::getVinitCaseByAnimateness($forms, $animateness);
321
        }
322
323
        // TVORIT
324
        // if ($last == 'ь')
325
        // 	$forms[Cases::TVORIT] = $prefix.'ом';
326
        // else if ($last == 'й' || (static::isConsonant($last) && !static::isHissingConsonant($last)))
327
        // 	$forms[Cases::TVORIT] = $prefix.'ем';
328
        // else
329
        // 	$forms[Cases::TVORIT] = $prefix.'ом'; # http://morpher.ru/Russian/Spelling.aspx#sibilant
330 71
        if ((static::isHissingConsonant($last) && $last !== 'ш')
331 68
            || (in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true) && static::isHissingConsonant(S::slice($word, -2, -1)))
332 71
            || ($last === 'ц' && S::slice($word, -2) !== 'ец')) {
333 6
            $forms[Cases::TVORIT] = $prefix.'ем';
334 65 View Code Duplication
        } elseif (in_array($last, ['й'/*, 'ч', 'щ'*/], true) || $soft_last) {
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...
335 25
            $forms[Cases::TVORIT] = $prefix.'ем';
336
        } else {
337 40
            $forms[Cases::TVORIT] = $prefix.'ом';
338
        }
339
340
        // PREDLOJ
341 71
        $forms[Cases::PREDLOJ] = static::getPredCaseOf12Declensions($word, $last, $prefix);
342
343 71
        return $forms;
344
    }
345
346
    /**
347
     * Получение всех форм слова третьего склонения.
348
     * @param string $word
349
     * @return string[]
350
     * @phpstan-return array<string, string>
351
     */
352 4
    public static function declinateThirdDeclension($word)
353
    {
354 4
        $word = S::lower($word);
355 4
        $prefix = S::slice($word, 0, -1);
356
        return [
357 4
            Cases::IMENIT => $word,
358 4
            Cases::RODIT => $prefix.'и',
359 4
            Cases::DAT => $prefix.'и',
360 4
            Cases::VINIT => $word,
361 4
            Cases::TVORIT => $prefix.'ью',
362 4
            Cases::PREDLOJ => $prefix.'и',
363
        ];
364
    }
365
366
    /**
367
     * Склонение существительных, образованных от прилагательных и причастий.
368
     * Rules are from http://rusgram.narod.ru/1216-1231.html
369
     * @param string $word
370
     * @param bool $animateness
371
     * @return string[]
372
     * @phpstan-return array<string, string>
373
     */
374 8
    public static function declinateAdjective($word, $animateness)
0 ignored issues
show
Unused Code introduced by
The parameter $animateness is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
375
    {
376 8
        $prefix = S::slice($word, 0, -2);
377
378 8
        switch (S::slice($word, -2)) {
379
            // Male adjectives
380 8
            case 'ой':
381 7 View Code Duplication
            case 'ый':
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...
382
                return [
383 2
                    Cases::IMENIT => $word,
384 2
                    Cases::RODIT => $prefix.'ого',
385 2
                    Cases::DAT => $prefix.'ому',
386 2
                    Cases::VINIT => $word,
387 2
                    Cases::TVORIT => $prefix.'ым',
388 2
                    Cases::PREDLOJ => $prefix.'ом',
389
                ];
390
391 6 View Code Duplication
            case 'ий':
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...
392
                return [
393 1
                    Cases::IMENIT => $word,
394 1
                    Cases::RODIT => $prefix.'его',
395 1
                    Cases::DAT => $prefix.'ему',
396 1
                    Cases::VINIT => $prefix.'его',
397 1
                    Cases::TVORIT => $prefix.'им',
398 1
                    Cases::PREDLOJ => $prefix.'ем',
399
                ];
400
401
            // Neuter adjectives
402 5
            case 'ое':
403 4
            case 'ее':
404 2
                $prefix = S::slice($word, 0, -1);
405
                return [
406 2
                    Cases::IMENIT => $word,
407 2
                    Cases::RODIT => $prefix.'го',
408 2
                    Cases::DAT => $prefix.'му',
409 2
                    Cases::VINIT => $word,
410 2
                    Cases::TVORIT => S::slice($word, 0, -2).(S::slice($word, -2, -1) == 'о' ? 'ы' : 'и').'м',
411 2
                    Cases::PREDLOJ => $prefix.'м',
412
                ];
413
414
            // Female adjectives
415 3
            case 'ая':
416 3
                $ending = static::isHissingConsonant(S::slice($prefix, -1)) ? 'ей' : 'ой';
417
                return [
418 3
                    Cases::IMENIT => $word,
419 3
                    Cases::RODIT => $prefix.$ending,
420 3
                    Cases::DAT => $prefix.$ending,
421 3
                    Cases::VINIT => $prefix.'ую',
422 3
                    Cases::TVORIT => $prefix.$ending,
423 3
                    Cases::PREDLOJ => $prefix.$ending,
424
                ];
425
426
            default: throw new RuntimeException('Unreachable');
427
        }
428
    }
429
430
    /**
431
     * Получение одной формы слова (падежа).
432
     * @param string $word Слово
433
     * @param string $case Падеж
434
     * @param bool $animateness Признак одушевленности
435
     * @return string
436
     * @throws \Exception
437
     */
438 43
    public static function getCase($word, $case, $animateness = false)
439
    {
440 43
        $case = static::canonizeCase($case);
441 43
        $forms = static::getCases($word, $animateness);
442 43
        return $forms[$case];
443
    }
444
445
    /**
446
     * @param string $word
447
     * @param string $last
448
     * @return string
449
     */
450 107
    public static function getPrefixOfSecondDeclension($word, $last)
451
    {
452
        // слова с бегающей гласной в корне
453 107
        $runaway_vowels_list = static::getRunAwayVowelsList();
454 107
        if (isset($runaway_vowels_list[$word])) {
455 9
            $vowel_offset = $runaway_vowels_list[$word];
456 9
            $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 455 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...
457
        }
458
459 107
        if (in_array($last, ['о', 'е', 'ё', 'ь', 'й'], true)) {
460 49
            $prefix = S::slice($word, 0, -1);
461
        }
462
        // уменьшительные формы слов (котенок) и слова с суффиксом ок
463 61
        elseif (in_array(S::slice($word, -3), ['чок', 'лок', 'нок']) && S::length($word) > 3) {
464 4
            $prefix = S::slice($word, 0, -2) . 'к';
465
        }
466
        // слова с суффиксом бец
467 57
        elseif (S::slice($word, -3) === 'бец' && S::length($word) > 4) {
468 1
            $prefix = S::slice($word, 0, -3).'бц';
469
        } else {
470 56
            $prefix = $word;
471
        }
472 107
        return $prefix;
473
    }
474
475
    /**
476
     * @param string $word
477
     * @param string $last
478
     * @param string $prefix
479
     * @return string
480
     */
481 94
    public static function getPredCaseOf12Declensions($word, $last, $prefix)
482
    {
483 94
        if (in_array(S::slice($word, -2), ['ий', 'ие'], true)) {
484 6
            if ($last == 'ё') {
485
                return $prefix.'е';
486
            } else {
487 6
                return $prefix.'и';
488
            }
489
        } else {
490 88
            return $prefix.'е';
491
        }
492
    }
493
494
    /**
495
     * @return int[]|false[]
496
     */
497 107 View Code Duplication
    public static function getRunAwayVowelsList()
0 ignored issues
show
Duplication introduced by
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...
498
    {
499 107
        $runawayVowelsNormalized = [];
500 107
        foreach (NounDeclension::$runawayVowelsExceptions as $word) {
501 107
            $runawayVowelsNormalized[str_replace('*', '', $word)] = S::indexOf($word, '*') - 1;
502
        }
503 107
        return $runawayVowelsNormalized;
504
    }
505
}
506