PageRenderTime 117ms CodeModel.GetById 21ms RepoModel.GetById 15ms app.codeStats 0ms

/Classes/TYPO3/FLOW3/I18n/Cldr/Reader/PluralsReader.php

https://github.com/christianjul/FLOW3-Composer
PHP | 307 lines | 133 code | 39 blank | 135 comment | 35 complexity | 2c16d12080bcc65b90f20c6fd0630da0 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-3.0
  1. <?php
  2. namespace TYPO3\FLOW3\I18n\Cldr\Reader;
  3. /* *
  4. * This script belongs to the FLOW3 framework. *
  5. * *
  6. * It is free software; you can redistribute it and/or modify it under *
  7. * the terms of the GNU Lesser General Public License, either version 3 *
  8. * of the License, or (at your option) any later version. *
  9. * *
  10. * The TYPO3 project - inspiring people to share! *
  11. * */
  12. use TYPO3\FLOW3\Annotations as FLOW3;
  13. /**
  14. * A reader for data placed in "plurals" tag in CLDR.
  15. *
  16. * There are a few similar words used in plurals.xml file of CLDR used by this
  17. * class. Following naming convention is used in the code (a name of
  18. * corresponding tag from xml file is provided in brackets, if any):
  19. * - ruleset: a set of plural rules for a locale [pluralRules]
  20. * - rule: a rule for one of the forms: zero, one, two, few, many [pluralRule]
  21. * - subrule: one of the conditions of rule. One rule can have many conditions
  22. * joined with "and" or "or" logical operator.
  23. *
  24. * @FLOW3\Scope("singleton")
  25. * @see http://www.unicode.org/reports/tr35/#Language_Plural_Rules
  26. */
  27. class PluralsReader {
  28. /**
  29. * An expression to catch one plural subrule. One rule consists of one or
  30. * more subrules.
  31. *
  32. * @todo improve the regexp pattern
  33. */
  34. const PATTERN_MATCH_SUBRULE = '/(n|nmod)([0-9]+)?(is|isnot|in|notin|within|notwithin)([0-9]+)(?:\.\.([0-9]+))?(and|or)?/';
  35. /**
  36. * Constants for every plural rule form defined in CLDR.
  37. */
  38. const RULE_ZERO = 'zero';
  39. const RULE_ONE = 'one';
  40. const RULE_TWO = 'two';
  41. const RULE_FEW = 'few';
  42. const RULE_MANY = 'many';
  43. const RULE_OTHER = 'other';
  44. /**
  45. * @var \TYPO3\FLOW3\I18n\Cldr\CldrRepository
  46. */
  47. protected $cldrRepository;
  48. /**
  49. * @var \TYPO3\FLOW3\Cache\Frontend\VariableFrontend
  50. */
  51. protected $cache;
  52. /**
  53. * An array of rulesets, indexed numerically.
  54. *
  55. * One ruleset contains one or more rules (at most 5, one for every plural
  56. * form - zero, one, two, few, many - a rule 'other' is implicit). There can
  57. * also be NULL ruleset, used by languages which don't have plurals.
  58. *
  59. * A rule is an array with following elements:
  60. * 'modulo' => $x | FALSE,
  61. * 'condition' => array(0 => 'conditionName', 1 => $x, 2 => $y),
  62. * 'logicalOperator' => 'and' | 'or' | FALSE
  63. *
  64. * Legend:
  65. * - if 'modulo' key has an integer value, tested variable (call it $n) has
  66. * to be replaced with the remainder of division of $n by $x. Otherwise
  67. * unchanged $n is used for conditional test.
  68. * - 'condition' is an indexed array where first element is a name of test
  69. * condition (one of: is, isnot, in, notin, within, notwithin). Second
  70. * element is a value to compare $n with. Third element is optional, and
  71. * is used only for tests where range is needed (last 4 from the list above)
  72. * - 'logicalOperator' represents a logical operation to be done with next
  73. * subrule in chain. If current subrule is a last one (or only one), this
  74. * element is set to FALSE.
  75. *
  76. * @var array
  77. */
  78. protected $rulesets;
  79. /**
  80. * An associative array holding information which ruleset is used by given
  81. * locale. One or more locales can use the same ruleset.
  82. *
  83. * @var array
  84. */
  85. protected $rulesetsIndices;
  86. /**
  87. * @param \TYPO3\FLOW3\I18n\Cldr\CldrRepository $repository
  88. * @return void
  89. */
  90. public function injectCldrRepository(\TYPO3\FLOW3\I18n\Cldr\CldrRepository $repository) {
  91. $this->cldrRepository = $repository;
  92. }
  93. /**
  94. * Injects the FLOW3_I18n_Cldr_Reader_PluralsReader cache
  95. *
  96. * @param \TYPO3\FLOW3\Cache\Frontend\VariableFrontend $cache
  97. * @return void
  98. */
  99. public function injectCache(\TYPO3\FLOW3\Cache\Frontend\VariableFrontend $cache) {
  100. $this->cache = $cache;
  101. }
  102. /**
  103. * Constructs the reader, loading parsed data from cache if available.
  104. *
  105. * @return void
  106. */
  107. public function initializeObject() {
  108. if ($this->cache->has('rulesets') && $this->cache->has('rulesetsIndices')) {
  109. $this->rulesets = $this->cache->get('rulesets');
  110. $this->rulesetsIndices = $this->cache->get('rulesetsIndices');
  111. } else {
  112. $this->generateRulesets();
  113. $this->cache->set('rulesets', $this->rulesets);
  114. $this->cache->set('rulesetsIndices', $this->rulesetsIndices);
  115. }
  116. }
  117. /**
  118. * Returns matching plural form based on $quantity and $locale provided.
  119. *
  120. * Plural form is one of following: zero, one, two, few, many, other.
  121. * Last one (other) is returned when number provided doesn't match any
  122. * of the rules, or there is no rules for given locale.
  123. *
  124. * @param mixed $quantity A number to find plural form for (float or int)
  125. * @param \TYPO3\FLOW3\I18n\Locale $locale
  126. * @return string One of plural form constants
  127. */
  128. public function getPluralForm($quantity, \TYPO3\FLOW3\I18n\Locale $locale) {
  129. if (!isset($this->rulesetsIndices[$locale->getLanguage()])) {
  130. return self::RULE_OTHER;
  131. }
  132. $ruleset = $this->rulesets[$locale->getLanguage()][$this->rulesetsIndices[$locale->getLanguage()]];
  133. if ($ruleset === NULL) {
  134. return self::RULE_OTHER;
  135. }
  136. foreach ($ruleset as $form => $rule) {
  137. foreach ($rule as $subrule) {
  138. $subrulePassed = FALSE;
  139. if ($subrule['modulo'] !== FALSE) {
  140. $quantity = fmod($quantity, $subrule['modulo']);
  141. }
  142. if ($quantity == floor($quantity)) {
  143. $quantity = (int)$quantity;
  144. }
  145. $condition = $subrule['condition'];
  146. switch ($condition[0]) {
  147. case 'is':
  148. case 'isnot':
  149. if (is_int($quantity) && $quantity === $condition[1]) $subrulePassed = TRUE;
  150. if ($condition[0] === 'isnot') $subrulePassed = !$subrulePassed;
  151. break;
  152. case 'in':
  153. case 'notin':
  154. if (is_int($quantity) && $quantity >= $condition[1] && $quantity <= $condition[2]) $subrulePassed = TRUE;
  155. if ($condition[0] === 'notin') $subrulePassed = !$subrulePassed;
  156. break;
  157. case 'within':
  158. case 'notwithin':
  159. if ($quantity >= $condition[1] && $quantity <= $condition[2]) $subrulePassed = TRUE;
  160. if ($condition[0] === 'notwithin') $subrulePassed = !$subrulePassed;
  161. break;
  162. }
  163. if (($subrulePassed && $subrule['logicalOperator'] === 'or') || (!$subrulePassed && $subrule['logicalOperator'] === 'and')) {
  164. break;
  165. }
  166. }
  167. if ($subrulePassed) {
  168. return $form;
  169. }
  170. }
  171. return self::RULE_OTHER;
  172. }
  173. /**
  174. * Returns array of plural forms available for particular locale.
  175. *
  176. * @param \TYPO3\FLOW3\I18n\Locale $locale Locale to return plural forms for
  177. * @return array Plural forms' names (one, zero, two, few, many, other) available for language set in this model
  178. */
  179. public function getPluralForms(\TYPO3\FLOW3\I18n\Locale $locale) {
  180. if (!isset($this->rulesetsIndices[$locale->getLanguage()])) {
  181. return array(self::RULE_OTHER);
  182. }
  183. return array_merge(array_keys($this->rulesets[$locale->getLanguage()][$this->rulesetsIndices[$locale->getLanguage()]]), array(self::RULE_OTHER));
  184. }
  185. /**
  186. * Generates an internal representation of plural rules which can be found
  187. * in plurals.xml CLDR file.
  188. *
  189. * The properties $rulesets and $rulesetsIndices should be empty before
  190. * running this method.
  191. *
  192. * @return void
  193. * @see \TYPO3\FLOW3\I18n\Cldr\Reader\PluralsReader::$rulesets
  194. */
  195. protected function generateRulesets() {
  196. $model = $this->cldrRepository->getModel('supplemental/plurals');
  197. $pluralRulesSet = $model->getRawArray('plurals');
  198. $index = 0;
  199. foreach ($pluralRulesSet as $pluralRulesNodeString => $pluralRules) {
  200. $localeLanguages = $model->getAttributeValue($pluralRulesNodeString, 'locales');
  201. foreach (explode(' ', $localeLanguages) as $localeLanguage) {
  202. $this->rulesetsIndices[$localeLanguage] = $index;
  203. }
  204. if (is_array($pluralRules)) {
  205. $ruleset = array();
  206. foreach ($pluralRules as $pluralRuleNodeString => $pluralRule) {
  207. $pluralForm = $model->getAttributeValue($pluralRuleNodeString, 'count');
  208. $ruleset[$pluralForm] = $this->parseRule($pluralRule);
  209. }
  210. foreach (explode(' ', $localeLanguages) as $localeLanguage) {
  211. $this->rulesets[$localeLanguage][$index] = $ruleset;
  212. }
  213. }
  214. ++$index;
  215. }
  216. }
  217. /**
  218. * Parses a plural rule from CLDR.
  219. *
  220. * A plural rule in CLDR is a string with one or more test conditions, with
  221. * 'and' or 'or' logical operators between them. Whole expression can look
  222. * like this:
  223. *
  224. * n is 0 OR n is not 1 AND n mod 100 in 1..19
  225. *
  226. * As CLDR documentation says, following test conditions can be used:
  227. * - is x, is not x: $n is (not) equal $x
  228. * - in x..y, not in x..y: $n is (not) one of integers from range <$x, $y>
  229. * - within x..y, not within x..y: $n is (not) any number from range <$x, $y>
  230. *
  231. * Where $n can be a number (also float) as is, or a result of $n mod $x.
  232. *
  233. * Array returned follows simple internal format (see documentation for
  234. * $rulesets property for details).
  235. *
  236. * @param string $rule
  237. * @return array Parsed rule
  238. * @throws \TYPO3\FLOW3\I18n\Cldr\Reader\Exception\InvalidPluralRuleException When plural rule does not match regexp pattern
  239. */
  240. protected function parseRule($rule) {
  241. $parsedRule = array();
  242. if (preg_match_all(self::PATTERN_MATCH_SUBRULE, strtolower(str_replace(' ', '', $rule)), $matches, \PREG_SET_ORDER)) {
  243. foreach ($matches as $matchedSubrule) {
  244. $subrule = array();
  245. if ($matchedSubrule[1] === 'nmod') {
  246. $subrule['modulo'] = (int)$matchedSubrule[2];
  247. } else {
  248. $subrule['modulo'] = FALSE;
  249. }
  250. $condition = array($matchedSubrule[3], (int)$matchedSubrule[4]);
  251. if (!in_array($matchedSubrule[3], array('is', 'isnot'), TRUE)) {
  252. $condition[2] = (int)$matchedSubrule[5];
  253. }
  254. $subrule['condition'] = $condition;
  255. if (isset($matchedSubrule[6]) && ($matchedSubrule[6] === 'and' || $matchedSubrule[6] === 'or')) {
  256. $subrule['logicalOperator'] = $matchedSubrule[6];
  257. } else {
  258. $subrule['logicalOperator'] = FALSE;
  259. }
  260. $parsedRule[] = $subrule;
  261. }
  262. } else {
  263. throw new \TYPO3\FLOW3\I18n\Cldr\Reader\Exception\InvalidPluralRuleException('A plural rule string is invalid. CLDR files might be corrupted.', 1275493982);
  264. }
  265. return $parsedRule;
  266. }
  267. }
  268. ?>