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