plural-forms.php 7.5 KB

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