@@ -139,6 +139,179 @@ final class Inflector
139
139
['elpoep ' , 6 , true , true , 'person ' ],
140
140
];
141
141
142
+ /**
143
+ * Map English singular to plural suffixes.
144
+ *
145
+ * @var array
146
+ *
147
+ * @see http://english-zone.com/spelling/plurals.html
148
+ */
149
+ private static $ singularMap = array (
150
+ // First entry: singular suffix, reversed
151
+ // Second entry: length of singular suffix
152
+ // Third entry: Whether the suffix may succeed a vocal
153
+ // Fourth entry: Whether the suffix may succeed a consonant
154
+ // Fifth entry: plural suffix, normal
155
+
156
+ // criterion (criteria)
157
+ array ('airetirc ' , 8 , false , false , 'criterion ' ),
158
+
159
+ // nebulae (nebula)
160
+ array ('aluben ' , 6 , false , false , 'nebulae ' ),
161
+
162
+ // children (child)
163
+ array ('dlihc ' , 5 , true , true , 'children ' ),
164
+
165
+ // prices (price)
166
+ array ('eci ' , 3 , false , true , 'ices ' ),
167
+
168
+ // services (service)
169
+ array ('ecivres ' , 7 , true , true , 'services ' ),
170
+
171
+ // lives (life), wives (wife)
172
+ array ('efi ' , 3 , false , true , 'ives ' ),
173
+
174
+ // selfies (selfie)
175
+ array ('eifles ' , 6 , true , true , 'selfies ' ),
176
+
177
+ // movies (movie)
178
+ array ('eivom ' , 5 , true , true , 'movies ' ),
179
+
180
+ // lice (louse)
181
+ array ('esuol ' , 5 , false , true , 'lice ' ),
182
+
183
+ // mice (mouse)
184
+ array ('esuom ' , 5 , false , true , 'mice ' ),
185
+
186
+ // geese (goose)
187
+ array ('esoo ' , 4 , false , true , 'eese ' ),
188
+
189
+ // houses (house), bases (base)
190
+ array ('es ' , 2 , true , true , 'ses ' ),
191
+
192
+ // geese (goose)
193
+ array ('esoog ' , 5 , true , true , 'geese ' ),
194
+
195
+ // caves (cave)
196
+ array ('ev ' , 2 , true , true , 'ves ' ),
197
+
198
+ // drives (drive)
199
+ array ('evird ' , 5 , false , true , 'drives ' ),
200
+
201
+ // objectives (objective), alternative (alternatives)
202
+ array ('evit ' , 4 , true , true , 'tives ' ),
203
+
204
+ // moves (move)
205
+ array ('evom ' , 4 , true , true , 'moves ' ),
206
+
207
+ // staves (staff)
208
+ array ('ffats ' , 5 , true , true , 'staves ' ),
209
+
210
+ // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
211
+ array ('ff ' , 2 , true , true , 'ffs ' ),
212
+
213
+ // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
214
+ array ('f ' , 1 , true , true , array ('fs ' , 'ves ' )),
215
+
216
+ // arches (arch)
217
+ array ('hc ' , 2 , true , true , 'ches ' ),
218
+
219
+ // bushes (bush)
220
+ array ('hs ' , 2 , true , true , 'shes ' ),
221
+
222
+ // teeth (tooth)
223
+ array ('htoot ' , 5 , true , true , 'teeth ' ),
224
+
225
+ // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
226
+ array ('mu ' , 2 , true , true , 'a ' ),
227
+
228
+ // echoes (echo)
229
+ array ('ohce ' , 4 , true , true , 'echoes ' ),
230
+
231
+ // men (man), women (woman)
232
+ array ('nam ' , 3 , true , true , 'men ' ),
233
+
234
+ // people (person)
235
+ array ('nosrep ' , 6 , true , true , array ('persons ' , 'people ' )),
236
+
237
+ // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
238
+ array ('noi ' , 3 , true , true , 'ions ' ),
239
+
240
+ // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
241
+ array ('no ' , 2 , true , true , 'a ' ),
242
+
243
+ // atlases (atlas)
244
+ array ('salta ' , 5 , true , true , 'atlases ' ),
245
+
246
+ // irises (iris)
247
+ array ('siri ' , 4 , true , true , 'irises ' ),
248
+
249
+ // analyses (analysis), ellipses (ellipsis), neuroses (neurosis)
250
+ // theses (thesis), emphases (emphasis), oases (oasis),
251
+ // crises (crisis)
252
+ array ('sis ' , 3 , true , true , 'ses ' ),
253
+
254
+ // accesses (access), addresses (address), kisses (kiss)
255
+ array ('ss ' , 2 , true , false , 'sses ' ),
256
+
257
+ // syllabi (syllabus)
258
+ array ('suballys ' , 8 , true , true , 'syllabi ' ),
259
+
260
+ // buses (bus)
261
+ array ('sub ' , 3 , true , true , 'buses ' ),
262
+
263
+ // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
264
+ array ('su ' , 2 , true , true , 'i ' ),
265
+
266
+ // news (news)
267
+ array ('swen ' , 4 , true , true , 'news ' ),
268
+
269
+ // feet (foot)
270
+ array ('toof ' , 4 , true , true , 'feet ' ),
271
+
272
+ // chateaux (chateau), bureaus (bureau)
273
+ array ('uae ' , 3 , false , true , array ('eaus ' , 'eaux ' )),
274
+
275
+ // oxen (ox)
276
+ array ('xo ' , 2 , false , false , 'oxen ' ),
277
+
278
+ // hoaxes (hoax)
279
+ array ('xaoh ' , 4 , true , false , 'hoaxes ' ),
280
+
281
+ // indices (index)
282
+ array ('xedni ' , 5 , false , true , array ('indicies ' , 'indexes ' )),
283
+
284
+ // indexes (index), matrixes (matrix)
285
+ array ('x ' , 1 , true , false , array ('cies ' , 'xes ' )),
286
+
287
+ // appendices (appendix)
288
+ array ('xi ' , 2 , false , true , 'ices ' ),
289
+
290
+ // babies (baby)
291
+ array ('y ' , 1 , false , true , 'ies ' ),
292
+
293
+ // quizzes (quiz)
294
+ array ('ziuq ' , 4 , true , false , 'quizzes ' ),
295
+
296
+ // waltzes (waltz)
297
+ array ('z ' , 1 , true , false , 'zes ' ),
298
+ );
299
+
300
+ /**
301
+ * A list of words which should not be inflected
302
+ *
303
+ * @var array
304
+ */
305
+ private static $ uninflected = array (
306
+ 'data ' ,
307
+ 'deer ' ,
308
+ 'feedback ' ,
309
+ 'fish ' ,
310
+ 'moose ' ,
311
+ 'series ' ,
312
+ 'sheep ' ,
313
+ );
314
+
142
315
/**
143
316
* This class should not be instantiated.
144
317
*/
@@ -165,6 +338,11 @@ public static function singularize(string $plural)
165
338
$ lowerPluralRev = strtolower ($ pluralRev );
166
339
$ pluralLength = \strlen ($ lowerPluralRev );
167
340
341
+ // Check if the word is one which is not inflected, return early if so
342
+ if (in_array (strtolower ($ plural ), self ::$ uninflected , true )) {
343
+ return $ plural ;
344
+ }
345
+
168
346
// The outer loop iterates over the entries of the plural table
169
347
// The inner loop $j iterates over the characters of the plural suffix
170
348
// in the plural table to compare them with the characters of the actual
@@ -229,4 +407,94 @@ public static function singularize(string $plural)
229
407
// Assume that plural and singular is identical
230
408
return $ plural ;
231
409
}
410
+
411
+ /**
412
+ * Returns the plural form of a word.
413
+ *
414
+ * If the method can't determine the form with certainty, an array of the
415
+ * possible plurals is returned.
416
+ *
417
+ * @param string $singular A word in plural form
418
+ *
419
+ * @return string|array The plural form or an array of possible plural
420
+ * forms
421
+ *
422
+ * @internal
423
+ */
424
+ public static function pluralize (string $ singular )
425
+ {
426
+ $ singularRev = strrev ($ singular );
427
+ $ lowerSingularRev = strtolower ($ singularRev );
428
+ $ singularLength = strlen ($ lowerSingularRev );
429
+
430
+ // Check if the word is one which is not inflected, return early if so
431
+ if (in_array (strtolower ($ singular ), self ::$ uninflected , true )) {
432
+ return $ singular ;
433
+ }
434
+
435
+ // The outer loop iterates over the entries of the singular table
436
+ // The inner loop $j iterates over the characters of the singular suffix
437
+ // in the singular table to compare them with the characters of the actual
438
+ // given singular suffix
439
+ foreach (self ::$ singularMap as $ map ) {
440
+ $ suffix = $ map [0 ];
441
+ $ suffixLength = $ map [1 ];
442
+ $ j = 0 ;
443
+
444
+ // Compare characters in the singular table and of the suffix of the
445
+ // given plural one by one
446
+
447
+ while ($ suffix [$ j ] === $ lowerSingularRev [$ j ]) {
448
+ // Let $j point to the next character
449
+ ++$ j ;
450
+
451
+ // Successfully compared the last character
452
+ // Add an entry with the plural suffix to the plural array
453
+ if ($ j === $ suffixLength ) {
454
+ // Is there any character preceding the suffix in the plural string?
455
+ if ($ j < $ singularLength ) {
456
+ $ nextIsVocal = false !== strpos ('aeiou ' , $ lowerSingularRev [$ j ]);
457
+
458
+ if (!$ map [2 ] && $ nextIsVocal ) {
459
+ // suffix may not succeed a vocal but next char is one
460
+ break ;
461
+ }
462
+
463
+ if (!$ map [3 ] && !$ nextIsVocal ) {
464
+ // suffix may not succeed a consonant but next char is one
465
+ break ;
466
+ }
467
+ }
468
+
469
+ $ newBase = substr ($ singular , 0 , $ singularLength - $ suffixLength );
470
+ $ newSuffix = $ map [4 ];
471
+
472
+ // Check whether the first character in the singular suffix
473
+ // is uppercased. If yes, uppercase the first character in
474
+ // the singular suffix too
475
+ $ firstUpper = ctype_upper ($ singularRev [$ j - 1 ]);
476
+
477
+ if (is_array ($ newSuffix )) {
478
+ $ plurals = array ();
479
+
480
+ foreach ($ newSuffix as $ newSuffixEntry ) {
481
+ $ plurals [] = $ newBase .($ firstUpper ? ucfirst ($ newSuffixEntry ) : $ newSuffixEntry );
482
+ }
483
+
484
+ return $ plurals ;
485
+ }
486
+
487
+ return $ newBase .($ firstUpper ? ucfirst ($ newSuffix ) : $ newSuffix );
488
+ }
489
+
490
+ // Suffix is longer than word
491
+ if ($ j === $ singularLength ) {
492
+ break ;
493
+ }
494
+ }
495
+ }
496
+
497
+ // Assume that plural is singular with a trailing `s`
498
+ return $ singular .'s ' ;
499
+ }
232
500
}
0 commit comments