PageRenderTime 28ms CodeModel.GetById 10ms app.highlight 13ms RepoModel.GetById 1ms app.codeStats 1ms

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