Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 7837f50

Browse files
committed
[Form] Added FormUtil::singularify()
1 parent 8245bf1 commit 7837f50

2 files changed

Lines changed: 293 additions & 0 deletions

File tree

src/Symfony/Component/Form/Util/FormUtil.php

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,181 @@
1616
*/
1717
abstract class FormUtil
1818
{
19+
const PLURAL_SUFFIX = 0;
20+
21+
const PLURAL_SUFFIX_LENGTH = 1;
22+
23+
const PLURAL_SUFFIX_AFTER_VOCAL = 2;
24+
25+
const PLURAL_SUFFIX_AFTER_CONS = 3;
26+
27+
const SINGULAR_SUFFIX = 4;
28+
29+
/**
30+
* Map english plural to singular suffixes
31+
*
32+
* @var array
33+
*
34+
* @see http://english-zone.com/spelling/plurals.html
35+
* @see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
36+
*/
37+
static private $pluralMap = array(
38+
// First entry: plural suffix, reversed
39+
// Second entry: length of plural suffix
40+
// Third entry: Whether the suffix may succeed a vocal
41+
// Fourth entry: Whether the suffix may succeed a consonant
42+
// Fifth entry: singular suffix, normal
43+
44+
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
45+
array('a', 1, true, true, array('on', 'um')),
46+
47+
// nebulae (nebula)
48+
array('ea', 2, true, true, 'a'),
49+
50+
// mice (mouse), lice (louse)
51+
array('eci', 3, false, true, 'ouse'),
52+
53+
// geese (goose)
54+
array('esee', 4, false, true, 'oose'),
55+
56+
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
57+
array('i', 1, true, true, 'us'),
58+
59+
// men (man), women (woman)
60+
array('nem', 3, true, true, 'man'),
61+
62+
// children (child)
63+
array('nerdlihc', 8, true, true, 'child'),
64+
65+
// oxen (ox)
66+
array('nexo', 4, false, false, 'ox'),
67+
68+
// indices (index), appendices (appendix)
69+
array('seci', 4, false, true, array('ex', 'ix')),
70+
71+
// babies (baby)
72+
array('sei', 3, false, true, 'y'),
73+
74+
// analyses (analysis), ellipses (ellipsis), funguses (fungus),
75+
// neuroses (neurosis), theses (thesis), emphases (emphasis),
76+
// oases (oasis), crises (crisis), houses (house), bases (base),
77+
// atlases (atlas), kisses (kiss)
78+
array('ses', 3, true, true, array('s', 'se', 'sis')),
79+
80+
// lives (life), wives (wife)
81+
array('sevi', 4, false, true, 'ife'),
82+
83+
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
84+
array('sev', 3, true, true, 'f'),
85+
86+
// axes (axis)
87+
array('sexa', 4, false, false, 'axis'),
88+
89+
// indexes (index), matrixes (matrix)
90+
array('sex', 3, true, false, 'x'),
91+
92+
// quizzes (quiz)
93+
array('sezz', 4, true, false, 'z'),
94+
95+
// bureaus (bureau)
96+
array('suae', 4, false, true, 'eau'),
97+
98+
// roses (rose), garages (garage), cassettes (cassette),
99+
// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
100+
// shoes (shoe)
101+
array('se', 2, true, true, array('', 'e')),
102+
103+
// tags (tag)
104+
array('s', 1, true, true, ''),
105+
106+
// chateaux (chateau)
107+
array('xuae', 4, false, true, 'eau'),
108+
);
109+
110+
/**
111+
* Returns the singular form of a word
112+
*
113+
* If the method can't determine the form with certainty, an array of the
114+
* possible singulars is returned.
115+
*
116+
* @param string $plural A word in plural form
117+
* @return string|array The singular form or an array of possible singular
118+
* forms
119+
*/
120+
static public function singularify($plural)
121+
{
122+
$pluralRev = strrev($plural);
123+
$lowerPluralRev = strtolower($pluralRev);
124+
$pluralLength = strlen($lowerPluralRev);
125+
126+
// The outer loop $i iterates over the entries of the plural table
127+
// The inner loop $j iterates over the characters of the plural suffix
128+
// in the plural table to compare them with the characters of the actual
129+
// given plural suffix
130+
for ($i = 0, $numPlurals = count(self::$pluralMap); $i < $numPlurals; ++$i) {
131+
$suffix = self::$pluralMap[$i][self::PLURAL_SUFFIX];
132+
$suffixLength = self::$pluralMap[$i][self::PLURAL_SUFFIX_LENGTH];
133+
$j = 0;
134+
135+
// Compare characters in the plural table and of the suffix of the
136+
// given plural one by one
137+
while ($suffix[$j] === $lowerPluralRev[$j]) {
138+
// Let $j point to the next character
139+
++$j;
140+
141+
// Successfully compared the last character
142+
// Add an entry with the singular suffix to the singular array
143+
if ($j === $suffixLength) {
144+
// Is there any character preceding the suffix in the plural string?
145+
if ($j < $pluralLength) {
146+
$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
147+
148+
if (!self::$pluralMap[$i][self::PLURAL_SUFFIX_AFTER_VOCAL] && $nextIsVocal) {
149+
break;
150+
}
151+
152+
if (!self::$pluralMap[$i][self::PLURAL_SUFFIX_AFTER_CONS] && !$nextIsVocal) {
153+
break;
154+
}
155+
}
156+
157+
$newBase = substr($plural, 0, $pluralLength - $suffixLength);
158+
$newSuffix = self::$pluralMap[$i][self::SINGULAR_SUFFIX];
159+
160+
// Check whether the first character in the plural suffix
161+
// is uppercased. If yes, uppercase the first character in
162+
// the singular suffix too
163+
$firstUpper = ctype_upper($pluralRev[$j - 1]);
164+
165+
if (is_array($newSuffix)) {
166+
$singulars = array();
167+
168+
foreach ($newSuffix as $newSuffixEntry) {
169+
$singulars[] = $newBase . ($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
170+
}
171+
172+
return $singulars;
173+
}
174+
175+
return $newBase . ($firstUpper ? ucFirst($newSuffix) : $newSuffix);
176+
}
177+
178+
// Suffix is longer than word
179+
if ($j === $pluralLength) {
180+
break;
181+
}
182+
}
183+
}
184+
185+
// Convert teeth to tooth, feet to foot
186+
if (false !== ($pos = strpos($plural, 'ee'))) {
187+
return substr_replace($plural, 'oo', $pos, 2);
188+
}
189+
190+
// Assume that plural and singular is identical
191+
return $plural;
192+
}
193+
19194
/**
20195
* Returns whether the given choice is a group.
21196
*

tests/Symfony/Tests/Component/Form/Util/FormUtilTest.php

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,122 @@ public function testIsChoiceSelected($expected, $choice, $value)
7777
{
7878
$this->assertSame($expected, FormUtil::isChoiceSelected($choice, $value));
7979
}
80+
81+
public function singularifyProvider()
82+
{
83+
// see http://english-zone.com/spelling/plurals.html
84+
// see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
85+
return array(
86+
array('tags', 'tag'),
87+
array('alumni', 'alumnus'),
88+
array('funguses', array('fungus', 'funguse', 'fungusis')),
89+
array('fungi', 'fungus'),
90+
array('axes', 'axis'),
91+
array('appendices', array('appendex', 'appendix')),
92+
array('indices', array('index', 'indix')),
93+
array('indexes', 'index'),
94+
array('children', 'child'),
95+
array('men', 'man'),
96+
array('women', 'woman'),
97+
array('oxen', 'ox'),
98+
array('bacteria', array('bacterion', 'bacterium')),
99+
array('criteria', array('criterion', 'criterium')),
100+
array('feet', 'foot'),
101+
array('nebulae', 'nebula'),
102+
array('babies', 'baby'),
103+
array('hooves', 'hoof'),
104+
array('chateaux', 'chateau'),
105+
array('echoes', array('echo', 'echoe')),
106+
array('analyses', array('analys', 'analyse', 'analysis')),
107+
array('theses', array('thes', 'these', 'thesis')),
108+
array('foci', 'focus'),
109+
array('focuses', array('focus', 'focuse', 'focusis')),
110+
array('oases', array('oas', 'oase', 'oasis')),
111+
array('matrices', array('matrex', 'matrix')),
112+
array('matrixes', 'matrix'),
113+
array('bureaus', 'bureau'),
114+
array('bureaux', 'bureau'),
115+
array('beaux', 'beau'),
116+
array('data', array('daton', 'datum')),
117+
array('phenomena', array('phenomenon', 'phenomenum')),
118+
array('strata', array('straton', 'stratum')),
119+
array('geese', 'goose'),
120+
array('teeth', 'tooth'),
121+
array('antennae', 'antenna'),
122+
array('antennas', 'antenna'),
123+
array('houses', array('hous', 'house', 'housis')),
124+
array('arches', array('arch', 'arche')),
125+
array('atlases', array('atlas', 'atlase', 'atlasis')),
126+
// array('axes', 'axe'),
127+
array('batches', array('batch', 'batche')),
128+
array('bushes', array('bush', 'bushe')),
129+
array('buses', array('bus', 'buse', 'busis')),
130+
array('calves', 'calf'),
131+
array('circuses', array('circus', 'circuse', 'circusis')),
132+
array('crises', array('cris', 'crise', 'crisis')),
133+
array('dwarves', 'dwarf'),
134+
array('elves', 'elf'),
135+
array('emphases', array('emphas', 'emphase', 'emphasis')),
136+
array('faxes', 'fax'),
137+
array('halves', 'half'),
138+
array('heroes', array('hero', 'heroe')),
139+
array('hoaxes', 'hoax'),
140+
array('irises', array('iris', 'irise', 'irisis')),
141+
array('kisses', array('kiss', 'kisse', 'kissis')),
142+
array('knives', 'knife'),
143+
array('lives', 'life'),
144+
array('lice', 'louse'),
145+
array('mice', 'mouse'),
146+
array('neuroses', array('neuros', 'neurose', 'neurosis')),
147+
array('plateaux', 'plateau'),
148+
array('poppies', 'poppy'),
149+
array('quizzes', 'quiz'),
150+
array('scarves', 'scarf'),
151+
array('spies', 'spy'),
152+
array('stories', 'story'),
153+
array('syllabi', 'syllabus'),
154+
array('thieves', 'thief'),
155+
array('waltzes', array('waltz', 'waltze')),
156+
array('wharves', 'wharf'),
157+
array('wives', 'wife'),
158+
array('ions', 'ion'),
159+
array('bases', array('bas', 'base', 'basis')),
160+
array('cars', 'car'),
161+
array('cassettes', array('cassett', 'cassette')),
162+
array('lamps', 'lamp'),
163+
array('hats', 'hat'),
164+
array('cups', 'cup'),
165+
array('boxes', 'box'),
166+
array('sandwiches', array('sandwich', 'sandwiche')),
167+
array('suitcases', array('suitcas', 'suitcase', 'suitcasis')),
168+
array('roses', array('ros', 'rose', 'rosis')),
169+
array('garages', array('garag', 'garage')),
170+
array('shoes', array('sho', 'shoe')),
171+
array('days', 'day'),
172+
array('boys', 'boy'),
173+
array('roofs', 'roof'),
174+
array('cliffs', 'cliff'),
175+
array('sheriffs', 'sheriff'),
176+
array('discos', 'disco'),
177+
array('pianos', 'piano'),
178+
array('photos', 'photo'),
179+
array('trees', array('tre', 'tree')),
180+
array('bees', array('be', 'bee')),
181+
array('cheeses', array('chees', 'cheese', 'cheesis')),
182+
array('radii', 'radius'),
183+
184+
// test casing: if the first letter was uppercase, it should remain so
185+
array('Men', 'Man'),
186+
array('GrandChildren', 'GrandChild'),
187+
array('SubTrees', array('SubTre', 'SubTree')),
188+
);
189+
}
190+
191+
/**
192+
* @dataProvider singularifyProvider
193+
*/
194+
public function testSingularify($plural, $singular)
195+
{
196+
$this->assertEquals($singular, FormUtil::singularify($plural));
197+
}
80198
}

0 commit comments

Comments
 (0)