Açıklama Yok

plural-forms.php 7.4KB

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