Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.
   1  <?php
   2  /**
   3   * Copyright 2007-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * @category   Horde
   6   * @package    Support
   7   * @license    http://www.horde.org/licenses/bsd
   8   */
   9  
  10  /**
  11   * Horde Inflector class.
  12   *
  13   * @todo Add the locale-bubbling pattern from
  14   *       Horde_Date_Parser/Horde_Support_Numerizer
  15   *
  16   * @category   Horde
  17   * @package    Support
  18   * @license    http://www.horde.org/licenses/bsd
  19   */
  20  class Horde_Support_Inflector
  21  {
  22      /**
  23       * Inflection cache
  24       *
  25       * @var array
  26       */
  27      protected $_cache = array();
  28  
  29      /**
  30       * Rules for pluralizing English nouns.
  31       *
  32       * @var array
  33       */
  34      protected $_pluralizationRules = array(
  35          '/move$/i' => 'moves',
  36          '/sex$/i' => 'sexes',
  37          '/child$/i' => 'children',
  38          '/man$/i' => 'men',
  39          '/foot$/i' => 'feet',
  40          '/person$/i' => 'people',
  41          '/(quiz)$/i' => '$1zes',
  42          '/^(ox)$/i' => '$1en',
  43          '/(m|l)ouse$/i' => '$1ice',
  44          '/(matr|vert|ind)ix|ex$/i' => '$1ices',
  45          '/(x|ch|ss|sh)$/i' => '$1es',
  46          '/([^aeiouy]|qu)ies$/i' => '$1y',
  47          '/([^aeiouy]|qu)y$/i' => '$1ies',
  48          '/(?:([^f])fe|([lr])f)$/i' => '$1$2ves',
  49          '/sis$/i' => 'ses',
  50          '/([ti])um$/i' => '$1a',
  51          '/(buffal|tomat)o$/i' => '$1oes',
  52          '/(bu)s$/i' => '$1ses',
  53          '/(alias|status)$/i' => '$1es',
  54          '/(octop|vir)us$/i' => '$1i',
  55          '/(ax|test)is$/i' => '$1es',
  56          '/s$/i' => 's',
  57          '/$/' => 's',
  58      );
  59  
  60      /**
  61       * Rules for singularizing English nouns.
  62       *
  63       * @var array
  64       */
  65      protected $_singularizationRules = array(
  66          '/cookies$/i' => 'cookie',
  67          '/moves$/i' => 'move',
  68          '/sexes$/i' => 'sex',
  69          '/children$/i' => 'child',
  70          '/men$/i' => 'man',
  71          '/feet$/i' => 'foot',
  72          '/people$/i' => 'person',
  73          '/databases$/i'=> 'database',
  74          '/(quiz)zes$/i' => '\1',
  75          '/(matr)ices$/i' => '\1ix',
  76          '/(vert|ind)ices$/i' => '\1ex',
  77          '/^(ox)en/i' => '\1',
  78          '/(alias|status)es$/i' => '\1',
  79          '/([octop|vir])i$/i' => '\1us',
  80          '/(cris|ax|test)es$/i' => '\1is',
  81          '/(shoe)s$/i' => '\1',
  82          '/(o)es$/i' => '\1',
  83          '/(bus)es$/i' => '\1',
  84          '/([m|l])ice$/i' => '\1ouse',
  85          '/(x|ch|ss|sh)es$/i' => '\1',
  86          '/(m)ovies$/i' => '\1ovie',
  87          '/(s)eries$/i' => '\1eries',
  88          '/([^aeiouy]|qu)ies$/i' => '\1y',
  89          '/([lr])ves$/i' => '\1f',
  90          '/(tive)s$/i' => '\1',
  91          '/(hive)s$/i' => '\1',
  92          '/([^f])ves$/i' => '\1fe',
  93          '/(^analy)ses$/i' => '\1sis',
  94          '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
  95          '/([ti])a$/i' => '\1um',
  96          '/(n)ews$/i' => '\1ews',
  97          '/(.*)s$/i' => '\1',
  98      );
  99  
 100      /**
 101       * An array of words with the same singular and plural spellings.
 102       *
 103       * @var array
 104       */
 105      protected $_uncountables = array(
 106          'aircraft',
 107          'cannon',
 108          'deer',
 109          'equipment',
 110          'fish',
 111          'information',
 112          'money',
 113          'moose',
 114          'rice',
 115          'series',
 116          'sheep',
 117          'species',
 118          'swine',
 119      );
 120  
 121      /**
 122       * Constructor.
 123       *
 124       * Stores a map of the uncountable words for quicker checks.
 125       */
 126      public function __construct()
 127      {
 128          $this->_uncountables_keys = array_flip($this->_uncountables);
 129      }
 130  
 131      /**
 132       * Adds an uncountable word.
 133       *
 134       * @param string $word The uncountable word.
 135       */
 136      public function uncountable($word)
 137      {
 138          $this->_uncountables[] = $word;
 139          $this->_uncountables_keys[$word] = true;
 140      }
 141  
 142      /**
 143       * Singular English word to pluralize.
 144       *
 145       * @param string $word Word to pluralize.
 146       *
 147       * @return string Plural form of $word.
 148       */
 149      public function pluralize($word)
 150      {
 151          if ($plural = $this->getCache($word, 'pluralize')) {
 152              return $plural;
 153          }
 154  
 155          if (isset($this->_uncountables_keys[$word])) {
 156              return $word;
 157          }
 158  
 159          foreach ($this->_pluralizationRules as $regexp => $replacement) {
 160              $plural = preg_replace($regexp, $replacement, $word, -1, $matches);
 161              if ($matches > 0) {
 162                  return $this->setCache($word, 'pluralize', $plural);
 163              }
 164          }
 165  
 166          return $this->setCache($word, 'pluralize', $word);
 167      }
 168  
 169      /**
 170       * Plural English word to singularize.
 171       *
 172       * @param string $word Word to singularize.
 173       *
 174       * @return string Singular form of $word.
 175       */
 176      public function singularize($word)
 177      {
 178          if ($singular = $this->getCache($word, 'singularize')) {
 179              return $singular;
 180          }
 181  
 182          if (isset($this->_uncountables_keys[$word])) {
 183              return $word;
 184          }
 185  
 186          foreach ($this->_singularizationRules as $regexp => $replacement) {
 187              $singular = preg_replace($regexp, $replacement, $word, -1, $matches);
 188              if ($matches > 0) {
 189                  return $this->setCache($word, 'singularize', $singular);
 190              }
 191          }
 192  
 193          return $this->setCache($word, 'singularize', $word);
 194      }
 195  
 196      /**
 197       * Camel-cases a word.
 198       *
 199       * @todo Do we want locale-specific or locale-independent camel casing?
 200       *
 201       * @param string $word         The word to camel-case.
 202       * @param string $firstLetter  Whether to upper or lower case the first.
 203       *                             letter of each slash-separated section.
 204       *
 205       * @return string Camelized $word
 206       */
 207      public function camelize($word, $firstLetter = 'upper')
 208      {
 209          if ($camelized = $this->getCache($word, 'camelize' . $firstLetter)) {
 210              return $camelized;
 211          }
 212  
 213          $camelized = $word;
 214          if (Horde_String::lower($camelized) != $camelized &&
 215              strpos($camelized, '_') !== false) {
 216              $camelized = str_replace('_', '/', $camelized);
 217          }
 218          if (strpos($camelized, '/') !== false) {
 219              $camelized = str_replace('/', '/ ', $camelized);
 220          }
 221          if (strpos($camelized, '_') !== false) {
 222              $camelized = strtr($camelized, '_', ' ');
 223          }
 224  
 225          $camelized = str_replace(' ', '', Horde_String::ucwords($camelized));
 226  
 227          if ($firstLetter == 'lower') {
 228              $parts = array();
 229              foreach (explode('/', $camelized) as $part) {
 230                  $part[0] = Horde_String::lower($part[0]);
 231                  $parts[] = $part;
 232              }
 233              $camelized = implode('/', $parts);
 234          }
 235  
 236          return $this->setCache($word, 'camelize' . $firstLetter, $camelized);
 237      }
 238  
 239      /**
 240       * Capitalizes all the words and replaces some characters in the string to
 241       * create a nicer looking title.
 242       *
 243       * Titleize is meant for creating pretty output.
 244       *
 245       * See:
 246       * - http://daringfireball.net/2008/05/title_case
 247       * - http://daringfireball.net/2008/08/title_case_update
 248       *
 249       * Examples:
 250       * 1. titleize("man from the boondocks") => "Man From The Boondocks"
 251       * 2. titleize("x-men: the last stand")  => "X Men: The Last Stand"
 252       */
 253      public function titleize($word)
 254      {
 255          throw new Exception('not implemented yet');
 256      }
 257  
 258      /**
 259       * The reverse of camelize().
 260       *
 261       * Makes an underscored form from the expression in the string.
 262       *
 263       * Examples:
 264       * 1. underscore("ActiveRecord")        => "active_record"
 265       * 2. underscore("ActiveRecord_Errors") => "active_record_errors"
 266       *
 267       * @todo Do we want locale-specific or locale-independent lowercasing?
 268       */
 269      public function underscore($camelCasedWord)
 270      {
 271          $word = $camelCasedWord;
 272          if ($result = $this->getCache($word, 'underscore')) {
 273              return $result;
 274          }
 275          $result = Horde_String::lower(preg_replace('/([a-z])([A-Z])/', "\$1}_\$2}", $word));
 276          return $this->setCache($word, 'underscore', $result);
 277      }
 278  
 279      /**
 280       * Replaces underscores with dashes in the string.
 281       *
 282       * Example:
 283       * 1. dasherize("puni_puni") => "puni-puni"
 284       */
 285      public function dasherize($underscoredWord)
 286      {
 287          if ($result = $this->getCache($underscoredWord, 'dasherize')) {
 288              return $result;
 289          }
 290  
 291          $result = str_replace('_', '-', $this->underscore($underscoredWord));
 292          return $this->setCache($underscoredWord, 'dasherize', $result);
 293      }
 294  
 295      /**
 296       * Capitalizes the first word and turns underscores into spaces and strips
 297       * _id.
 298       *
 299       * Like titleize(), this is meant for creating pretty output.
 300       *
 301       * Examples:
 302       * 1. humanize("employee_salary") => "Employee salary"
 303       * 2. humanize("author_id")       => "Author"
 304       */
 305      public function humanize($lowerCaseAndUnderscoredWord)
 306      {
 307          $word = $lowerCaseAndUnderscoredWord;
 308          if ($result = $this->getCache($word, 'humanize')) {
 309              return $result;
 310          }
 311  
 312          $result = ucfirst(str_replace('_', ' ', $this->underscore($word)));
 313          if (substr($result, -3, 3) == ' id') {
 314              $result = str_replace(' id', '', $result);
 315          }
 316          return $this->setCache($word, 'humanize', $result);
 317      }
 318  
 319      /**
 320       * Removes the module part from the expression in the string.
 321       *
 322       * Examples:
 323       * 1. demodulize("Fax_Job") => "Job"
 324       * 1. demodulize("User")    => "User"
 325       */
 326      public function demodulize($classNameInModule)
 327      {
 328          $result = explode('_', $classNameInModule);
 329          return array_pop($result);
 330      }
 331  
 332      /**
 333       * Creates the name of a table like Rails does for models to table names.
 334       *
 335       * This method uses the pluralize() method on the last word in the string.
 336       *
 337       * Examples:
 338       * 1. tableize("RawScaledScorer") => "raw_scaled_scorers"
 339       * 2. tableize("egg_and_ham")     => "egg_and_hams"
 340       * 3. tableize("fancyCategory")   => "fancy_categories"
 341       */
 342      public function tableize($className)
 343      {
 344          if ($result = $this->getCache($className, 'tableize')) {
 345              return $result;
 346          }
 347  
 348          $result = $this->pluralize($this->underscore($className));
 349          $result = str_replace('/', '_', $result);
 350          return $this->setCache($className, 'tableize', $result);
 351      }
 352  
 353      /**
 354       * Creates a class name from a table name like Rails does for table names
 355       * to models.
 356       *
 357       * Examples:
 358       * 1. classify("egg_and_hams") => "EggAndHam"
 359       * 2. classify("post")         => "Post"
 360       */
 361      public function classify($tableName)
 362      {
 363          if ($result = $this->getCache($tableName, 'classify')) {
 364              return $result;
 365          }
 366          $result = $this->camelize($this->singularize($tableName));
 367  
 368          // classes use underscores instead of slashes for namespaces
 369          $result = str_replace('/', '_', $result);
 370          return $this->setCache($tableName, 'classify', $result);
 371      }
 372  
 373      /**
 374       * Creates a foreign key name from a class name.
 375       *
 376       * $separateClassNameAndIdWithUnderscore sets whether the method should put
 377       * '_' between the name and 'id'.
 378       *
 379       * Examples:
 380       * 1. foreignKey("Message")        => "message_id"
 381       * 2. foreignKey("Message", false) => "messageid"
 382       * 3. foreignKey("Fax_Job")        => "fax_job_id"
 383       */
 384      public function foreignKey($className, $separateClassNameAndIdWithUnderscore = true)
 385      {
 386          throw new Exception('not implemented yet');
 387      }
 388  
 389      /**
 390       * Turns a number into an ordinal string used to denote the position in an
 391       * ordered sequence such as 1st, 2nd, 3rd, 4th.
 392       *
 393       * Examples:
 394       * 1. ordinalize(1)      => "1st"
 395       * 2. ordinalize(2)      => "2nd"
 396       * 3. ordinalize(1002)   => "1002nd"
 397       * 4. ordinalize(1003)   => "1003rd"
 398       */
 399      public function ordinalize($number)
 400      {
 401          throw new Exception('not implemented yet');
 402      }
 403  
 404      /**
 405       * Clears the inflection cache.
 406       */
 407      public function clearCache()
 408      {
 409          $this->_cache = array();
 410      }
 411  
 412      /**
 413       * Retuns a cached inflection.
 414       *
 415       * @return string | false
 416       */
 417      public function getCache($word, $rule)
 418      {
 419          return isset($this->_cache[$word . '|' . $rule]) ?
 420              $this->_cache[$word . '|' . $rule] : false;
 421      }
 422  
 423      /**
 424       * Caches an inflection.
 425       *
 426       * @param string $word   The word being inflected.
 427       * @param string $rule   The inflection rule.
 428       * @param string $value  The inflected value of $word.
 429       *
 430       * @return string The inflected value
 431       */
 432      public function setCache($word, $rule, $value)
 433      {
 434          $this->_cache[$word . '|' . $rule] = $value;
 435          return $value;
 436      }
 437  }