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
|
|
|
public static function pluralize($word, $count = 2, $animateness = false) |
57
|
|
|
{ |
58
|
|
|
switch (self::getNumeralForm($count)) { |
59
|
|
|
case self::ONE: |
60
|
|
|
return $word; |
61
|
|
|
case self::TWO_FOUR: |
62
|
|
|
return NounDeclension::getCase($word, self::RODIT, $animateness); |
63
|
|
|
case self::FIVE_OTHER: |
64
|
|
|
return NounPluralization::getCase($word, self::RODIT, $animateness); |
65
|
|
|
} |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
public static function getNumeralForm($count) |
69
|
|
|
{ |
70
|
|
|
if ($count > 100) { |
71
|
|
|
$count %= 100; |
72
|
|
|
} |
73
|
|
|
$ending = $count % 10; |
74
|
|
|
if (($count > 20 && $ending == 1) || $count == 1) { |
75
|
|
|
return self::ONE; |
76
|
|
|
} elseif (($count > 20 && in_array($ending, range(2, 4))) || in_array($count, range(2, 4))) { |
77
|
|
|
return self::TWO_FOUR; |
78
|
|
|
} else { |
79
|
|
|
return self::FIVE_OTHER; |
80
|
|
|
} |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
public static function getCase($word, $case, $animateness = false) |
84
|
|
|
{ |
85
|
|
|
$case = self::canonizeCase($case); |
86
|
|
|
$forms = self::getCases($word, $animateness); |
87
|
|
|
return $forms[$case]; |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
public static function getCases($word, $animateness = false) |
91
|
|
|
{ |
92
|
|
|
$word = S::lower($word); |
93
|
|
|
|
94
|
|
|
if (in_array($word, self::$immutableWords)) { |
95
|
|
|
return array( |
96
|
|
|
self::IMENIT => $word, |
97
|
|
|
self::RODIT => $word, |
98
|
|
|
self::DAT => $word, |
99
|
|
|
self::VINIT => $word, |
100
|
|
|
self::TVORIT => $word, |
101
|
|
|
self::PREDLOJ => self::choosePrepositionByFirstLetter($word, 'об', 'о').' '.$word, |
102
|
|
|
); |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
// Адъективное склонение (Сущ, образованные от прилагательных и причастий) - прохожий, существительное |
106
|
|
View Code Duplication |
if (in_array(S::slice($word, -2), array('ой', 'ий', 'ый', 'ая', 'ое', 'ее')) && $word != 'гений') { |
|
|
|
|
107
|
|
|
return self::declinateAdjective($word, $animateness); |
108
|
|
|
} |
109
|
|
|
|
110
|
|
|
// Субстантивное склонение (существительные) |
111
|
|
|
return self::declinateSubstative($word, $animateness); |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
protected static function declinateSubstative($word, $animateness) |
115
|
|
|
{ |
116
|
|
|
$prefix = S::slice($word, 0, -1); |
117
|
|
|
$last = S::slice($word, -1); |
118
|
|
|
|
119
|
|
|
$runaway_vowels_list = static::getRunAwayVowelsList(); |
120
|
|
|
if (isset($runaway_vowels_list[$word])) { |
121
|
|
|
$vowel_offset = $runaway_vowels_list[$word]; |
122
|
|
|
$word = S::slice($word, 0, $vowel_offset) . S::slice($word, $vowel_offset + 1); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
if (($declension = NounDeclension::getDeclension($word)) == NounDeclension::SECOND_DECLENSION) { |
126
|
|
|
$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) == 'и')); |
127
|
|
|
$prefix = NounDeclension::getPrefixOfSecondDeclension($word, $last); |
128
|
|
|
} elseif ($declension == NounDeclension::FIRST_DECLENSION) { |
129
|
|
|
$soft_last = self::checkLastConsonantSoftness($word); |
130
|
|
|
} else { |
131
|
|
|
$soft_last = S::slice($word, -2) == 'сь'; |
132
|
|
|
} |
133
|
|
|
|
134
|
|
|
$forms = array(); |
135
|
|
|
|
136
|
|
|
if ($last == 'ч' || in_array(S::slice($word, -2), array('чь', 'сь')) || (self::isVowel($last) && in_array(S::slice($word, -2, -1), array('ч', 'к')))) { // before ч, чь, сь, ч+vowel, к+vowel |
137
|
|
|
$forms[Cases::IMENIT] = $prefix.'и'; |
138
|
|
|
} elseif ($last == 'н' || $last == 'ц') { |
139
|
|
|
$forms[Cases::IMENIT] = $prefix.'ы'; |
140
|
|
View Code Duplication |
} else { |
|
|
|
|
141
|
|
|
$forms[Cases::IMENIT] = self::chooseVowelAfterConsonant($last, $soft_last, $prefix.'я', $prefix.'а'); |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
// RODIT |
145
|
|
|
if (isset(self::$genitiveExceptions[$word])) { |
146
|
|
|
$forms[Cases::RODIT] = self::$genitiveExceptions[$word]; |
147
|
|
|
} elseif (in_array($last, array('о', 'е'))) { |
148
|
|
|
// exceptions |
149
|
|
|
if (in_array($word, self::$neuterExceptions)) { |
150
|
|
|
$forms[Cases::RODIT] = $prefix.'ей'; |
151
|
|
View Code Duplication |
} elseif (S::slice($word, -2, -1) == 'и') { |
|
|
|
|
152
|
|
|
$forms[Cases::RODIT] = $prefix.'й'; |
153
|
|
|
} else { |
154
|
|
|
$forms[Cases::RODIT] = $prefix; |
155
|
|
|
} |
156
|
|
|
} elseif (S::slice($word, -2) == 'ка') { // words ending with -ка: чашка, вилка, ложка, тарелка, копейка, батарейка |
157
|
|
|
if (S::slice($word, -3, -2) == 'л') { |
158
|
|
|
$forms[Cases::RODIT] = S::slice($word, 0, -2).'ок'; |
159
|
|
View Code Duplication |
} elseif (S::slice($word, -3, -2) == 'й') { |
|
|
|
|
160
|
|
|
$forms[Cases::RODIT] = S::slice($word, 0, -3).'ек'; |
161
|
|
|
} else { |
162
|
|
|
$forms[Cases::RODIT] = S::slice($word, 0, -2).'ек'; |
163
|
|
|
} |
164
|
|
|
} elseif (in_array($last, array('а'))) { // обида, ябеда |
165
|
|
|
$forms[Cases::RODIT] = $prefix; |
166
|
|
|
} elseif (in_array($last, array('я'))) { // молния |
167
|
|
|
$forms[Cases::RODIT] = $prefix.'й'; |
168
|
|
|
} elseif (RussianLanguage::isHissingConsonant($last) || ($soft_last && $last != 'й') || in_array(S::slice($word, -2), array('чь', 'сь'))) { |
169
|
|
|
$forms[Cases::RODIT] = $prefix.'ей'; |
170
|
|
View Code Duplication |
} elseif ($last == 'й' || S::slice($word, -2) == 'яц') { // месяц |
|
|
|
|
171
|
|
|
$forms[Cases::RODIT] = $prefix.'ев'; |
172
|
|
|
} else { // (self::isConsonant($last) && !RussianLanguage::isHissingConsonant($last)) |
173
|
|
|
$forms[Cases::RODIT] = $prefix.'ов'; |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
// DAT |
177
|
|
|
$forms[Cases::DAT] = self::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ям', $prefix.'ам'); |
178
|
|
|
|
179
|
|
|
// VINIT |
180
|
|
|
$forms[Cases::VINIT] = NounDeclension::getVinitCaseByAnimateness($forms, $animateness); |
181
|
|
|
|
182
|
|
|
// TVORIT |
183
|
|
|
// my personal rule |
184
|
|
|
if ($last == 'ь' && $declension == NounDeclension::THIRD_DECLENSION && !in_array(S::slice($word, -2), array('чь', 'сь'))) { |
185
|
|
|
$forms[Cases::TVORIT] = $prefix.'ми'; |
186
|
|
|
} else { |
187
|
|
|
$forms[Cases::TVORIT] = self::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ями', $prefix.'ами'); |
188
|
|
|
} |
189
|
|
|
|
190
|
|
|
// PREDLOJ |
191
|
|
|
$forms[Cases::PREDLOJ] = self::chooseVowelAfterConsonant($last, $soft_last && S::slice($word, -2, -1) != 'ч', $prefix.'ях', $prefix.'ах'); |
192
|
|
|
$forms[Cases::PREDLOJ] = self::choosePrepositionByFirstLetter($forms[Cases::PREDLOJ], 'об', 'о').' '.$forms[Cases::PREDLOJ]; |
193
|
|
|
return $forms; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Rules are from http://rusgram.narod.ru/1216-1231.html |
198
|
|
|
*/ |
199
|
|
|
protected static function declinateAdjective($word, $animateness) |
200
|
|
|
{ |
201
|
|
|
$prefix = S::slice($word, 0, -2); |
202
|
|
|
$vowel = self::isHissingConsonant(S::slice($prefix, -1)) ? 'и' : 'ы'; |
203
|
|
|
return array( |
204
|
|
|
Cases::IMENIT => $prefix.$vowel.'е', |
205
|
|
|
Cases::RODIT => $prefix.$vowel.'х', |
206
|
|
|
Cases::DAT => $prefix.$vowel.'м', |
207
|
|
|
Cases::VINIT => $prefix.$vowel.($animateness ? 'х' : 'е'), |
208
|
|
|
Cases::TVORIT => $prefix.$vowel.'ми', |
209
|
|
|
Cases::PREDLOJ => self::choosePrepositionByFirstLetter($prefix, 'об', 'о').' '.$prefix.$vowel.'х', |
210
|
|
|
); |
211
|
|
|
} |
212
|
|
|
} |
213
|
|
|
|
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..