/wp-includes/pomo/plural-forms.php

https://bitbucket.org/skyarch-iijima/wordpress · PHP · 343 lines · 208 code · 49 blank · 86 comment · 27 complexity · d62335468f20184122e157a9662b01a6 MD5 · raw file

  1. <?php
  2. /**
  3. * A gettext Plural-Forms parser.
  4. *
  5. * @since 4.9.0
  6. */
  7. class Plural_Forms {
  8. /**
  9. * Operator characters.
  10. *
  11. * @since 4.9.0
  12. * @var string OP_CHARS Operator characters.
  13. */
  14. const OP_CHARS = '|&><!=%?:';
  15. /**
  16. * Valid number characters.
  17. *
  18. * @since 4.9.0
  19. * @var string NUM_CHARS Valid number characters.
  20. */
  21. const NUM_CHARS = '0123456789';
  22. /**
  23. * Operator precedence.
  24. *
  25. * Operator precedence from highest to lowest. Higher numbers indicate
  26. * higher precedence, and are executed first.
  27. *
  28. * @see https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
  29. *
  30. * @since 4.9.0
  31. * @var array $op_precedence Operator precedence from highest to lowest.
  32. */
  33. protected static $op_precedence = array(
  34. '%' => 6,
  35. '<' => 5,
  36. '<=' => 5,
  37. '>' => 5,
  38. '>=' => 5,
  39. '==' => 4,
  40. '!=' => 4,
  41. '&&' => 3,
  42. '||' => 2,
  43. '?:' => 1,
  44. '?' => 1,
  45. '(' => 0,
  46. ')' => 0,
  47. );
  48. /**
  49. * Tokens generated from the string.
  50. *
  51. * @since 4.9.0
  52. * @var array $tokens List of tokens.
  53. */
  54. protected $tokens = array();
  55. /**
  56. * Cache for repeated calls to the function.
  57. *
  58. * @since 4.9.0
  59. * @var array $cache Map of $n => $result
  60. */
  61. protected $cache = array();
  62. /**
  63. * Constructor.
  64. *
  65. * @since 4.9.0
  66. *
  67. * @param string $str Plural function (just the bit after `plural=` from Plural-Forms)
  68. */
  69. public function __construct( $str ) {
  70. $this->parse( $str );
  71. }
  72. /**
  73. * Parse a Plural-Forms string into tokens.
  74. *
  75. * Uses the shunting-yard algorithm to convert the string to Reverse Polish
  76. * Notation tokens.
  77. *
  78. * @since 4.9.0
  79. *
  80. * @param string $str String to parse.
  81. */
  82. protected function parse( $str ) {
  83. $pos = 0;
  84. $len = strlen( $str );
  85. // Convert infix operators to postfix using the shunting-yard algorithm.
  86. $output = array();
  87. $stack = array();
  88. while ( $pos < $len ) {
  89. $next = substr( $str, $pos, 1 );
  90. switch ( $next ) {
  91. // Ignore whitespace
  92. case ' ':
  93. case "\t":
  94. $pos++;
  95. break;
  96. // Variable (n)
  97. case 'n':
  98. $output[] = array( 'var' );
  99. $pos++;
  100. break;
  101. // Parentheses
  102. case '(':
  103. $stack[] = $next;
  104. $pos++;
  105. break;
  106. case ')':
  107. $found = false;
  108. while ( ! empty( $stack ) ) {
  109. $o2 = $stack[ count( $stack ) - 1 ];
  110. if ( $o2 !== '(' ) {
  111. $output[] = array( 'op', array_pop( $stack ) );
  112. continue;
  113. }
  114. // Discard open paren.
  115. array_pop( $stack );
  116. $found = true;
  117. break;
  118. }
  119. if ( ! $found ) {
  120. throw new Exception( 'Mismatched parentheses' );
  121. }
  122. $pos++;
  123. break;
  124. // Operators
  125. case '|':
  126. case '&':
  127. case '>':
  128. case '<':
  129. case '!':
  130. case '=':
  131. case '%':
  132. case '?':
  133. $end_operator = strspn( $str, self::OP_CHARS, $pos );
  134. $operator = substr( $str, $pos, $end_operator );
  135. if ( ! array_key_exists( $operator, self::$op_precedence ) ) {
  136. throw new Exception( sprintf( 'Unknown operator "%s"', $operator ) );
  137. }
  138. while ( ! empty( $stack ) ) {
  139. $o2 = $stack[ count( $stack ) - 1 ];
  140. // Ternary is right-associative in C
  141. if ( $operator === '?:' || $operator === '?' ) {
  142. if ( self::$op_precedence[ $operator ] >= self::$op_precedence[ $o2 ] ) {
  143. break;
  144. }
  145. } elseif ( self::$op_precedence[ $operator ] > self::$op_precedence[ $o2 ] ) {
  146. break;
  147. }
  148. $output[] = array( 'op', array_pop( $stack ) );
  149. }
  150. $stack[] = $operator;
  151. $pos += $end_operator;
  152. break;
  153. // Ternary "else"
  154. case ':':
  155. $found = false;
  156. $s_pos = count( $stack ) - 1;
  157. while ( $s_pos >= 0 ) {
  158. $o2 = $stack[ $s_pos ];
  159. if ( $o2 !== '?' ) {
  160. $output[] = array( 'op', array_pop( $stack ) );
  161. $s_pos--;
  162. continue;
  163. }
  164. // Replace.
  165. $stack[ $s_pos ] = '?:';
  166. $found = true;
  167. break;
  168. }
  169. if ( ! $found ) {
  170. throw new Exception( 'Missing starting "?" ternary operator' );
  171. }
  172. $pos++;
  173. break;
  174. // Default - number or invalid
  175. default:
  176. if ( $next >= '0' && $next <= '9' ) {
  177. $span = strspn( $str, self::NUM_CHARS, $pos );
  178. $output[] = array( 'value', intval( substr( $str, $pos, $span ) ) );
  179. $pos += $span;
  180. continue;
  181. }
  182. throw new Exception( sprintf( 'Unknown symbol "%s"', $next ) );
  183. }
  184. }
  185. while ( ! empty( $stack ) ) {
  186. $o2 = array_pop( $stack );
  187. if ( $o2 === '(' || $o2 === ')' ) {
  188. throw new Exception( 'Mismatched parentheses' );
  189. }
  190. $output[] = array( 'op', $o2 );
  191. }
  192. $this->tokens = $output;
  193. }
  194. /**
  195. * Get the plural form for a number.
  196. *
  197. * Caches the value for repeated calls.
  198. *
  199. * @since 4.9.0
  200. *
  201. * @param int $num Number to get plural form for.
  202. * @return int Plural form value.
  203. */
  204. public function get( $num ) {
  205. if ( isset( $this->cache[ $num ] ) ) {
  206. return $this->cache[ $num ];
  207. }
  208. return $this->cache[ $num ] = $this->execute( $num );
  209. }
  210. /**
  211. * Execute the plural form function.
  212. *
  213. * @since 4.9.0
  214. *
  215. * @param int $n Variable "n" to substitute.
  216. * @return int Plural form value.
  217. */
  218. public function execute( $n ) {
  219. $stack = array();
  220. $i = 0;
  221. $total = count( $this->tokens );
  222. while ( $i < $total ) {
  223. $next = $this->tokens[$i];
  224. $i++;
  225. if ( $next[0] === 'var' ) {
  226. $stack[] = $n;
  227. continue;
  228. } elseif ( $next[0] === 'value' ) {
  229. $stack[] = $next[1];
  230. continue;
  231. }
  232. // Only operators left.
  233. switch ( $next[1] ) {
  234. case '%':
  235. $v2 = array_pop( $stack );
  236. $v1 = array_pop( $stack );
  237. $stack[] = $v1 % $v2;
  238. break;
  239. case '||':
  240. $v2 = array_pop( $stack );
  241. $v1 = array_pop( $stack );
  242. $stack[] = $v1 || $v2;
  243. break;
  244. case '&&':
  245. $v2 = array_pop( $stack );
  246. $v1 = array_pop( $stack );
  247. $stack[] = $v1 && $v2;
  248. break;
  249. case '<':
  250. $v2 = array_pop( $stack );
  251. $v1 = array_pop( $stack );
  252. $stack[] = $v1 < $v2;
  253. break;
  254. case '<=':
  255. $v2 = array_pop( $stack );
  256. $v1 = array_pop( $stack );
  257. $stack[] = $v1 <= $v2;
  258. break;
  259. case '>':
  260. $v2 = array_pop( $stack );
  261. $v1 = array_pop( $stack );
  262. $stack[] = $v1 > $v2;
  263. break;
  264. case '>=':
  265. $v2 = array_pop( $stack );
  266. $v1 = array_pop( $stack );
  267. $stack[] = $v1 >= $v2;
  268. break;
  269. case '!=':
  270. $v2 = array_pop( $stack );
  271. $v1 = array_pop( $stack );
  272. $stack[] = $v1 != $v2;
  273. break;
  274. case '==':
  275. $v2 = array_pop( $stack );
  276. $v1 = array_pop( $stack );
  277. $stack[] = $v1 == $v2;
  278. break;
  279. case '?:':
  280. $v3 = array_pop( $stack );
  281. $v2 = array_pop( $stack );
  282. $v1 = array_pop( $stack );
  283. $stack[] = $v1 ? $v2 : $v3;
  284. break;
  285. default:
  286. throw new Exception( sprintf( 'Unknown operator "%s"', $next[1] ) );
  287. }
  288. }
  289. if ( count( $stack ) !== 1 ) {
  290. throw new Exception( 'Too many values remaining on the stack' );
  291. }
  292. return (int) $stack[0];
  293. }
  294. }