feature #35092 [Inflector][String] Move Inflector in String (fancyweb)

This PR was merged into the 5.1-dev branch.

Discussion
----------

[Inflector][String] Move Inflector in String

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | yes
| Tickets       | https://github.com/orgs/symfony/projects/1#card-30499514
| License       | MIT
| Doc PR        | -

Needs https://github.com/symfony/symfony/pull/35091.

Should we have a standalone inflector (like the Slugger) or 2 new methods (pluralize and singularize) on the AbstractString class? I implemented both but since we only handle English I finally preferred the first one.

TODO (after the "move" is OK):
- [x] Deprecate the Inflector component
- [x] Use the String inflector in Symfony's code

Commits
-------

9c6a5c0093 [String] Move Inflector in String
This commit is contained in:
Fabien Potencier 2020-05-05 08:52:01 +02:00
commit 3e737ec28f
12 changed files with 875 additions and 448 deletions

View File

@ -71,6 +71,11 @@ HttpKernel
not returning an array is deprecated
* Deprecated support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
Inflector
---------
* The component has been deprecated, use `EnglishInflector` from the String component instead.
Mailer
------

View File

@ -66,6 +66,10 @@ HttpKernel
* Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+
* Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
Inflector
---------
* The component has been removed, use `EnglishInflector` from the String component instead.
Mailer
------

View File

@ -0,0 +1,7 @@
CHANGELOG
=========
5.1.0
-----
* The component has been deprecated, use `EnglishInflector` from the String component instead.

View File

@ -11,315 +11,20 @@
namespace Symfony\Component\Inflector;
use Symfony\Component\String\Inflector\EnglishInflector;
trigger_deprecation('symfony/inflector', '5.1', sprintf('The "%s" class is deprecated, use "%s" instead.', Inflector::class, EnglishInflector::class));
/**
* Converts words between singular and plural forms.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated since Symfony 5.1, use Symfony\Component\String\Inflector\EnglishInflector instead.
*/
final class Inflector
{
/**
* Map English plural to singular suffixes.
*
* @see http://english-zone.com/spelling/plurals.html
*/
private static $pluralMap = [
// First entry: plural suffix, reversed
// Second entry: length of plural suffix
// Third entry: Whether the suffix may succeed a vocal
// Fourth entry: Whether the suffix may succeed a consonant
// Fifth entry: singular suffix, normal
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['a', 1, true, true, ['on', 'um']],
// nebulae (nebula)
['ea', 2, true, true, 'a'],
// services (service)
['secivres', 8, true, true, 'service'],
// mice (mouse), lice (louse)
['eci', 3, false, true, 'ouse'],
// geese (goose)
['esee', 4, false, true, 'oose'],
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
['i', 1, true, true, 'us'],
// men (man), women (woman)
['nem', 3, true, true, 'man'],
// children (child)
['nerdlihc', 8, true, true, 'child'],
// oxen (ox)
['nexo', 4, false, false, 'ox'],
// indices (index), appendices (appendix), prices (price)
['seci', 4, false, true, ['ex', 'ix', 'ice']],
// selfies (selfie)
['seifles', 7, true, true, 'selfie'],
// movies (movie)
['seivom', 6, true, true, 'movie'],
// feet (foot)
['teef', 4, true, true, 'foot'],
// geese (goose)
['eseeg', 5, true, true, 'goose'],
// teeth (tooth)
['hteet', 5, true, true, 'tooth'],
// news (news)
['swen', 4, true, true, 'news'],
// series (series)
['seires', 6, true, true, 'series'],
// babies (baby)
['sei', 3, false, true, 'y'],
// accesses (access), addresses (address), kisses (kiss)
['sess', 4, true, false, 'ss'],
// analyses (analysis), ellipses (ellipsis), fungi (fungus),
// neuroses (neurosis), theses (thesis), emphases (emphasis),
// oases (oasis), crises (crisis), houses (house), bases (base),
// atlases (atlas)
['ses', 3, true, true, ['s', 'se', 'sis']],
// objectives (objective), alternative (alternatives)
['sevit', 5, true, true, 'tive'],
// drives (drive)
['sevird', 6, false, true, 'drive'],
// lives (life), wives (wife)
['sevi', 4, false, true, 'ife'],
// moves (move)
['sevom', 5, true, true, 'move'],
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
['sev', 3, true, true, ['f', 've', 'ff']],
// axes (axis), axes (ax), axes (axe)
['sexa', 4, false, false, ['ax', 'axe', 'axis']],
// indexes (index), matrixes (matrix)
['sex', 3, true, false, 'x'],
// quizzes (quiz)
['sezz', 4, true, false, 'z'],
// bureaus (bureau)
['suae', 4, false, true, 'eau'],
// fees (fee), trees (tree), employees (employee)
['see', 3, true, true, 'ee'],
// roses (rose), garages (garage), cassettes (cassette),
// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
// shoes (shoe)
['se', 2, true, true, ['', 'e']],
// tags (tag)
['s', 1, true, true, ''],
// chateaux (chateau)
['xuae', 4, false, true, 'eau'],
// people (person)
['elpoep', 6, true, true, 'person'],
];
/**
* Map English singular to plural suffixes.
*
* @see http://english-zone.com/spelling/plurals.html
*/
private static $singularMap = [
// First entry: singular suffix, reversed
// Second entry: length of singular suffix
// Third entry: Whether the suffix may succeed a vocal
// Fourth entry: Whether the suffix may succeed a consonant
// Fifth entry: plural suffix, normal
// criterion (criteria)
['airetirc', 8, false, false, 'criterion'],
// nebulae (nebula)
['aluben', 6, false, false, 'nebulae'],
// children (child)
['dlihc', 5, true, true, 'children'],
// prices (price)
['eci', 3, false, true, 'ices'],
// services (service)
['ecivres', 7, true, true, 'services'],
// lives (life), wives (wife)
['efi', 3, false, true, 'ives'],
// selfies (selfie)
['eifles', 6, true, true, 'selfies'],
// movies (movie)
['eivom', 5, true, true, 'movies'],
// lice (louse)
['esuol', 5, false, true, 'lice'],
// mice (mouse)
['esuom', 5, false, true, 'mice'],
// geese (goose)
['esoo', 4, false, true, 'eese'],
// houses (house), bases (base)
['es', 2, true, true, 'ses'],
// geese (goose)
['esoog', 5, true, true, 'geese'],
// caves (cave)
['ev', 2, true, true, 'ves'],
// drives (drive)
['evird', 5, false, true, 'drives'],
// objectives (objective), alternative (alternatives)
['evit', 4, true, true, 'tives'],
// moves (move)
['evom', 4, true, true, 'moves'],
// staves (staff)
['ffats', 5, true, true, 'staves'],
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
['ff', 2, true, true, 'ffs'],
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
['f', 1, true, true, ['fs', 'ves']],
// arches (arch)
['hc', 2, true, true, 'ches'],
// bushes (bush)
['hs', 2, true, true, 'shes'],
// teeth (tooth)
['htoot', 5, true, true, 'teeth'],
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['mu', 2, true, true, 'a'],
// men (man), women (woman)
['nam', 3, true, true, 'men'],
// people (person)
['nosrep', 6, true, true, ['persons', 'people']],
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['noi', 3, true, true, 'ions'],
// seasons (season), treasons (treason), poisons (poison), lessons (lesson)
['nos', 3, true, true, 'sons'],
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['no', 2, true, true, 'a'],
// echoes (echo)
['ohce', 4, true, true, 'echoes'],
// heroes (hero)
['oreh', 4, true, true, 'heroes'],
// atlases (atlas)
['salta', 5, true, true, 'atlases'],
// irises (iris)
['siri', 4, true, true, 'irises'],
// analyses (analysis), ellipses (ellipsis), neuroses (neurosis)
// theses (thesis), emphases (emphasis), oases (oasis),
// crises (crisis)
['sis', 3, true, true, 'ses'],
// accesses (access), addresses (address), kisses (kiss)
['ss', 2, true, false, 'sses'],
// syllabi (syllabus)
['suballys', 8, true, true, 'syllabi'],
// buses (bus)
['sub', 3, true, true, 'buses'],
// circuses (circus)
['suc', 3, true, true, 'cuses'],
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
['su', 2, true, true, 'i'],
// news (news)
['swen', 4, true, true, 'news'],
// feet (foot)
['toof', 4, true, true, 'feet'],
// chateaux (chateau), bureaus (bureau)
['uae', 3, false, true, ['eaus', 'eaux']],
// oxen (ox)
['xo', 2, false, false, 'oxen'],
// hoaxes (hoax)
['xaoh', 4, true, false, 'hoaxes'],
// indices (index)
['xedni', 5, false, true, ['indicies', 'indexes']],
// boxes (box)
['xo', 2, false, true, 'oxes'],
// indexes (index), matrixes (matrix)
['x', 1, true, false, ['cies', 'xes']],
// appendices (appendix)
['xi', 2, false, true, 'ices'],
// babies (baby)
['y', 1, false, true, 'ies'],
// quizzes (quiz)
['ziuq', 4, true, false, 'quizzes'],
// waltzes (waltz)
['z', 1, true, true, 'zes'],
];
/**
* A list of words which should not be inflected, reversed.
*/
private static $uninflected = [
'atad',
'reed',
'kcabdeef',
'hsif',
'ofni',
'esoom',
'seires',
'peehs',
'seiceps',
];
private static $englishInflector;
/**
* This class should not be instantiated.
@ -340,78 +45,11 @@ final class Inflector
*/
public static function singularize(string $plural)
{
$pluralRev = strrev($plural);
$lowerPluralRev = strtolower($pluralRev);
$pluralLength = \strlen($lowerPluralRev);
// Check if the word is one which is not inflected, return early if so
if (\in_array($lowerPluralRev, self::$uninflected, true)) {
return $plural;
if (1 === \count($singulars = self::getEnglishInflector()->singularize($plural))) {
return $singulars[0];
}
// The outer loop iterates over the entries of the plural table
// The inner loop $j iterates over the characters of the plural suffix
// in the plural table to compare them with the characters of the actual
// given plural suffix
foreach (self::$pluralMap as $map) {
$suffix = $map[0];
$suffixLength = $map[1];
$j = 0;
// Compare characters in the plural table and of the suffix of the
// given plural one by one
while ($suffix[$j] === $lowerPluralRev[$j]) {
// Let $j point to the next character
++$j;
// Successfully compared the last character
// Add an entry with the singular suffix to the singular array
if ($j === $suffixLength) {
// Is there any character preceding the suffix in the plural string?
if ($j < $pluralLength) {
$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
if (!$map[2] && $nextIsVocal) {
// suffix may not succeed a vocal but next char is one
break;
}
if (!$map[3] && !$nextIsVocal) {
// suffix may not succeed a consonant but next char is one
break;
}
}
$newBase = substr($plural, 0, $pluralLength - $suffixLength);
$newSuffix = $map[4];
// Check whether the first character in the plural suffix
// is uppercased. If yes, uppercase the first character in
// the singular suffix too
$firstUpper = ctype_upper($pluralRev[$j - 1]);
if (\is_array($newSuffix)) {
$singulars = [];
foreach ($newSuffix as $newSuffixEntry) {
$singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
}
return $singulars;
}
return $newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix);
}
// Suffix is longer than word
if ($j === $pluralLength) {
break;
}
}
}
// Assume that plural and singular is identical
return $plural;
return $singulars;
}
/**
@ -426,78 +64,19 @@ final class Inflector
*/
public static function pluralize(string $singular)
{
$singularRev = strrev($singular);
$lowerSingularRev = strtolower($singularRev);
$singularLength = \strlen($lowerSingularRev);
// Check if the word is one which is not inflected, return early if so
if (\in_array($lowerSingularRev, self::$uninflected, true)) {
return $singular;
if (1 === \count($plurals = self::getEnglishInflector()->pluralize($singular))) {
return $plurals[0];
}
// The outer loop iterates over the entries of the singular table
// The inner loop $j iterates over the characters of the singular suffix
// in the singular table to compare them with the characters of the actual
// given singular suffix
foreach (self::$singularMap as $map) {
$suffix = $map[0];
$suffixLength = $map[1];
$j = 0;
return $plurals;
}
// Compare characters in the singular table and of the suffix of the
// given plural one by one
while ($suffix[$j] === $lowerSingularRev[$j]) {
// Let $j point to the next character
++$j;
// Successfully compared the last character
// Add an entry with the plural suffix to the plural array
if ($j === $suffixLength) {
// Is there any character preceding the suffix in the plural string?
if ($j < $singularLength) {
$nextIsVocal = false !== strpos('aeiou', $lowerSingularRev[$j]);
if (!$map[2] && $nextIsVocal) {
// suffix may not succeed a vocal but next char is one
break;
}
if (!$map[3] && !$nextIsVocal) {
// suffix may not succeed a consonant but next char is one
break;
}
}
$newBase = substr($singular, 0, $singularLength - $suffixLength);
$newSuffix = $map[4];
// Check whether the first character in the singular suffix
// is uppercased. If yes, uppercase the first character in
// the singular suffix too
$firstUpper = ctype_upper($singularRev[$j - 1]);
if (\is_array($newSuffix)) {
$plurals = [];
foreach ($newSuffix as $newSuffixEntry) {
$plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
}
return $plurals;
}
return $newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix);
}
// Suffix is longer than word
if ($j === $singularLength) {
break;
}
}
private static function getEnglishInflector(): EnglishInflector
{
if (!self::$englishInflector) {
self::$englishInflector = new EnglishInflector();
}
// Assume that plural is singular with a trailing `s`
return $singular.'s';
return self::$englishInflector;
}
}

View File

@ -1,6 +1,11 @@
Inflector Component
===================
**CAUTION**: this component is deprecated since Symfony 5.1. Instead, use the
[String component EnglishInflector](https://github.com/symfony/symfony/tree/master/src/Symfony/Component/String/Inflector/EnglishInflector.php).
-----
Inflector converts words between their singular and plural forms (English only).
Resources

View File

@ -14,6 +14,9 @@ namespace Symfony\Component\Inflector\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Inflector\Inflector;
/**
* @group legacy
*/
class InflectorTest extends TestCase
{
public function singularizeProvider()

View File

@ -24,7 +24,9 @@
],
"require": {
"php": "^7.2.5",
"symfony/polyfill-ctype": "~1.8"
"symfony/deprecation-contracts": "^2.1",
"symfony/polyfill-ctype": "~1.8",
"symfony/string": "^5.1"
},
"autoload": {
"psr-4": { "Symfony\\Component\\Inflector\\": "" },

View File

@ -11,7 +11,6 @@
namespace Symfony\Component\PropertyInfo\Extractor;
use Symfony\Component\Inflector\Inflector;
use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyListExtractorInterface;
@ -21,6 +20,8 @@ use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\String\Inflector\EnglishInflector;
use Symfony\Component\String\Inflector\InflectorInterface;
/**
* Extracts data using the reflection API.
@ -62,6 +63,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
private $enableConstructorExtraction;
private $methodReflectionFlags;
private $propertyReflectionFlags;
private $inflector;
private $arrayMutatorPrefixesFirst;
private $arrayMutatorPrefixesLast;
@ -71,7 +73,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
* @param string[]|null $accessorPrefixes
* @param string[]|null $arrayMutatorPrefixes
*/
public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC)
public function __construct(array $mutatorPrefixes = null, array $accessorPrefixes = null, array $arrayMutatorPrefixes = null, bool $enableConstructorExtraction = true, int $accessFlags = self::ALLOW_PUBLIC, InflectorInterface $inflector = null)
{
$this->mutatorPrefixes = null !== $mutatorPrefixes ? $mutatorPrefixes : self::$defaultMutatorPrefixes;
$this->accessorPrefixes = null !== $accessorPrefixes ? $accessorPrefixes : self::$defaultAccessorPrefixes;
@ -79,6 +81,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
$this->enableConstructorExtraction = $enableConstructorExtraction;
$this->methodReflectionFlags = $this->getMethodsFlags($accessFlags);
$this->propertyReflectionFlags = $this->getPropertyFlags($accessFlags);
$this->inflector = $inflector ?? new EnglishInflector();
$this->arrayMutatorPrefixesFirst = array_merge($this->arrayMutatorPrefixes, array_diff($this->mutatorPrefixes, $this->arrayMutatorPrefixes));
$this->arrayMutatorPrefixesLast = array_reverse($this->arrayMutatorPrefixesFirst);
@ -284,7 +287,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
$camelized = $this->camelize($property);
$constructor = $reflClass->getConstructor();
$singulars = (array) Inflector::singularize($camelized);
$singulars = $this->inflector->singularize($camelized);
$errors = [];
if (null !== $constructor && $allowConstruct) {
@ -552,7 +555,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
private function getMutatorMethod(string $class, string $property): ?array
{
$ucProperty = ucfirst($property);
$ucSingulars = (array) Inflector::singularize($ucProperty);
$ucSingulars = $this->inflector->singularize($ucProperty);
$mutatorPrefixes = \in_array($ucProperty, $ucSingulars, true) ? $this->arrayMutatorPrefixesLast : $this->arrayMutatorPrefixesFirst;
@ -592,7 +595,7 @@ class ReflectionExtractor implements PropertyListExtractorInterface, PropertyTyp
}
foreach ($reflectionProperties as $reflectionProperty) {
foreach ((array) Inflector::singularize($reflectionProperty->name) as $name) {
foreach ($this->inflector->singularize($reflectionProperty->name) as $name) {
if (strtolower($name) === strtolower($matches[2])) {
return $reflectionProperty->name;
}

View File

@ -24,8 +24,8 @@
],
"require": {
"php": "^7.2.5",
"symfony/inflector": "^4.4|^5.0",
"symfony/polyfill-php80": "^1.15"
"symfony/polyfill-php80": "^1.15",
"symfony/string": "^5.1"
},
"require-dev": {
"symfony/serializer": "^4.4|^5.0",

View File

@ -0,0 +1,477 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\String\Inflector;
final class EnglishInflector implements InflectorInterface
{
/**
* Map English plural to singular suffixes.
*
* @see http://english-zone.com/spelling/plurals.html
*/
private static $pluralMap = [
// First entry: plural suffix, reversed
// Second entry: length of plural suffix
// Third entry: Whether the suffix may succeed a vocal
// Fourth entry: Whether the suffix may succeed a consonant
// Fifth entry: singular suffix, normal
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['a', 1, true, true, ['on', 'um']],
// nebulae (nebula)
['ea', 2, true, true, 'a'],
// services (service)
['secivres', 8, true, true, 'service'],
// mice (mouse), lice (louse)
['eci', 3, false, true, 'ouse'],
// geese (goose)
['esee', 4, false, true, 'oose'],
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
['i', 1, true, true, 'us'],
// men (man), women (woman)
['nem', 3, true, true, 'man'],
// children (child)
['nerdlihc', 8, true, true, 'child'],
// oxen (ox)
['nexo', 4, false, false, 'ox'],
// indices (index), appendices (appendix), prices (price)
['seci', 4, false, true, ['ex', 'ix', 'ice']],
// selfies (selfie)
['seifles', 7, true, true, 'selfie'],
// movies (movie)
['seivom', 6, true, true, 'movie'],
// feet (foot)
['teef', 4, true, true, 'foot'],
// geese (goose)
['eseeg', 5, true, true, 'goose'],
// teeth (tooth)
['hteet', 5, true, true, 'tooth'],
// news (news)
['swen', 4, true, true, 'news'],
// series (series)
['seires', 6, true, true, 'series'],
// babies (baby)
['sei', 3, false, true, 'y'],
// accesses (access), addresses (address), kisses (kiss)
['sess', 4, true, false, 'ss'],
// analyses (analysis), ellipses (ellipsis), fungi (fungus),
// neuroses (neurosis), theses (thesis), emphases (emphasis),
// oases (oasis), crises (crisis), houses (house), bases (base),
// atlases (atlas)
['ses', 3, true, true, ['s', 'se', 'sis']],
// objectives (objective), alternative (alternatives)
['sevit', 5, true, true, 'tive'],
// drives (drive)
['sevird', 6, false, true, 'drive'],
// lives (life), wives (wife)
['sevi', 4, false, true, 'ife'],
// moves (move)
['sevom', 5, true, true, 'move'],
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
['sev', 3, true, true, ['f', 've', 'ff']],
// axes (axis), axes (ax), axes (axe)
['sexa', 4, false, false, ['ax', 'axe', 'axis']],
// indexes (index), matrixes (matrix)
['sex', 3, true, false, 'x'],
// quizzes (quiz)
['sezz', 4, true, false, 'z'],
// bureaus (bureau)
['suae', 4, false, true, 'eau'],
// fees (fee), trees (tree), employees (employee)
['see', 3, true, true, 'ee'],
// roses (rose), garages (garage), cassettes (cassette),
// waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
// shoes (shoe)
['se', 2, true, true, ['', 'e']],
// tags (tag)
['s', 1, true, true, ''],
// chateaux (chateau)
['xuae', 4, false, true, 'eau'],
// people (person)
['elpoep', 6, true, true, 'person'],
];
/**
* Map English singular to plural suffixes.
*
* @see http://english-zone.com/spelling/plurals.html
*/
private static $singularMap = [
// First entry: singular suffix, reversed
// Second entry: length of singular suffix
// Third entry: Whether the suffix may succeed a vocal
// Fourth entry: Whether the suffix may succeed a consonant
// Fifth entry: plural suffix, normal
// criterion (criteria)
['airetirc', 8, false, false, 'criterion'],
// nebulae (nebula)
['aluben', 6, false, false, 'nebulae'],
// children (child)
['dlihc', 5, true, true, 'children'],
// prices (price)
['eci', 3, false, true, 'ices'],
// services (service)
['ecivres', 7, true, true, 'services'],
// lives (life), wives (wife)
['efi', 3, false, true, 'ives'],
// selfies (selfie)
['eifles', 6, true, true, 'selfies'],
// movies (movie)
['eivom', 5, true, true, 'movies'],
// lice (louse)
['esuol', 5, false, true, 'lice'],
// mice (mouse)
['esuom', 5, false, true, 'mice'],
// geese (goose)
['esoo', 4, false, true, 'eese'],
// houses (house), bases (base)
['es', 2, true, true, 'ses'],
// geese (goose)
['esoog', 5, true, true, 'geese'],
// caves (cave)
['ev', 2, true, true, 'ves'],
// drives (drive)
['evird', 5, false, true, 'drives'],
// objectives (objective), alternative (alternatives)
['evit', 4, true, true, 'tives'],
// moves (move)
['evom', 4, true, true, 'moves'],
// staves (staff)
['ffats', 5, true, true, 'staves'],
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
['ff', 2, true, true, 'ffs'],
// hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
['f', 1, true, true, ['fs', 'ves']],
// arches (arch)
['hc', 2, true, true, 'ches'],
// bushes (bush)
['hs', 2, true, true, 'shes'],
// teeth (tooth)
['htoot', 5, true, true, 'teeth'],
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['mu', 2, true, true, 'a'],
// men (man), women (woman)
['nam', 3, true, true, 'men'],
// people (person)
['nosrep', 6, true, true, ['persons', 'people']],
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['noi', 3, true, true, 'ions'],
// seasons (season), treasons (treason), poisons (poison), lessons (lesson)
['nos', 3, true, true, 'sons'],
// bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
['no', 2, true, true, 'a'],
// echoes (echo)
['ohce', 4, true, true, 'echoes'],
// heroes (hero)
['oreh', 4, true, true, 'heroes'],
// atlases (atlas)
['salta', 5, true, true, 'atlases'],
// irises (iris)
['siri', 4, true, true, 'irises'],
// analyses (analysis), ellipses (ellipsis), neuroses (neurosis)
// theses (thesis), emphases (emphasis), oases (oasis),
// crises (crisis)
['sis', 3, true, true, 'ses'],
// accesses (access), addresses (address), kisses (kiss)
['ss', 2, true, false, 'sses'],
// syllabi (syllabus)
['suballys', 8, true, true, 'syllabi'],
// buses (bus)
['sub', 3, true, true, 'buses'],
// circuses (circus)
['suc', 3, true, true, 'cuses'],
// fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
['su', 2, true, true, 'i'],
// news (news)
['swen', 4, true, true, 'news'],
// feet (foot)
['toof', 4, true, true, 'feet'],
// chateaux (chateau), bureaus (bureau)
['uae', 3, false, true, ['eaus', 'eaux']],
// oxen (ox)
['xo', 2, false, false, 'oxen'],
// hoaxes (hoax)
['xaoh', 4, true, false, 'hoaxes'],
// indices (index)
['xedni', 5, false, true, ['indicies', 'indexes']],
// boxes (box)
['xo', 2, false, true, 'oxes'],
// indexes (index), matrixes (matrix)
['x', 1, true, false, ['cies', 'xes']],
// appendices (appendix)
['xi', 2, false, true, 'ices'],
// babies (baby)
['y', 1, false, true, 'ies'],
// quizzes (quiz)
['ziuq', 4, true, false, 'quizzes'],
// waltzes (waltz)
['z', 1, true, true, 'zes'],
];
/**
* A list of words which should not be inflected, reversed.
*/
private static $uninflected = [
'atad',
'reed',
'kcabdeef',
'hsif',
'ofni',
'esoom',
'seires',
'peehs',
'seiceps',
];
/**
* {@inheritdoc}
*/
public function singularize(string $plural): array
{
$pluralRev = strrev($plural);
$lowerPluralRev = strtolower($pluralRev);
$pluralLength = \strlen($lowerPluralRev);
// Check if the word is one which is not inflected, return early if so
if (\in_array($lowerPluralRev, self::$uninflected, true)) {
return [$plural];
}
// The outer loop iterates over the entries of the plural table
// The inner loop $j iterates over the characters of the plural suffix
// in the plural table to compare them with the characters of the actual
// given plural suffix
foreach (self::$pluralMap as $map) {
$suffix = $map[0];
$suffixLength = $map[1];
$j = 0;
// Compare characters in the plural table and of the suffix of the
// given plural one by one
while ($suffix[$j] === $lowerPluralRev[$j]) {
// Let $j point to the next character
++$j;
// Successfully compared the last character
// Add an entry with the singular suffix to the singular array
if ($j === $suffixLength) {
// Is there any character preceding the suffix in the plural string?
if ($j < $pluralLength) {
$nextIsVocal = false !== strpos('aeiou', $lowerPluralRev[$j]);
if (!$map[2] && $nextIsVocal) {
// suffix may not succeed a vocal but next char is one
break;
}
if (!$map[3] && !$nextIsVocal) {
// suffix may not succeed a consonant but next char is one
break;
}
}
$newBase = substr($plural, 0, $pluralLength - $suffixLength);
$newSuffix = $map[4];
// Check whether the first character in the plural suffix
// is uppercased. If yes, uppercase the first character in
// the singular suffix too
$firstUpper = ctype_upper($pluralRev[$j - 1]);
if (\is_array($newSuffix)) {
$singulars = [];
foreach ($newSuffix as $newSuffixEntry) {
$singulars[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
}
return $singulars;
}
return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
}
// Suffix is longer than word
if ($j === $pluralLength) {
break;
}
}
}
// Assume that plural and singular is identical
return [$plural];
}
/**
* {@inheritdoc}
*/
public function pluralize(string $singular): array
{
$singularRev = strrev($singular);
$lowerSingularRev = strtolower($singularRev);
$singularLength = \strlen($lowerSingularRev);
// Check if the word is one which is not inflected, return early if so
if (\in_array($lowerSingularRev, self::$uninflected, true)) {
return [$singular];
}
// The outer loop iterates over the entries of the singular table
// The inner loop $j iterates over the characters of the singular suffix
// in the singular table to compare them with the characters of the actual
// given singular suffix
foreach (self::$singularMap as $map) {
$suffix = $map[0];
$suffixLength = $map[1];
$j = 0;
// Compare characters in the singular table and of the suffix of the
// given plural one by one
while ($suffix[$j] === $lowerSingularRev[$j]) {
// Let $j point to the next character
++$j;
// Successfully compared the last character
// Add an entry with the plural suffix to the plural array
if ($j === $suffixLength) {
// Is there any character preceding the suffix in the plural string?
if ($j < $singularLength) {
$nextIsVocal = false !== strpos('aeiou', $lowerSingularRev[$j]);
if (!$map[2] && $nextIsVocal) {
// suffix may not succeed a vocal but next char is one
break;
}
if (!$map[3] && !$nextIsVocal) {
// suffix may not succeed a consonant but next char is one
break;
}
}
$newBase = substr($singular, 0, $singularLength - $suffixLength);
$newSuffix = $map[4];
// Check whether the first character in the singular suffix
// is uppercased. If yes, uppercase the first character in
// the singular suffix too
$firstUpper = ctype_upper($singularRev[$j - 1]);
if (\is_array($newSuffix)) {
$plurals = [];
foreach ($newSuffix as $newSuffixEntry) {
$plurals[] = $newBase.($firstUpper ? ucfirst($newSuffixEntry) : $newSuffixEntry);
}
return $plurals;
}
return [$newBase.($firstUpper ? ucfirst($newSuffix) : $newSuffix)];
}
// Suffix is longer than word
if ($j === $singularLength) {
break;
}
}
}
// Assume that plural is singular with a trailing `s`
return [$singular.'s'];
}
}

View File

@ -0,0 +1,33 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\String\Inflector;
interface InflectorInterface
{
/**
* Returns the singular forms of a string.
*
* If the method can't determine the form with certainty, several possible singulars are returned.
*
* @return string[] An array of possible singular forms
*/
public function singularize(string $plural): array;
/**
* Returns the plural forms of a string.
*
* If the method can't determine the form with certainty, several possible plurals are returned.
*
* @return string[] An array of possible plural forms
*/
public function pluralize(string $singular): array;
}

View File

@ -0,0 +1,309 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\String\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\String\Inflector\EnglishInflector;
class EnglishInflectorTest extends TestCase
{
public function singularizeProvider()
{
// see http://english-zone.com/spelling/plurals.html
// see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
return [
['accesses', 'access'],
['addresses', 'address'],
['agendas', 'agenda'],
['alumnae', 'alumna'],
['alumni', 'alumnus'],
['analyses', ['analys', 'analyse', 'analysis']],
['antennae', 'antenna'],
['antennas', 'antenna'],
['appendices', ['appendex', 'appendix', 'appendice']],
['arches', ['arch', 'arche']],
['atlases', ['atlas', 'atlase', 'atlasis']],
['axes', ['ax', 'axe', 'axis']],
['babies', 'baby'],
['bacteria', ['bacterion', 'bacterium']],
['bases', ['bas', 'base', 'basis']],
['batches', ['batch', 'batche']],
['beaux', 'beau'],
['bees', 'bee'],
['boxes', 'box'],
['boys', 'boy'],
['bureaus', 'bureau'],
['bureaux', 'bureau'],
['buses', ['bus', 'buse', 'busis']],
['bushes', ['bush', 'bushe']],
['calves', ['calf', 'calve', 'calff']],
['cars', 'car'],
['cassettes', ['cassett', 'cassette']],
['caves', ['caf', 'cave', 'caff']],
['chateaux', 'chateau'],
['cheeses', ['chees', 'cheese', 'cheesis']],
['children', 'child'],
['circuses', ['circus', 'circuse', 'circusis']],
['cliffs', 'cliff'],
['committee', 'committee'],
['crises', ['cris', 'crise', 'crisis']],
['criteria', ['criterion', 'criterium']],
['cups', 'cup'],
['data', 'data'],
['days', 'day'],
['discos', 'disco'],
['devices', ['devex', 'devix', 'device']],
['drives', 'drive'],
['drivers', 'driver'],
['dwarves', ['dwarf', 'dwarve', 'dwarff']],
['echoes', ['echo', 'echoe']],
['elves', ['elf', 'elve', 'elff']],
['emphases', ['emphas', 'emphase', 'emphasis']],
['employees', 'employee'],
['faxes', 'fax'],
['fees', 'fee'],
['feet', 'foot'],
['feedback', 'feedback'],
['foci', 'focus'],
['focuses', ['focus', 'focuse', 'focusis']],
['formulae', 'formula'],
['formulas', 'formula'],
['fungi', 'fungus'],
['funguses', ['fungus', 'funguse', 'fungusis']],
['garages', ['garag', 'garage']],
['geese', 'goose'],
['halves', ['half', 'halve', 'halff']],
['hats', 'hat'],
['heroes', ['hero', 'heroe']],
['hippopotamuses', ['hippopotamus', 'hippopotamuse', 'hippopotamusis']], //hippopotami
['hoaxes', 'hoax'],
['hooves', ['hoof', 'hoove', 'hooff']],
['houses', ['hous', 'house', 'housis']],
['indexes', 'index'],
['indices', ['index', 'indix', 'indice']],
['ions', 'ion'],
['irises', ['iris', 'irise', 'irisis']],
['kisses', 'kiss'],
['knives', 'knife'],
['lamps', 'lamp'],
['lessons', 'lesson'],
['leaves', ['leaf', 'leave', 'leaff']],
['lice', 'louse'],
['lives', 'life'],
['matrices', ['matrex', 'matrix', 'matrice']],
['matrixes', 'matrix'],
['men', 'man'],
['mice', 'mouse'],
['moves', 'move'],
['movies', 'movie'],
['nebulae', 'nebula'],
['neuroses', ['neuros', 'neurose', 'neurosis']],
['news', 'news'],
['oases', ['oas', 'oase', 'oasis']],
['objectives', 'objective'],
['oxen', 'ox'],
['parties', 'party'],
['people', 'person'],
['persons', 'person'],
['phenomena', ['phenomenon', 'phenomenum']],
['photos', 'photo'],
['pianos', 'piano'],
['plateaux', 'plateau'],
['poisons', 'poison'],
['poppies', 'poppy'],
['prices', ['prex', 'prix', 'price']],
['quizzes', 'quiz'],
['radii', 'radius'],
['roofs', 'roof'],
['roses', ['ros', 'rose', 'rosis']],
['sandwiches', ['sandwich', 'sandwiche']],
['scarves', ['scarf', 'scarve', 'scarff']],
['schemas', 'schema'], //schemata
['seasons', 'season'],
['selfies', 'selfie'],
['series', 'series'],
['services', 'service'],
['sheriffs', 'sheriff'],
['shoes', ['sho', 'shoe']],
['species', 'species'],
['spies', 'spy'],
['staves', ['staf', 'stave', 'staff']],
['stories', 'story'],
['strata', ['straton', 'stratum']],
['suitcases', ['suitcas', 'suitcase', 'suitcasis']],
['syllabi', 'syllabus'],
['tags', 'tag'],
['teeth', 'tooth'],
['theses', ['thes', 'these', 'thesis']],
['thieves', ['thief', 'thieve', 'thieff']],
['treasons', 'treason'],
['trees', 'tree'],
['waltzes', ['waltz', 'waltze']],
['wives', 'wife'],
// test casing: if the first letter was uppercase, it should remain so
['Men', 'Man'],
['GrandChildren', 'GrandChild'],
['SubTrees', 'SubTree'],
// Known issues
//['insignia', 'insigne'],
//['insignias', 'insigne'],
//['rattles', 'rattle'],
];
}
public function pluralizeProvider()
{
// see http://english-zone.com/spelling/plurals.html
// see http://www.scribd.com/doc/3271143/List-of-100-Irregular-Plural-Nouns-in-English
return [
['access', 'accesses'],
['address', 'addresses'],
['agenda', 'agendas'],
['alumnus', 'alumni'],
['analysis', 'analyses'],
['antenna', 'antennas'], //antennae
['appendix', ['appendicies', 'appendixes']],
['arch', 'arches'],
['atlas', 'atlases'],
['axe', 'axes'],
['baby', 'babies'],
['bacterium', 'bacteria'],
['base', 'bases'],
['batch', 'batches'],
['beau', ['beaus', 'beaux']],
['bee', 'bees'],
['box', 'boxes'],
['boy', 'boys'],
['bureau', ['bureaus', 'bureaux']],
['bus', 'buses'],
['bush', 'bushes'],
['calf', ['calfs', 'calves']],
['car', 'cars'],
['cassette', 'cassettes'],
['cave', 'caves'],
['chateau', ['chateaus', 'chateaux']],
['cheese', 'cheeses'],
['child', 'children'],
['circus', 'circuses'],
['cliff', 'cliffs'],
['committee', 'committees'],
['crisis', 'crises'],
['criteria', 'criterion'],
['cup', 'cups'],
['data', 'data'],
['day', 'days'],
['disco', 'discos'],
['device', 'devices'],
['drive', 'drives'],
['driver', 'drivers'],
['dwarf', ['dwarfs', 'dwarves']],
['echo', 'echoes'],
['elf', ['elfs', 'elves']],
['emphasis', 'emphases'],
['fax', ['facies', 'faxes']],
['feedback', 'feedback'],
['focus', 'focuses'],
['foot', 'feet'],
['formula', 'formulas'], //formulae
['fungus', 'fungi'],
['garage', 'garages'],
['goose', 'geese'],
['half', ['halfs', 'halves']],
['hat', 'hats'],
['hero', 'heroes'],
['hippopotamus', 'hippopotami'], //hippopotamuses
['hoax', 'hoaxes'],
['hoof', ['hoofs', 'hooves']],
['house', 'houses'],
['index', ['indicies', 'indexes']],
['ion', 'ions'],
['iris', 'irises'],
['kiss', 'kisses'],
['knife', 'knives'],
['lamp', 'lamps'],
['leaf', ['leafs', 'leaves']],
['lesson', 'lessons'],
['life', 'lives'],
['louse', 'lice'],
['man', 'men'],
['matrix', ['matricies', 'matrixes']],
['mouse', 'mice'],
['move', 'moves'],
['movie', 'movies'],
['nebula', 'nebulae'],
['neurosis', 'neuroses'],
['news', 'news'],
['oasis', 'oases'],
['objective', 'objectives'],
['ox', 'oxen'],
['party', 'parties'],
['person', ['persons', 'people']],
['phenomenon', 'phenomena'],
['photo', 'photos'],
['piano', 'pianos'],
['plateau', ['plateaus', 'plateaux']],
['poison', 'poisons'],
['poppy', 'poppies'],
['price', 'prices'],
['quiz', 'quizzes'],
['radius', 'radii'],
['roof', ['roofs', 'rooves']],
['rose', 'roses'],
['sandwich', 'sandwiches'],
['scarf', ['scarfs', 'scarves']],
['schema', 'schemas'], //schemata
['season', 'seasons'],
['selfie', 'selfies'],
['series', 'series'],
['service', 'services'],
['sheriff', 'sheriffs'],
['shoe', 'shoes'],
['species', 'species'],
['spy', 'spies'],
['staff', 'staves'],
['story', 'stories'],
['stratum', 'strata'],
['suitcase', 'suitcases'],
['syllabus', 'syllabi'],
['tag', 'tags'],
['thief', ['thiefs', 'thieves']],
['tooth', 'teeth'],
['treason', 'treasons'],
['tree', 'trees'],
['waltz', 'waltzes'],
['wife', 'wives'],
// test casing: if the first letter was uppercase, it should remain so
['Man', 'Men'],
['GrandChild', 'GrandChildren'],
['SubTree', 'SubTrees'],
];
}
/**
* @dataProvider singularizeProvider
*/
public function testSingularize(string $plural, $singular)
{
$this->assertSame(\is_array($singular) ? $singular : [$singular], (new EnglishInflector())->singularize($plural));
}
/**
* @dataProvider pluralizeProvider
*/
public function testPluralize(string $singular, $plural)
{
$this->assertSame(\is_array($plural) ? $plural : [$plural], (new EnglishInflector())->pluralize($singular));
}
}