NounDeclension   F
last analyzed

Complexity

Total Complexity 69

Size/Duplication

Total Lines 467
Duplicated Lines 9.21 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 96.86%

Importance

Changes 0
Metric Value
dl 43
loc 467
ccs 154
cts 159
cp 0.9686
rs 2.88
c 0
b 0
f 0
wmc 69
lcom 1
cbo 4

11 Methods

Rating   Name   Duplication   Size   Complexity  
A isMutable() 0 8 3
B detectGender() 0 14 7
C getDeclension() 0 18 12
B getCases() 13 48 8
A declinateFirstDeclension() 3 35 4
C declinateSecondDeclension() 3 54 14
A declinateThirdDeclension() 0 13 1
B declinateAdjective() 18 55 9
A getCase() 0 6 1
B getPrefixOfSecondDeclension() 6 20 7
A getPredCaseOf12Declensions() 0 12 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like NounDeclension often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use NounDeclension, and based on these observations, apply Extract Interface, too.

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
    /** @var string[]  */
106
    protected static $masculineWithSoftAndRunAwayVowels = [
107
        'день',
108
        'пень',
109
        'парень',
110
        'камень',
111
        'корень',
112
        'трутень',
113
    ];
114
115
    /**
116
     * Проверка, изменяемое ли слово.
117
     * @param string $word Слово для проверки
118
     * @param bool $animateness Признак одушевленности
119
     * @return bool
120
     */
121 43
    public static function isMutable($word, $animateness = false)
0 ignored issues
show
Unused Code introduced by wapmorgan
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...
122
    {
123 43
        $word = S::lower($word);
124 43
        if (in_array(S::slice($word, -1), ['у', 'и', 'е', 'о', 'ю'], true) || in_array($word, static::$immutableWords, true)) {
125 43
            return false;
126
        }
127
        return true;
128
    }
129
130
    /**
131
     * Определение рода существительного.
132
     * @param string $word
133
     * @return string
134
     */
135 8
    public static function detectGender($word)
136
    {
137 8
    	$word = S::lower($word);
138 8
    	$last = S::slice($word, -1);
139
		// пытаемся угадать род объекта, хотя бы примерно, чтобы правильно склонять
140 8
		if (S::slice($word, -2) == 'мя' || in_array($last, ['о', 'е', 'и', 'у'], true))
141 2
			return static::NEUTER;
142
143 6
		if (in_array($last, ['а', 'я'], true) ||
144 6
			($last == 'ь' && !in_array($word, static::$masculineWithSoft, true) && !in_array($word, static::$masculineWithSoftAndRunAwayVowels, true)))
145 3
			return static::FEMALE;
146
147 3
		return static::MALE;
148
    }
149
150
    /**
151
     * Определение склонения (по школьной программе) существительного.
152
     * @param string $word
153
     * @param bool $animateness
154
     * @return int
155
     */
156 199
    public static function getDeclension($word, $animateness = false)
0 ignored issues
show
Unused Code introduced by wapmorgan
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...
157
    {
158 199
        $word = S::lower($word);
159 199
        $last = S::slice($word, -1);
160 199
        if (isset(static::$abnormalExceptions[$word]) || in_array($word, static::$abnormalExceptions, true)) {
161 3
            return 2;
162
        }
163
164 196
        if (in_array($last, ['а', 'я'], true) && S::slice($word, -2) != 'мя') {
165 64
            return 1;
166 147
        } elseif (static::isConsonant($last) || in_array($last, ['о', 'е', 'ё'], true)
167 41
            || ($last == 'ь' && static::isConsonant(S::slice($word, -2, -1)) && !static::isHissingConsonant(S::slice($word, -2, -1))
168 147
                && (in_array($word, static::$masculineWithSoft, true)) || in_array($word, static::$masculineWithSoftAndRunAwayVowels, true))) {
169 138
            return 2;
170
        } else {
171 9
            return 3;
172
        }
173
    }
174
175
    /**
176
     * Получение слова во всех 6 падежах.
177
     * @param string $word
178
     * @param bool $animateness Признак одушевлённости
179
     * @return string[]
180
     * @phpstan-return array<string, string>
181
     */
182 109
    public static function getCases($word, $animateness = false)
183
    {
184 109
        $word = S::lower($word);
185
186
        // Адъективное склонение (Сущ, образованные от прилагательных и причастий) - прохожий, существительное
187 109
        if (static::isAdjectiveNoun($word)) {
188 8
            return static::declinateAdjective($word, $animateness);
189
        }
190
191
        // Субстантивное склонение (существительные)
192 101 View Code Duplication
        if (in_array($word, static::$immutableWords, true)) {
0 ignored issues
show
Duplication introduced by wapmorgan
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...
193
            return [
194 3
                static::IMENIT => $word,
195 3
                static::RODIT => $word,
196 3
                static::DAT => $word,
197 3
                static::VINIT => $word,
198 3
                static::TVORIT => $word,
199 3
                static::PREDLOJ => $word,
200
            ];
201
        }
202
203 98 View Code Duplication
        if (isset(static::$abnormalExceptions[$word])) {
0 ignored issues
show
Duplication introduced by Anatoly Pashin
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 2
            return array_combine([static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ], static::$abnormalExceptions[$word]);
205
        }
206
207 96
        if (in_array($word, static::$abnormalExceptions, true)) {
208 1
            $prefix = S::slice($word, 0, -1);
209
            return [
210 1
                static::IMENIT => $word,
211 1
                static::RODIT => $prefix.'ени',
212 1
                static::DAT => $prefix.'ени',
213 1
                static::VINIT => $word,
214 1
                static::TVORIT => $prefix.'енем',
215 1
                static::PREDLOJ => $prefix.'ени',
216
            ];
217
        }
218
219 95
        switch (static::getDeclension($word)) {
220 95
            case static::FIRST_DECLENSION:
221 25
                return static::declinateFirstDeclension($word);
222 72
            case static::SECOND_DECLENSION:
223 68
                return static::declinateSecondDeclension($word, $animateness);
224 4
            case static::THIRD_DECLENSION:
225 4
                return static::declinateThirdDeclension($word);
226
227
            default: throw new RuntimeException('Unreachable');
228
        }
229
    }
230
231
    /**
232
     * Получение всех форм слова первого склонения.
233
     * @param string $word
234
     * @return string[]
235
     * @phpstan-return array<string, string>
236
     */
237 25
    public static function declinateFirstDeclension($word)
238
    {
239 25
        $word = S::lower($word);
240 25
        $prefix = S::slice($word, 0, -1);
241 25
        $last = S::slice($word, -1);
242 25
        $soft_last = static::checkLastConsonantSoftness($word);
243
        $forms =  [
244 25
            Cases::IMENIT => $word,
245
        ];
246
247
        // RODIT
248 25
        $forms[Cases::RODIT] = static::chooseVowelAfterConsonant($last, $soft_last || (in_array(S::slice($word, -2, -1), ['г', 'к', 'х'], true)), $prefix.'и', $prefix.'ы');
249
250
        // DAT
251 25
        $forms[Cases::DAT] = static::getPredCaseOf12Declensions($word, $last, $prefix);
252
253
        // VINIT
254 25
        $forms[Cases::VINIT] = static::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) !== 'ч', $prefix.'ю', $prefix.'у');
255
256
        // TVORIT
257 25
        if ($last === 'ь') {
258
            $forms[Cases::TVORIT] = $prefix.'ой';
259 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by wapmorgan
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...
260 25
            $forms[Cases::TVORIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'ей', $prefix.'ой');
261
        }
262
263
        // 	if ($last == 'й' || (static::isConsonant($last) && !static::isHissingConsonant($last)) || static::checkLastConsonantSoftness($word))
264
        // 	$forms[Cases::TVORIT] = $prefix.'ей';
265
        // else
266
        // 	$forms[Cases::TVORIT] = $prefix.'ой'; # http://morpher.ru/Russian/Spelling.aspx#sibilant
267
268
        // PREDLOJ the same as DAT
269 25
        $forms[Cases::PREDLOJ] = $forms[Cases::DAT];
270 25
        return $forms;
271
    }
272
273
    /**
274
     * Получение всех форм слова второго склонения.
275
     * @param string $word
276
     * @param bool $animateness
277
     * @return string[]
278
     * @phpstan-return array<string, string>
279
     */
280 68
    public static function declinateSecondDeclension($word, $animateness = false)
281
    {
282 68
        $word = S::lower($word);
283 68
        $last = S::slice($word, -1);
284 68
        $soft_last = $last === 'й'
285
            || (
286 65
                in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true)
287
                && (
288
                        (
289 23
                        static::isConsonant(S::slice($word, -2, -1))
290 19
                        && !static::isHissingConsonant(S::slice($word, -2, -1))
291
                        )
292 68
                    || S::slice($word, -2, -1) === 'и')
293
               );
294 68
        $prefix = static::getPrefixOfSecondDeclension($word, $last);
295
        $forms =  [
296 68
            Cases::IMENIT => $word,
297
        ];
298
299
        // RODIT
300 68
        $forms[Cases::RODIT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'я', $prefix.'а');
301
302
        // DAT
303 68
        $forms[Cases::DAT] = static::chooseVowelAfterConsonant($last, $soft_last, $prefix.'ю', $prefix.'у');
304
305
        // VINIT
306 68
        if (in_array($last, ['о', 'е', 'ё'], true)) {
307 13
            $forms[Cases::VINIT] = $word;
308
        } else {
309 55
            $forms[Cases::VINIT] = static::getVinitCaseByAnimateness($forms, $animateness);
310
        }
311
312
        // TVORIT
313
        // if ($last == 'ь')
314
        // 	$forms[Cases::TVORIT] = $prefix.'ом';
315
        // else if ($last == 'й' || (static::isConsonant($last) && !static::isHissingConsonant($last)))
316
        // 	$forms[Cases::TVORIT] = $prefix.'ем';
317
        // else
318
        // 	$forms[Cases::TVORIT] = $prefix.'ом'; # http://morpher.ru/Russian/Spelling.aspx#sibilant
319 68
        if ((static::isHissingConsonant($last) && $last !== 'ш')
320 65
            || (in_array($last, ['ь', 'е', 'ё', 'ю', 'я'], true) && static::isHissingConsonant(S::slice($word, -2, -1)))
321 68
            || ($last === 'ц' && S::slice($word, -2) !== 'ец')) {
322 6
            $forms[Cases::TVORIT] = $prefix.'ем';
323 62 View Code Duplication
        } elseif (in_array($last, ['й'/*, 'ч', 'щ'*/], true) || $soft_last) {
0 ignored issues
show
Duplication introduced by wapmorgan
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...
324 24
            $forms[Cases::TVORIT] = $prefix.'ем';
325
        } else {
326 38
            $forms[Cases::TVORIT] = $prefix.'ом';
327
        }
328
329
        // PREDLOJ
330 68
        $forms[Cases::PREDLOJ] = static::getPredCaseOf12Declensions($word, $last, $prefix);
331
332 68
        return $forms;
333
    }
334
335
    /**
336
     * Получение всех форм слова третьего склонения.
337
     * @param string $word
338
     * @return string[]
339
     * @phpstan-return array<string, string>
340
     */
341 4
    public static function declinateThirdDeclension($word)
342
    {
343 4
        $word = S::lower($word);
344 4
        $prefix = S::slice($word, 0, -1);
345
        return [
346 4
            Cases::IMENIT => $word,
347 4
            Cases::RODIT => $prefix.'и',
348 4
            Cases::DAT => $prefix.'и',
349 4
            Cases::VINIT => $word,
350 4
            Cases::TVORIT => $prefix.'ью',
351 4
            Cases::PREDLOJ => $prefix.'и',
352
        ];
353
    }
354
355
    /**
356
     * Склонение существительных, образованных от прилагательных и причастий.
357
     * Rules are from http://rusgram.narod.ru/1216-1231.html
358
     * @param string $word
359
     * @param bool $animateness
360
     * @return string[]
361
     * @phpstan-return array<string, string>
362
     */
363 8
    public static function declinateAdjective($word, $animateness)
0 ignored issues
show
Unused Code introduced by wapmorgan
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...
364
    {
365 8
        $prefix = S::slice($word, 0, -2);
366
367 8
        switch (S::slice($word, -2)) {
368
            // Male adjectives
369 8
            case 'ой':
370 7 View Code Duplication
            case 'ый':
0 ignored issues
show
Duplication introduced by wapmorgan
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...
371
                return [
372 2
                    Cases::IMENIT => $word,
373 2
                    Cases::RODIT => $prefix.'ого',
374 2
                    Cases::DAT => $prefix.'ому',
375 2
                    Cases::VINIT => $word,
376 2
                    Cases::TVORIT => $prefix.'ым',
377 2
                    Cases::PREDLOJ => $prefix.'ом',
378
                ];
379
380 6 View Code Duplication
            case 'ий':
0 ignored issues
show
Duplication introduced by wapmorgan
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...
381
                return [
382 1
                    Cases::IMENIT => $word,
383 1
                    Cases::RODIT => $prefix.'его',
384 1
                    Cases::DAT => $prefix.'ему',
385 1
                    Cases::VINIT => $prefix.'его',
386 1
                    Cases::TVORIT => $prefix.'им',
387 1
                    Cases::PREDLOJ => $prefix.'ем',
388
                ];
389
390
            // Neuter adjectives
391 5
            case 'ое':
392 4
            case 'ее':
393 2
                $prefix = S::slice($word, 0, -1);
394
                return [
395 2
                    Cases::IMENIT => $word,
396 2
                    Cases::RODIT => $prefix.'го',
397 2
                    Cases::DAT => $prefix.'му',
398 2
                    Cases::VINIT => $word,
399 2
                    Cases::TVORIT => S::slice($word, 0, -2).(S::slice($word, -2, -1) == 'о' ? 'ы' : 'и').'м',
400 2
                    Cases::PREDLOJ => $prefix.'м',
401
                ];
402
403
            // Female adjectives
404 3
            case 'ая':
405 3
                $ending = static::isHissingConsonant(S::slice($prefix, -1)) ? 'ей' : 'ой';
406
                return [
407 3
                    Cases::IMENIT => $word,
408 3
                    Cases::RODIT => $prefix.$ending,
409 3
                    Cases::DAT => $prefix.$ending,
410 3
                    Cases::VINIT => $prefix.'ую',
411 3
                    Cases::TVORIT => $prefix.$ending,
412 3
                    Cases::PREDLOJ => $prefix.$ending,
413
                ];
414
415
            default: throw new RuntimeException('Unreachable');
416
        }
417
    }
418
419
    /**
420
     * Получение одной формы слова (падежа).
421
     * @param string $word Слово
422
     * @param string $case Падеж
423
     * @param bool $animateness Признак одушевленности
424
     * @return string
425
     * @throws \Exception
426
     */
427 43
    public static function getCase($word, $case, $animateness = false)
428
    {
429 43
        $case = static::canonizeCase($case);
430 43
        $forms = static::getCases($word, $animateness);
431 43
        return $forms[$case];
432
    }
433
434
    /**
435
     * @param string $word
436
     * @param string $last
437
     * @return string
438
     */
439 103
    public static function getPrefixOfSecondDeclension($word, $last)
440
    {
441
        // слова с бегающей гласной в корне
442 103
        if (in_array($word, static::$masculineWithSoftAndRunAwayVowels, true)) {
443 7
            $prefix = S::slice($word, 0, -3).S::slice($word, -2, -1);
444 98
        } elseif (in_array($last, ['о', 'е', 'ё', 'ь', 'й'], true)) {
445 40
            $prefix = S::slice($word, 0, -1);
446
        }
447
        // уменьшительные формы слов (котенок) и слова с суффиксом ок
448 59 View Code Duplication
        elseif (S::slice($word, -2) === 'ок' && S::length($word) > 3) {
0 ignored issues
show
Duplication introduced by wapmorgan
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...
449 4
            $prefix = S::slice($word, 0, -2) . 'к';
450
        }
451
        // слова с суффиксом бец
452 55 View Code Duplication
        elseif (S::slice($word, -3) === 'бец' && S::length($word) > 4) {
0 ignored issues
show
Duplication introduced by wapmorgan
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...
453 1
            $prefix = S::slice($word, 0, -3).'бц';
454
        } else {
455 54
            $prefix = $word;
456
        }
457 103
        return $prefix;
458
    }
459
460
    /**
461
     * @param string $word
462
     * @param string $last
463
     * @param string $prefix
464
     * @return string
465
     */
466 91
    public static function getPredCaseOf12Declensions($word, $last, $prefix)
467
    {
468 91
        if (in_array(S::slice($word, -2), ['ий', 'ие'], true)) {
469 6
            if ($last == 'ё') {
470
                return $prefix.'е';
471
            } else {
472 6
                return $prefix.'и';
473
            }
474
        } else {
475 85
            return $prefix.'е';
476
        }
477
    }
478
}
479