vendor/symfony/string/Inflector/EnglishInflector.php line 14

Open in your IDE?
  1. <?php
  2. /*
  3.  * This file is part of the Symfony package.
  4.  *
  5.  * (c) Fabien Potencier <fabien@symfony.com>
  6.  *
  7.  * For the full copyright and license information, please view the LICENSE
  8.  * file that was distributed with this source code.
  9.  */
  10. namespace Symfony\Component\String\Inflector;
  11. final class EnglishInflector implements InflectorInterface
  12. {
  13.     /**
  14.      * Map English plural to singular suffixes.
  15.      *
  16.      * @see http://english-zone.com/spelling/plurals.html
  17.      */
  18.     private const PLURAL_MAP = [
  19.         // First entry: plural suffix, reversed
  20.         // Second entry: length of plural suffix
  21.         // Third entry: Whether the suffix may succeed a vocal
  22.         // Fourth entry: Whether the suffix may succeed a consonant
  23.         // Fifth entry: singular suffix, normal
  24.         // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
  25.         ['a'1truetrue, ['on''um']],
  26.         // nebulae (nebula)
  27.         ['ea'2truetrue'a'],
  28.         // services (service)
  29.         ['secivres'8truetrue'service'],
  30.         // mice (mouse), lice (louse)
  31.         ['eci'3falsetrue'ouse'],
  32.         // geese (goose)
  33.         ['esee'4falsetrue'oose'],
  34.         // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
  35.         ['i'1truetrue'us'],
  36.         // men (man), women (woman)
  37.         ['nem'3truetrue'man'],
  38.         // children (child)
  39.         ['nerdlihc'8truetrue'child'],
  40.         // oxen (ox)
  41.         ['nexo'4falsefalse'ox'],
  42.         // indices (index), appendices (appendix), prices (price)
  43.         ['seci'4falsetrue, ['ex''ix''ice']],
  44.         // selfies (selfie)
  45.         ['seifles'7truetrue'selfie'],
  46.         // zombies (zombie)
  47.         ['seibmoz'7truetrue'zombie'],
  48.         // movies (movie)
  49.         ['seivom'6truetrue'movie'],
  50.         // conspectuses (conspectus), prospectuses (prospectus)
  51.         ['sesutcep'8truetrue'pectus'],
  52.         // feet (foot)
  53.         ['teef'4truetrue'foot'],
  54.         // geese (goose)
  55.         ['eseeg'5truetrue'goose'],
  56.         // teeth (tooth)
  57.         ['hteet'5truetrue'tooth'],
  58.         // news (news)
  59.         ['swen'4truetrue'news'],
  60.         // series (series)
  61.         ['seires'6truetrue'series'],
  62.         // babies (baby)
  63.         ['sei'3falsetrue'y'],
  64.         // accesses (access), addresses (address), kisses (kiss)
  65.         ['sess'4truefalse'ss'],
  66.         // analyses (analysis), ellipses (ellipsis), fungi (fungus),
  67.         // neuroses (neurosis), theses (thesis), emphases (emphasis),
  68.         // oases (oasis), crises (crisis), houses (house), bases (base),
  69.         // atlases (atlas)
  70.         ['ses'3truetrue, ['s''se''sis']],
  71.         // objectives (objective), alternative (alternatives)
  72.         ['sevit'5truetrue'tive'],
  73.         // drives (drive)
  74.         ['sevird'6falsetrue'drive'],
  75.         // lives (life), wives (wife)
  76.         ['sevi'4falsetrue'ife'],
  77.         // moves (move)
  78.         ['sevom'5truetrue'move'],
  79.         // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf), caves (cave), staves (staff)
  80.         ['sev'3truetrue, ['f''ve''ff']],
  81.         // axes (axis), axes (ax), axes (axe)
  82.         ['sexa'4falsefalse, ['ax''axe''axis']],
  83.         // indexes (index), matrixes (matrix)
  84.         ['sex'3truefalse'x'],
  85.         // quizzes (quiz)
  86.         ['sezz'4truefalse'z'],
  87.         // bureaus (bureau)
  88.         ['suae'4falsetrue'eau'],
  89.         // fees (fee), trees (tree), employees (employee)
  90.         ['see'3truetrue'ee'],
  91.         // edges (edge)
  92.         ['segd'4truetrue'dge'],
  93.         // roses (rose), garages (garage), cassettes (cassette),
  94.         // waltzes (waltz), heroes (hero), bushes (bush), arches (arch),
  95.         // shoes (shoe)
  96.         ['se'2truetrue, ['''e']],
  97.         // tags (tag)
  98.         ['s'1truetrue''],
  99.         // chateaux (chateau)
  100.         ['xuae'4falsetrue'eau'],
  101.         // people (person)
  102.         ['elpoep'6truetrue'person'],
  103.     ];
  104.     /**
  105.      * Map English singular to plural suffixes.
  106.      *
  107.      * @see http://english-zone.com/spelling/plurals.html
  108.      */
  109.     private const SINGULAR_MAP = [
  110.         // First entry: singular suffix, reversed
  111.         // Second entry: length of singular suffix
  112.         // Third entry: Whether the suffix may succeed a vocal
  113.         // Fourth entry: Whether the suffix may succeed a consonant
  114.         // Fifth entry: plural suffix, normal
  115.         // criterion (criteria)
  116.         ['airetirc'8falsefalse'criterion'],
  117.         // nebulae (nebula)
  118.         ['aluben'6falsefalse'nebulae'],
  119.         // children (child)
  120.         ['dlihc'5truetrue'children'],
  121.         // prices (price)
  122.         ['eci'3falsetrue'ices'],
  123.         // services (service)
  124.         ['ecivres'7truetrue'services'],
  125.         // lives (life), wives (wife)
  126.         ['efi'3falsetrue'ives'],
  127.         // selfies (selfie)
  128.         ['eifles'6truetrue'selfies'],
  129.         // movies (movie)
  130.         ['eivom'5truetrue'movies'],
  131.         // lice (louse)
  132.         ['esuol'5falsetrue'lice'],
  133.         // mice (mouse)
  134.         ['esuom'5falsetrue'mice'],
  135.         // geese (goose)
  136.         ['esoo'4falsetrue'eese'],
  137.         // houses (house), bases (base)
  138.         ['es'2truetrue'ses'],
  139.         // geese (goose)
  140.         ['esoog'5truetrue'geese'],
  141.         // caves (cave)
  142.         ['ev'2truetrue'ves'],
  143.         // drives (drive)
  144.         ['evird'5falsetrue'drives'],
  145.         // objectives (objective), alternative (alternatives)
  146.         ['evit'4truetrue'tives'],
  147.         // moves (move)
  148.         ['evom'4truetrue'moves'],
  149.         // staves (staff)
  150.         ['ffats'5truetrue'staves'],
  151.         // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
  152.         ['ff'2truetrue'ffs'],
  153.         // hooves (hoof), dwarves (dwarf), elves (elf), leaves (leaf)
  154.         ['f'1truetrue, ['fs''ves']],
  155.         // arches (arch)
  156.         ['hc'2truetrue'ches'],
  157.         // bushes (bush)
  158.         ['hs'2truetrue'shes'],
  159.         // teeth (tooth)
  160.         ['htoot'5truetrue'teeth'],
  161.         // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
  162.         ['mu'2truetrue'a'],
  163.         // men (man), women (woman)
  164.         ['nam'3truetrue'men'],
  165.         // people (person)
  166.         ['nosrep'6truetrue, ['persons''people']],
  167.         // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
  168.         ['noi'3truetrue'ions'],
  169.         // coupon (coupons)
  170.         ['nop'3truetrue'pons'],
  171.         // seasons (season), treasons (treason), poisons (poison), lessons (lesson)
  172.         ['nos'3truetrue'sons'],
  173.         // bacteria (bacterium), criteria (criterion), phenomena (phenomenon)
  174.         ['no'2truetrue'a'],
  175.         // echoes (echo)
  176.         ['ohce'4truetrue'echoes'],
  177.         // heroes (hero)
  178.         ['oreh'4truetrue'heroes'],
  179.         // atlases (atlas)
  180.         ['salta'5truetrue'atlases'],
  181.         // irises (iris)
  182.         ['siri'4truetrue'irises'],
  183.         // analyses (analysis), ellipses (ellipsis), neuroses (neurosis)
  184.         // theses (thesis), emphases (emphasis), oases (oasis),
  185.         // crises (crisis)
  186.         ['sis'3truetrue'ses'],
  187.         // accesses (access), addresses (address), kisses (kiss)
  188.         ['ss'2truefalse'sses'],
  189.         // syllabi (syllabus)
  190.         ['suballys'8truetrue'syllabi'],
  191.         // buses (bus)
  192.         ['sub'3truetrue'buses'],
  193.         // circuses (circus)
  194.         ['suc'3truetrue'cuses'],
  195.         // conspectuses (conspectus), prospectuses (prospectus)
  196.         ['sutcep'6truetrue'pectuses'],
  197.         // fungi (fungus), alumni (alumnus), syllabi (syllabus), radii (radius)
  198.         ['su'2truetrue'i'],
  199.         // news (news)
  200.         ['swen'4truetrue'news'],
  201.         // feet (foot)
  202.         ['toof'4truetrue'feet'],
  203.         // chateaux (chateau), bureaus (bureau)
  204.         ['uae'3falsetrue, ['eaus''eaux']],
  205.         // oxen (ox)
  206.         ['xo'2falsefalse'oxen'],
  207.         // hoaxes (hoax)
  208.         ['xaoh'4truefalse'hoaxes'],
  209.         // indices (index)
  210.         ['xedni'5falsetrue, ['indicies''indexes']],
  211.         // boxes (box)
  212.         ['xo'2falsetrue'oxes'],
  213.         // indexes (index), matrixes (matrix)
  214.         ['x'1truefalse, ['cies''xes']],
  215.         // appendices (appendix)
  216.         ['xi'2falsetrue'ices'],
  217.         // babies (baby)
  218.         ['y'1falsetrue'ies'],
  219.         // quizzes (quiz)
  220.         ['ziuq'4truefalse'quizzes'],
  221.         // waltzes (waltz)
  222.         ['z'1truetrue'zes'],
  223.     ];
  224.     /**
  225.      * A list of words which should not be inflected, reversed.
  226.      */
  227.     private const UNINFLECTED = [
  228.         '',
  229.         // data
  230.         'atad',
  231.         // deer
  232.         'reed',
  233.         // feedback
  234.         'kcabdeef',
  235.         // fish
  236.         'hsif',
  237.         // info
  238.         'ofni',
  239.         // moose
  240.         'esoom',
  241.         // series
  242.         'seires',
  243.         // sheep
  244.         'peehs',
  245.         // species
  246.         'seiceps',
  247.     ];
  248.     /**
  249.      * {@inheritdoc}
  250.      */
  251.     public function singularize(string $plural): array
  252.     {
  253.         $pluralRev strrev($plural);
  254.         $lowerPluralRev strtolower($pluralRev);
  255.         $pluralLength \strlen($lowerPluralRev);
  256.         // Check if the word is one which is not inflected, return early if so
  257.         if (\in_array($lowerPluralRevself::UNINFLECTEDtrue)) {
  258.             return [$plural];
  259.         }
  260.         // The outer loop iterates over the entries of the plural table
  261.         // The inner loop $j iterates over the characters of the plural suffix
  262.         // in the plural table to compare them with the characters of the actual
  263.         // given plural suffix
  264.         foreach (self::PLURAL_MAP as $map) {
  265.             $suffix $map[0];
  266.             $suffixLength $map[1];
  267.             $j 0;
  268.             // Compare characters in the plural table and of the suffix of the
  269.             // given plural one by one
  270.             while ($suffix[$j] === $lowerPluralRev[$j]) {
  271.                 // Let $j point to the next character
  272.                 ++$j;
  273.                 // Successfully compared the last character
  274.                 // Add an entry with the singular suffix to the singular array
  275.                 if ($j === $suffixLength) {
  276.                     // Is there any character preceding the suffix in the plural string?
  277.                     if ($j $pluralLength) {
  278.                         $nextIsVocal false !== strpos('aeiou'$lowerPluralRev[$j]);
  279.                         if (!$map[2] && $nextIsVocal) {
  280.                             // suffix may not succeed a vocal but next char is one
  281.                             break;
  282.                         }
  283.                         if (!$map[3] && !$nextIsVocal) {
  284.                             // suffix may not succeed a consonant but next char is one
  285.                             break;
  286.                         }
  287.                     }
  288.                     $newBase substr($plural0$pluralLength $suffixLength);
  289.                     $newSuffix $map[4];
  290.                     // Check whether the first character in the plural suffix
  291.                     // is uppercased. If yes, uppercase the first character in
  292.                     // the singular suffix too
  293.                     $firstUpper ctype_upper($pluralRev[$j 1]);
  294.                     if (\is_array($newSuffix)) {
  295.                         $singulars = [];
  296.                         foreach ($newSuffix as $newSuffixEntry) {
  297.                             $singulars[] = $newBase.($firstUpper ucfirst($newSuffixEntry) : $newSuffixEntry);
  298.                         }
  299.                         return $singulars;
  300.                     }
  301.                     return [$newBase.($firstUpper ucfirst($newSuffix) : $newSuffix)];
  302.                 }
  303.                 // Suffix is longer than word
  304.                 if ($j === $pluralLength) {
  305.                     break;
  306.                 }
  307.             }
  308.         }
  309.         // Assume that plural and singular is identical
  310.         return [$plural];
  311.     }
  312.     /**
  313.      * {@inheritdoc}
  314.      */
  315.     public function pluralize(string $singular): array
  316.     {
  317.         $singularRev strrev($singular);
  318.         $lowerSingularRev strtolower($singularRev);
  319.         $singularLength \strlen($lowerSingularRev);
  320.         // Check if the word is one which is not inflected, return early if so
  321.         if (\in_array($lowerSingularRevself::UNINFLECTEDtrue)) {
  322.             return [$singular];
  323.         }
  324.         // The outer loop iterates over the entries of the singular table
  325.         // The inner loop $j iterates over the characters of the singular suffix
  326.         // in the singular table to compare them with the characters of the actual
  327.         // given singular suffix
  328.         foreach (self::SINGULAR_MAP as $map) {
  329.             $suffix $map[0];
  330.             $suffixLength $map[1];
  331.             $j 0;
  332.             // Compare characters in the singular table and of the suffix of the
  333.             // given plural one by one
  334.             while ($suffix[$j] === $lowerSingularRev[$j]) {
  335.                 // Let $j point to the next character
  336.                 ++$j;
  337.                 // Successfully compared the last character
  338.                 // Add an entry with the plural suffix to the plural array
  339.                 if ($j === $suffixLength) {
  340.                     // Is there any character preceding the suffix in the plural string?
  341.                     if ($j $singularLength) {
  342.                         $nextIsVocal false !== strpos('aeiou'$lowerSingularRev[$j]);
  343.                         if (!$map[2] && $nextIsVocal) {
  344.                             // suffix may not succeed a vocal but next char is one
  345.                             break;
  346.                         }
  347.                         if (!$map[3] && !$nextIsVocal) {
  348.                             // suffix may not succeed a consonant but next char is one
  349.                             break;
  350.                         }
  351.                     }
  352.                     $newBase substr($singular0$singularLength $suffixLength);
  353.                     $newSuffix $map[4];
  354.                     // Check whether the first character in the singular suffix
  355.                     // is uppercased. If yes, uppercase the first character in
  356.                     // the singular suffix too
  357.                     $firstUpper ctype_upper($singularRev[$j 1]);
  358.                     if (\is_array($newSuffix)) {
  359.                         $plurals = [];
  360.                         foreach ($newSuffix as $newSuffixEntry) {
  361.                             $plurals[] = $newBase.($firstUpper ucfirst($newSuffixEntry) : $newSuffixEntry);
  362.                         }
  363.                         return $plurals;
  364.                     }
  365.                     return [$newBase.($firstUpper ucfirst($newSuffix) : $newSuffix)];
  366.                 }
  367.                 // Suffix is longer than word
  368.                 if ($j === $singularLength) {
  369.                     break;
  370.                 }
  371.             }
  372.         }
  373.         // Assume that plural is singular with a trailing `s`
  374.         return [$singular.'s'];
  375.     }
  376. }