8000 Add Inflector component (from StringUtil of PropertyAccess) · symfony/symfony@d85bcad · GitHub
[go: up one dir, main page]

Skip to content

Commit d85bcad

Browse files
committed
Add Inflector component (from StringUtil of PropertyAccess)
1 parent 003507d commit d85bcad

File tree

9 files changed

+342
-189
lines changed

9 files changed

+342
-189
lines changed
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Inflector;
13+
14+
/**
15+
* Converts words between singular and plural forms.
16+
*
17+
* @author Bernhard Schussek <bschussek@gmail.com>
18+
*/
19+
final class Inflector
20+
{
21+
/**
22+
* Map English plural to singular suffixes.
23+
*
24+
* @var array
25+
*
26+
* @see http://english-zone.com/spelling/plurals.html
27+
* @see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
28+
*/
29+
private static $pluralMap = array(
30+
// First entry: plural suffix, reversed
31+
// Second entry: length of plural suffix
32+
// Third entry: Whether the suffix may succeed a vocal
33+
// Fourth entry: Whether the suffix may 6D4E succeed a consonant
34+
// Fifth entry: singular suffix, normal
35+
36+
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
37+
array('a', 1, true, true, array('on', 'um')),
38+
39+
// nebulae (nebula)
40+
array('ea', 2, true, true, 'a'),
41+
42+
// services (service)
43+
array('secivres', 8, true, true, 'service'),
44+
45+
// mice (mouse), lice (louse)
46+
array('eci', 3, false, true, 'ouse'),
47+
48+
// geese (goose)
49+
array('esee', 4, false, true, 'oose'),
50+
51+
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
52+
array('i', 1, true, true, 'us'),
53+
54+
// men (man), women (woman)
55+
array('nem', 3, true, true, 'man'),
56+
57+
// children (child)
58+
array('nerdlihc', 8, true, true, 'child'),
59+
60+
// oxen (ox)
61+
array('nexo', 4, false, false, 'ox'),
62+
63+
// indices (index), appendices (appendix), prices (price)
64+
array('seci', 4, false, true, array('ex', 'ix', 'ice')),
65+
66+
// selfies (selfie)
67+
array('seifles', 7, true, true, 'selfie'),
68+
69+
// movies (movie)
70+
array('seivom', 6, true, true, 'movie'),
71+
72+
// news (news)
73+
array('swen', 4, true, true, 'news'),
74+
75+
// series (series)
76+
array('seires', 6, true, true, 'series'),
77+
78+
// babies (baby)
79+
array('sei', 3, false, true, 'y'),
80+
81+
// accesses (access), addresses (address), kisses (kiss)
82+
array('sess', 4, true, false, 'ss'),
83+
84+
// analyses (analysis), ellipses (ellipsis), funguses (fungus),
85+
// neuroses (neurosis), theses (thesis), emphases (emphasis),
86+
// oases (oasis), crises (crisis), houses (house), bases (base),
87+
// atlases (atlas)
88+
array('ses', 3, true, true, array('s', 'se', 'sis')),
89+
90+
// objectives (objective), alternative (alternatives)
91+
array('sevit', 5, true, true, 'tive'),
92+
93+
// drives (drive)
94+
array('sevird', 6, false, true, 'drive'),
95+
96+
// lives (life), wives (wife)
97+
array('sevi', 4, false, true, 'ife'),
98+
99+
// moves (move)
100+
array('sevom', 5, true, true, 'move'),
101+
102+
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
103+
array('sev', 3, true, true, array('f', 've', 'ff')),
104+
105+
// axes (axis), axes (ax), axes (axe)
106+
array('sexa', 4, false, false, array('ax', 'axe', 'axis')),
107+
108+
// indexes (index), matrixes (matrix)
109+
array('sex', 3, true, false, 'x'),
110+
111+
// quizzes (quiz)
112+
array('sezz', 4, true, false, 'z'),
113+
114+
// bureaus (bureau)
115+
array('suae', 4, false, true, 'eau'),
116+
117+
// roses (rose), garages (garage), cassettes (cassette),
118+
// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
119+
// shoes (shoe)
120+
array('se', 2, true, true, array('', 'e')),
121+
122+
// tags (tag)
123+
array('s', 1, true, true, ''),
124+
125+
// chateaux (chateau)
126+
array('xuae', 4, false, true, 'eau'),
127+
);
128+
129+
/**
130+
* This class should not be instantiated.
131+
*/
132+
private function __construct()
133+
{
134+
}
135+
136+
/**
137+
* Returns the singular form of a word.
138+
*
139+
* If the method can't determine the form with certainty, an array of the
140+
* possible singulars is returned.
141+
*
142+
* @param string $plural A word in plural form
143+
*
144+
* @return string|array The singular form or an array of possible singular
145+
* forms
146+
*/
147+
public static function singularize($plural)
148+
{
149+
$pluralRev = strrev($plural);
150+
$lowerPluralRev = strtolower($pluralRev);
151+
$pluralLength = strlen($lowerPluralRev);
152+
153+
// The outer loop iterates over the entries of the plural table
154+
// The inner loop $j iterates over the characters of the plural suffix
155+
// in the plural table to compare them with the characters of the actual
156+
// given plural suffix
157+
foreach (self::$pluralMap as $map) {
158+
$suffix = $map[0];
159+
$suffixLength = $map[1];
160+
$j = 0;
161+
162+
// Compare characters in the plural table and of the suffix of the
163+
// given plural one by one
164+
while ($suffix[$j] === $lowerPluralRev[$j]) {
165+
// Let $j point to the next character
166+
++$j;
167+
168+
// Successfully compared the last character
169+
// Add an entry with the singular suffix to the singular array
170+
if ($j === $suffixLength) {
171+
// Is there any character preceding the suffix in the plural string?
172+
if ($j < $pluralLength) {
173+
$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
174+
175+
if (!$map[2] && $nextIsVocal) {
176+
// suffix may not succeed a vocal but next char is one
177+
break;
178+
}
179+
180+
if (!$map[3] && !$nextIsVocal) {
181+
// suffix may not succeed a consonant but next char is one
182+
break;
183+
}
184+
}
185+
186+
$newBase = substr($plural, 0, $pluralLength - $suffixLength);
187+
$newSuffix = $map[4];
188+
189+
// Check whether the first character in the plural suffix
190+
// is uppercased. If yes, uppercase the first character in
191+
// the singular suffix too
192+
$firstUpper = ctype_upper($pluralRev[$j - 1]);
193+
194+
if (is_array($newSuffix)) {
195+
$singulars = array();
196+
197+
foreach ($newSuffix as $newSuffixEntry) {
198+
$singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
199+
}
200+
201+
return $singulars;
202+
}
203+
204+
return $newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix);
205+
}
206+
207+
// Suffix is longer than word
208+
if ($j === $pluralLength) {
209+
break;
210+
}
211+
}
212+
}
213+
214+
// Convert teeth to tooth, feet to foot
215+
if (false !== ($pos = strpos($plural, 'ee')) && strlen($plural) > 3 && 'feedback' !== $plural) {
216+
return substr_replace($plural, 'oo', $pos, 2);
217+
}
218+
219+
// Assume that plural and singular is identical
220+
return $plural;
221+
}
222+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2015-2016 Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Inflector Component
2+
======================
3+
4+
Inflector converts words between their singular and plural forms (English only).
5+
6+
Resources
7+
---------
8+
9+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
10+
* [Report issues](https://github.com/symfony/symfony/issues) and
11+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
12+
in the [main Symfony repository](https://github.com/symfony/symfony)

src/Symfony/Component/PropertyAccess/Tests/StringUtilTest.php renamed to src/Symfony/Component/Inflector/Tests/InflectorTest.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
* file that was distributed with this source code.
1010
*/
1111

12-
namespace Symfony\Component\PropertyAccess\Tests;
12+
namespace Symfony\Component\Inflector\Tests;
1313

14-
use Symfony\Component\PropertyAccess\StringUtil;
14+
use Symfony\Component\Inflector\Inflector;
1515

16-
class StringUtilTest extends \PHPUnit_Framework_TestCase
16+
class InflectorTest extends \PHPUnit_Framework_TestCase
1717
{
18-
public function singularifyProvider()
18+
public function singularizeProvider()
1919
{
2020
// see http://english-zone.com/spelling/plurals.html
2121
// see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
@@ -152,11 +152,11 @@ public function singularifyProvider()
152152
}
153153

154154
/**
155-
* @dataProvider singularifyProvider
155+
* @dataProvider singularizeProvider
156156
*/
157-
public function testSingularify($plural, $singular)
157+
public function testSingularize($plural, $singular)
158158
{
159-
$single = StringUtil::singularify($plural);
159+
$single = Inflector::singularize($plural);
160160
if (is_string($singular) && is_array($single)) {
161161
$this->fail("--- Expected\n`string`: ".$singular."\n+++ Actual\n`array`: ".implode(', ', $single));
162162
} elseif (is_array($singular) && is_string($single)) {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "symfony/inflector",
3+
"type": "library",
4+
"description": "Symfony Inflector Component",
5+
"keywords": [
6+
"string",
7+
"inflection",
8+
"singularize",
9+
"pluralize",
10+
"words",
11+
"symfony"
12+
],
13+
"homepage": "https://symfony.com",
14+
"license": "MIT",
15+
"authors": [
16+
{
17+
"name": "Bernhard Schussek",
18+
"email": "bschussek@gmail.com"
19+
},
20+
{
21+
"name": "Symfony Community",
22+
"homepage": "https://symfony.com/contributors"
23+
}
24+
],
25+
"require": {
26+
"php": ">=5.5.9"
27+
},
28+
"autoload": {
29+
"psr-4": { "Symfony\\Component\\Inflector\\": "" },
30+
"exclude-from-classmap": [
31+
"/Tests/"
32+
]
33+
},
34+
"minimum-stability": "dev",
35+
"extra": {
36+
"branch-alias": {
37+
"dev-master": "3.1-dev"
38+
}
39+
}
40+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
3+
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
5+
backupGlobals="false"
6+
colors="true"
7+
bootstrap="vendor/autoload.php"
8+
>
9+
<php>
10+
<ini name="error_reporting" value="-1" />
11+
</php>
12+
13+
<testsuites>
14+
<testsuite name="Symfony Inflector Component Test Suite">
15+
<directory>./Tests/</directory>
16+
</testsuite>
17+
</testsuites>
18+
19+
<filter>
20+
<whitelist>
21+
<directory>./</directory>
22+
<exclude>
23+
<directory>./Resources</directory>
24+
<directory>./Tests</directory>
25+
<directory>./vendor</directory>
26+
</exclude>
27+
</whitelist>
28+
</filter>
29+
</phpunit>

src/Symfony/Component/PropertyAccess/PropertyAccessor.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\PropertyAccess;
1313

14+
use Symfony\Component\Inflector\Inflector;
1415
use Symfony\Component\PropertyAccess\Exception\AccessException;
1516
use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException;
1617
use Symfony\Component\PropertyAccess\Exception\NoSuchIndexException;
@@ -681,7 +682,7 @@ private function getWriteAccessInfo($object, $property, $value)
681682
$reflClass = new \ReflectionClass($object);
682683
$access[self::ACCESS_HAS_PROPERTY] = $reflClass->hasProperty($property);
683684
$camelized = $this->camelize($property);
684-
$singulars = (array) StringUtil::singularify($camelized);
685+
$singulars = (array) Inflector::singularize($camelized);
685686

686687
if (is_array($value) || $value instanceof \Traversable) {
687688
$methods = $this->findAdderAndRemover($reflClass, $singulars);
@@ -765,7 +766,7 @@ private function isPropertyWritable($object, $property)
765766
return true;
766767
}
767768

768-
$singulars = (array) StringUtil::singularify($camelized);
769+
$singulars = (array) Inflector::singularize($camelized);
769770

770771
// Any of the two methods is required, but not yet known
771772
if (null !== $this->findAdderAndRemover($reflClass, $singulars)) {

0 commit comments

Comments
 (0)
0