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