Completed
Push — master ( c51d20...659fdf )
by f
01:34
created

src/Russian/GeographicalNamesInflection.php (1 issue)

Severity

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: https://ru.wikipedia.org/wiki/%D0%A1%D0%BA%D0%BB%D0%BE%D0%BD%D0%B5%D0%BD%D0%B8%D0%B5_%D0%B3%D0%B5%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%87%D0%B5%D1%81%D0%BA%D0%B8%D1%85_%D0%BD%D0%B0%D0%B7%D0%B2%D0%B0%D0%BD%D0%B8%D0%B9_%D0%B2_%D1%80%D1%83%D1%81%D1%81%D0%BA%D0%BE%D0%BC_%D1%8F%D0%B7%D1%8B%D0%BA%D0%B5
8
 */
9
class GeographicalNamesInflection extends \morphos\BaseInflection implements Cases
10
{
11
    use RussianLanguage, CasesHelper;
12
13
    /** @var string[]  */
14
    protected static $abbreviations = [
15
        'сша',
16
        'оаэ',
17
        'ссср',
18
        'юар',
19
    ];
20
21
    /** @var string[]  */
22
    protected static $delimiters = [
23
        ' ',
24
        '-на-',
25
        '-эль-',
26
        '-де-',
27
        '-сюр-',
28
        '-ан-',
29
        '-ла-',
30
        '-',
31
    ];
32
33
    /** @var string[]  */
34
    protected static $ovAbnormalExceptions = [
35
        'осташков',
36
    ];
37
38
    /** @var string[]  */
39
    protected static $immutableNames = [
40
        'алматы',
41
        'сочи',
42
        'гоа',
43
        'кемерово',
44
        'назарово',
45
        'иваново',
46
47
        // части
48
        'санкт',
49
        'йошкар',
50
        'улан',
51
        'ханты',
52
        'буда',
53
        'рублёво',
54
55
        'пунта',
56
        'куала',
57
        'рас',
58
        'шарм',
59
60
        'гран',
61
        'гранд',
62
        'вильфранш',
63
        'льорет',
64
        'андорра',
65
        'экс',
66
        'эс',
67
        'сен',
68
        'ла',
69
    ];
70
71
    /** @var string[]  */
72
    protected static $runawayVowelsExceptions = [
73
        'торжо*к',
74
        'волоче*к',
75
        'орё*л',
76
        'египе*т',
77
        'лунине*ц',
78
        'городо*к',
79
        'новогрудо*к',
80
        'острове*ц',
81
        'черепове*ц',
82
    ];
83
84
    /**
85
     * @var string[]
86
     * @phpstan-var array<string, string>
87
     */
88
    protected static $misspellings = [
89
        'орел' => 'орёл',
90
        'рублево' => 'рублёво',
91
    ];
92
93
    /**
94
     * @return int[]|false[]
95
     */
96 12 View Code Duplication
    protected static function getRunAwayVowelsList()
97
    {
98 12
        $runawayVowelsNormalized = [];
99 12
        foreach (static::$runawayVowelsExceptions as $word) {
100 12
            $runawayVowelsNormalized[str_replace('*', '', $word)] = S::indexOf($word, '*') - 1;
101
        }
102 12
        return $runawayVowelsNormalized;
103
    }
104
105
    /**
106
     * Проверяет, склоняемо ли название
107
     * @param string $name Название
108
     * @return bool
109
     */
110 2
    public static function isMutable($name)
111
    {
112 2
        $name = S::lower($name);
113
114
        // // ends with 'ы' or 'и': plural form
115
        // if (in_array(S::slice($name, -1), array('и', 'ы')))
116
        //     return false;
117
118 2
        if (in_array($name, static::$abbreviations, true) || in_array($name, static::$immutableNames, true)) {
119 2
            return false;
120
        }
121
122
        if (strpos($name, ' ') !== false) {
123
            // explode() is not applicable because Geographical unit may have few words
124
            $first_part = S::slice($name, 0, S::findFirstPosition($name, ' '));
125
            $last_part = S::slice($name,
126
                S::findLastPosition($name, ' ') + 1);
127
128
            // город N, село N, хутор N, район N, поселок N, округ N, республика N
129
            // N область, N край, N район, N волость
130
            if (in_array($first_part, ['город', 'село', 'хутор', 'район', 'поселок', 'округ', 'республика'], true)
131
                || in_array($last_part, ['край', 'область', 'район', 'волость'], true)) {
132
                return true;
133
            }
134
135
            // пгт N
136
            if ($first_part === 'пгт')
137
                return false;
138
        }
139
140
        // ends with 'е' or 'о', but not with 'ово/ёво/ево/ино/ыно'
141
        if (in_array(S::slice($name, -1), ['е', 'о'], true)
142
            && !in_array(S::slice($name, -3, -1), ['ов', 'ёв', 'ев', 'ин', 'ын'], true)) {
143
            return false;
144
        }
145
        return true;
146
    }
147
148
    /**
149
     * Получение всех форм названия
150
     * @param string $name
151
     * @return string[]
152
     * @phpstan-return array<string, string>
153
     * @throws \Exception
154
     */
155 45
    public static function getCases($name)
156
    {
157 45
        $name = S::lower($name);
158
159
        // Проверка на неизменяемость и сложное название
160 45
        if (in_array($name, static::$immutableNames, true)) {
161 2
            return array_fill_keys(
162 2
                [static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ, static::LOCATIVE]
163 2
                , S::name($name));
164
        }
165
166 45
        if (strpos($name, ' ') !== false) {
167 9
            $first_part = S::slice($name, 0, S::findFirstPosition($name, ' '));
168
            // город N, село N, хутор N, пгт N
169 9
            if (in_array($first_part, ['город', 'село', 'хутор', 'пгт', 'район', 'поселок', 'округ', 'республика'], true)) {
170 3
                if ($first_part === 'пгт')
171
                    return array_fill_keys(
172
                        [static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ, static::LOCATIVE],
173
                        'пгт '.S::name(S::slice($name, 4)));
174
175 3
                if ($first_part === 'республика') {
176 1
                    $prefix = array_map(['\\morphos\\S', 'name'], NounDeclension::getCases($first_part));
177
                } else {
178 2
                    $prefix = NounDeclension::getCases($first_part);
179
                }
180 3
                $prefix[Cases::LOCATIVE] = $prefix[Cases::PREDLOJ];
181
182 3
                return static::composeCasesFromWords([$prefix,
183 3
                    array_fill_keys(
184 3
                        array_merge(static::getAllCases(), [\morphos\Russian\Cases::LOCATIVE]),
185 3
                        S::name(S::slice($name, S::length($first_part) + 1)))
186
                ]);
187
            }
188
189 6
            $last_part = S::slice($name,
190 6
                S::findLastPosition($name, ' ') + 1);
191
            // N область, N край
192 6
            if (in_array($last_part, ['край', 'область', 'район', 'волость'], true)) {
193 2
                $last_part_cases = NounDeclension::getCases($last_part);
194 2
                $last_part_cases[Cases::LOCATIVE] = $last_part_cases[Cases::PREDLOJ];
195 2
                return static::composeCasesFromWords(
196
                    [
197 2
                        static::getCases(S::slice($name, 0, S::findLastPosition($name, ' '))),
198 2
                        $last_part_cases,
199
                    ]);
200
            }
201
        }
202
203
        // Сложное название с разделителем
204 42
        foreach (static::$delimiters as $delimiter) {
205 42
            if (strpos($name, $delimiter) !== false) {
206 7
                $parts = explode($delimiter, $name);
207 7
                $result = [];
208 7
                foreach ($parts as $i => $part) {
209 7
                    $result[$i] = static::getCases($part);
210
                }
211 42
                return static::composeCasesFromWords($result, $delimiter);
212
            }
213
        }
214
215
        // Исправление ошибок
216 42
        if (array_key_exists($name, static::$misspellings)) {
217
            $name = static::$misspellings[$name];
218
        }
219
220
        // Само склонение
221 42
        if (!in_array($name, static::$abbreviations, true)) {
222 40
            switch (S::slice($name, -2)) {
223
                // Нижний, Русский
224 40
                case 'ий':
225 3
                    $prefix = S::name(S::slice($name, 0, -2));
226
                    return [
227 3
                        static::IMENIT => $prefix.'ий',
228 3
                        static::RODIT => $prefix.(static::isVelarConsonant(S::slice($name, -3, -2)) ? 'ого' : 'его'),
229 3
                        static::DAT => $prefix.(static::isVelarConsonant(S::slice($name, -3, -2)) ? 'ому' : 'ему'),
230 3
                        static::VINIT => $prefix.'ий',
231 3
                        static::TVORIT => $prefix.'им',
232 3
                        static::PREDLOJ => $prefix.(static::chooseEndingBySonority($prefix, 'ем', 'ом')),
233 3
                        static::LOCATIVE => $prefix.(static::chooseEndingBySonority($prefix, 'ем', 'ом')),
234
                    ];
235
236
                // Ростовская
237 39
                case 'ая':
238 1
                    $prefix = S::name(S::slice($name, 0, -2));
239
                    return [
240 1
                        static::IMENIT => $prefix.'ая',
241 1
                        static::RODIT => $prefix.'ой',
242 1
                        static::DAT => $prefix.'ой',
243 1
                        static::VINIT => $prefix.'ую',
244 1
                        static::TVORIT => $prefix.'ой',
245 1
                        static::PREDLOJ => $prefix.'ой',
246 1
                        static::LOCATIVE => $prefix.'ой',
247
                    ];
248
249
                // Нижняя, Верхняя, Средняя
250 38
                case 'яя':
251
                    $prefix = S::name(S::slice($name, 0, -2));
252
                    return [
253
                        static::IMENIT => $prefix.'яя',
254
                        static::RODIT => $prefix.'ей',
255
                        static::DAT => $prefix.'ей',
256
                        static::VINIT => $prefix.'юю',
257
                        static::TVORIT => $prefix.'ей',
258
                        static::PREDLOJ => $prefix.'ей',
259
                        static::LOCATIVE => $prefix.'ей',
260
                    ];
261
262
                // Россошь
263 38
                case 'шь':
264
                // Пермь, Кемь
265 37
                case 'мь':
266
                // Рязань, Назрань
267 36
                case 'нь':
268
                // Сысерть
269 35
                case 'ть':
270
                // Керчь
271 35
                case 'чь':
272 4
                    $prefix = S::name(S::slice($name, 0, -1));
273
                    return [
274 4
                        static::IMENIT => $prefix.'ь',
275 4
                        static::RODIT => $prefix.'и',
276 4
                        static::DAT => $prefix.'и',
277 4
                        static::VINIT => $prefix.'ь',
278 4
                        static::TVORIT => $prefix.'ью',
279 4
                        static::PREDLOJ => $prefix.'и',
280 4
                        static::LOCATIVE => $prefix.'и',
281
                    ];
282
283
                // Грозный, Благодарный
284 34
                case 'ый':
285 2
                    $prefix = S::name(S::slice($name, 0, -2));
286
                    return [
287 2
                        static::IMENIT => $prefix.'ый',
288 2
                        static::RODIT => $prefix.'ого',
289 2
                        static::DAT => $prefix.'ому',
290 2
                        static::VINIT => $prefix.'ый',
291 2
                        static::TVORIT => $prefix.'ым',
292 2
                        static::PREDLOJ => $prefix.'ом',
293 2
                        static::LOCATIVE => $prefix.'ом',
294
                    ];
295
296
                // Ставрополь, Ярославль, Электросталь
297 32
                case 'ль':
298 2
                    $prefix = S::name(S::slice($name, 0, -1));
299
300 2
                    if ($name === 'электросталь')
301
                        return [
302 1
                            static::IMENIT => $prefix.'ь',
303 1
                            static::RODIT => $prefix.'и',
304 1
                            static::DAT => $prefix.'и',
305 1
                            static::VINIT => $prefix.'ь',
306 1
                            static::TVORIT => $prefix.'ью',
307 1
                            static::PREDLOJ => $prefix.'и',
308 1
                            static::LOCATIVE => $prefix.'и',
309
                        ];
310
311
                    return [
312 1
                        static::IMENIT => $prefix.'ь',
313 1
                        static::RODIT => $prefix.'я',
314 1
                        static::DAT => $prefix.'ю',
315 1
                        static::VINIT => $prefix.'ь',
316 1
                        static::TVORIT => $prefix.'ем',
317 1
                        static::PREDLOJ => $prefix.'е',
318 1
                        static::LOCATIVE => $prefix.'е',
319
                    ];
320
321
                // Тверь, Анадырь
322 30
                case 'рь':
323 2
                    $prefix = S::name(S::slice($name, 0, -1));
324 2
                    $last_vowel = S::slice($prefix, -2, -1);
325
                    return [
326 2
                        static::IMENIT => $prefix . 'ь',
327 2
                        static::RODIT => $prefix . (static::isBinaryVowel($last_vowel) ? 'и' : 'я'),
328 2
                        static::DAT => $prefix . (static::isBinaryVowel($last_vowel) ? 'и' : 'ю'),
329 2
                        static::VINIT => $prefix . 'ь',
330 2
                        static::TVORIT => $prefix . (static::isBinaryVowel($last_vowel) ? 'ью' : 'ем'),
331 2
                        static::PREDLOJ => $prefix . (static::isBinaryVowel($last_vowel) ? 'и' : 'е'),
332 2
                        static::LOCATIVE => $prefix . (static::isBinaryVowel($last_vowel) ? 'и' : 'е'),
333
                    ];
334
335
                // Березники, Ессентуки
336 28
                case 'ки':
337
                // Старые Дороги
338 27
                case 'ги':
339
                // Ушачи, Ивацевичи
340 27
                case 'чи':
341 3
                    $prefix = S::name(S::slice($name, 0, -1));
342
                    return [
343 3
                        static::IMENIT => $prefix . 'и',
344 3
                        static::RODIT => ($name === 'луки'
345 1
                            ? $prefix
346 2
                            : (S::slice($name, -2) === 'чи'
347 1
                                ? $prefix . 'ей'
348 3
                                : $prefix . 'ов')),
349 3
                        static::DAT => $prefix . 'ам',
350 3
                        static::VINIT => $prefix . 'и',
351 3
                        static::TVORIT => $prefix . 'ами',
352 3
                        static::PREDLOJ => $prefix . 'ах',
353 3
                        static::LOCATIVE => $prefix . 'ах',
354
                    ];
355
356
                // Набережные
357 26
                case 'ые':
358
                // Великие
359 26
                case 'ие':
360 2
                    $prefix = S::name(S::slice($name, 0, -1));
361
                    return [
362 2
                        static::IMENIT => $prefix . 'е',
363 2
                        static::RODIT => $prefix . 'х',
364 2
                        static::DAT => $prefix . 'м',
365 2
                        static::VINIT => $prefix . 'е',
366 2
                        static::TVORIT => $prefix . 'ми',
367 2
                        static::PREDLOJ => $prefix . 'х',
368 2
                        static::LOCATIVE => $prefix . 'х',
369
                    ];
370
371
                // Челны
372 25
                case 'ны':
373
                // Мосты
374 24
                case 'ты':
375
                // Столбцы
376 24
                case 'цы':
377 2
                    $prefix = S::name(S::slice($name, 0, -1));
378
                    return [
379 2
                        static::IMENIT => $prefix . 'ы',
380 2
                        static::RODIT => $prefix . 'ов',
381 2
                        static::DAT => $prefix . 'ам',
382 2
                        static::VINIT => $prefix . 'ы',
383 2
                        static::TVORIT => $prefix . 'ами',
384 2
                        static::PREDLOJ => $prefix . 'ах',
385 2
                        static::LOCATIVE => $prefix . 'ах',
386
                    ];
387
388
                // Глубокое
389 23
                case 'ое':
390 1
                    $prefix = S::name(S::slice($name, 0, -2));
391
                    return [
392 1
                        static::IMENIT => $prefix.'ое',
393 1
                        static::RODIT => $prefix.'ого',
394 1
                        static::DAT => $prefix.'ому',
395 1
                        static::VINIT => $prefix.'ое',
396 1
                        static::TVORIT => $prefix.'им',
397 1
                        static::PREDLOJ => $prefix.'ом',
398 1
                        static::LOCATIVE => $prefix.'ом',
399
                    ];
400
401
            }
402
403 22
            switch (S::slice($name, -1)) {
404 22
                case 'р':
405
                    // Бор
406
                    $prefix = S::name(S::slice($name, 0, -1));
407
                    return [
408
                        static::IMENIT => $prefix.'р',
409
                        static::RODIT => $prefix.'ра',
410
                        static::DAT => $prefix.'ру',
411
                        static::VINIT => $prefix.'р',
412
                        static::TVORIT => $prefix.'ром',
413
                        static::PREDLOJ => $prefix.'ре',
414
                        static::LOCATIVE => $prefix.'ру',
415
                    ];
416
417 22
                case 'ы':
418
                    // Чебоксары, Шахты
419 1
                    $prefix = S::name(S::slice($name, 0, -1));
420
                    return [
421 1
                        static::IMENIT => $prefix.'ы',
422 1
                        static::RODIT => $prefix,
423 1
                        static::DAT => $prefix.'ам',
424 1
                        static::VINIT => $prefix.'ы',
425 1
                        static::TVORIT => $prefix.'ами',
426 1
                        static::PREDLOJ => $prefix.'ах',
427 1
                        static::LOCATIVE => $prefix.'ах',
428
                    ];
429
430 21
                case 'я':
431
                    // Азия
432 1
                    $prefix = S::name(S::slice($name, 0, -1));
433
                    return [
434 1
                        static::IMENIT => S::name($name),
435 1
                        static::RODIT => $prefix.'и',
436 1
                        static::DAT => $prefix.'и',
437 1
                        static::VINIT => $prefix.'ю',
438 1
                        static::TVORIT => $prefix.'ей',
439 1
                        static::PREDLOJ => $prefix.'и',
440 1
                        static::LOCATIVE => $prefix.'и',
441
                    ];
442
443 20
                case 'а':
444
                    // Москва, Рига
445 5
                    $prefix = S::name(S::slice($name, 0, -1));
446
                    return [
447 5
                        static::IMENIT => $prefix.'а',
448 5
                        static::RODIT => $prefix.(static::isVelarConsonant(S::slice($name, -2, -1)) || static::isHissingConsonant(S::slice($name, -2, -1)) ? 'и' : 'ы'),
449 5
                        static::DAT => $prefix.'е',
450 5
                        static::VINIT => $prefix.'у',
451 5
                        static::TVORIT => $prefix.'ой',
452 5
                        static::PREDLOJ => $prefix.'е',
453 5
                        static::LOCATIVE => $prefix.'е',
454
                    ];
455
456 15
                case 'й':
457
                    // Ишимбай
458 2
                    $prefix = S::name(S::slice($name, 0, -1));
459
                    return [
460 2
                        static::IMENIT => $prefix . 'й',
461 2
                        static::RODIT => $prefix . 'я',
462 2
                        static::DAT => $prefix . 'ю',
463 2
                        static::VINIT => $prefix . 'й',
464 2
                        static::TVORIT => $prefix . 'ем',
465 2
                        static::PREDLOJ => $prefix . 'е',
466 2
                        static::LOCATIVE => $prefix . 'е',
467
                    ];
468
            }
469
470 13
            if (static::isConsonant(S::slice($name,  -1)) && !in_array($name, static::$ovAbnormalExceptions, true)) {
471 12
                $runaway_vowels_list = static::getRunAwayVowelsList();
472
473
                // if run-away vowel in name
474 12
                if (isset($runaway_vowels_list[$name])) {
475 4
                    $runaway_vowel_offset = $runaway_vowels_list[$name];
476 4
                    $prefix = S::name(S::slice($name, 0, $runaway_vowel_offset) . S::slice($name, $runaway_vowel_offset + 1));
477
                } else {
478 8
                    $prefix = S::name($name);
479
                }
480
481
                // Париж, Валаам, Киев
482
                return [
0 ignored issues
show
The expression return array(static::IME... seems to be an array, but some of its elements' types (boolean) are incompatible with the return type documented by morphos\Russian\Geograph...mesInflection::getCases of type string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
483 12
                    static::IMENIT => S::name($name),
484 12
                    static::RODIT => $prefix . 'а',
485 12
                    static::DAT => $prefix . 'у',
486 12
                    static::VINIT => S::name($name),
487 12
                    static::TVORIT => $prefix . (static::isVelarConsonant(S::slice($name, -2, -1)) ? 'ем' : 'ом'),
488 12
                    static::PREDLOJ => $prefix . 'е',
489 12
                    static::LOCATIVE => $prefix.($name === 'крым' ? 'у' : 'е'),
490
                ];
491
            }
492
493
            // ов, ово, ёв, ёво, ев, ево, ...
494 2
            $suffixes = ['ов', 'ёв', 'ев', 'ин', 'ын'];
495 2
            if ((in_array(S::slice($name, -1), ['е', 'о'], true) && in_array(S::slice($name, -3, -1), $suffixes, true)) || in_array(S::slice($name, -2), $suffixes, true)) {
496
                // ово, ёво, ...
497 1
                if (in_array(S::slice($name, -3, -1), $suffixes, true)) {
498
                    $prefix = S::name(S::slice($name, 0, -1));
499
                }
500
                // ов, её, ...
501 1
                elseif (in_array(S::slice($name, -2), $suffixes, true)) {
502 1
                    $prefix = S::name($name);
503
                } else {
504
                    $prefix = '';
505
                }
506
507
                return [
508 1
                    static::IMENIT => S::name($name),
509 1
                    static::RODIT => $prefix.'а',
510 1
                    static::DAT => $prefix.'у',
511 1
                    static::VINIT => S::name($name),
512 1
                    static::TVORIT => $prefix.'ым',
513 1
                    static::PREDLOJ => $prefix.'е',
514 1
                    static::LOCATIVE => $prefix.'е',
515
                ];
516
            }
517
        }
518
519
        // if no rules matches or name is immutable
520 3
        $name = in_array($name, static::$abbreviations, true) ? S::upper($name) : S::name($name);
521 3
        return array_fill_keys(
522 3
            [static::IMENIT, static::RODIT, static::DAT, static::VINIT, static::TVORIT, static::PREDLOJ, static::LOCATIVE],
523 3
        $name);
524
    }
525
526
    /**
527
     * Получение одной формы (падежа) названия.
528
     * @param string $name  Название
529
     * @param string $case Падеж. Одна из констант {@see \morphos\Russian\Cases} или {@see \morphos\Cases}.
530
     * @see \morphos\Russian\Cases
531
     * @return string
532
     * @throws \Exception
533
     */
534
    public static function getCase($name, $case)
535
    {
536
        $case = static::canonizeCase($case);
537
        $forms = static::getCases($name);
538
        return $forms[$case];
539
    }
540
}
541