| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | <?php | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * A gettext Plural-Forms parser. | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @since 4.9.0 | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | class Plural_Forms { | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Operator characters. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * @var string OP_CHARS Operator characters. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	const OP_CHARS = '|&><!=%?:'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Valid number characters. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * @var string NUM_CHARS Valid number characters. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	const NUM_CHARS = '0123456789'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Operator precedence. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * Operator precedence from highest to lowest. Higher numbers indicate | 
					
						
							|  |  |  | 	 * higher precedence, and are executed first. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @see https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
 | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * @var array $op_precedence Operator precedence from highest to lowest. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	protected static $op_precedence = array( | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 		'%'  => 6, | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 		'<'  => 5, | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 		'<=' => 5, | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 		'>'  => 5, | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 		'>=' => 5, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		'==' => 4, | 
					
						
							|  |  |  | 		'!=' => 4, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		'&&' => 3, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		'||' => 2, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		'?:' => 1, | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 		'?'  => 1, | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 		'('  => 0, | 
					
						
							|  |  |  | 		')'  => 0, | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 	); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Tokens generated from the string. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * @var array $tokens List of tokens. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	protected $tokens = array(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Cache for repeated calls to the function. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * @var array $cache Map of $n => $result | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	protected $cache = array(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Constructor. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @param string $str Plural function (just the bit after `plural=` from Plural-Forms) | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	public function __construct( $str ) { | 
					
						
							|  |  |  | 		$this->parse( $str ); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Parse a Plural-Forms string into tokens. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * Uses the shunting-yard algorithm to convert the string to Reverse Polish | 
					
						
							|  |  |  | 	 * Notation tokens. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @param string $str String to parse. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	protected function parse( $str ) { | 
					
						
							|  |  |  | 		$pos = 0; | 
					
						
							|  |  |  | 		$len = strlen( $str ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Convert infix operators to postfix using the shunting-yard algorithm.
 | 
					
						
							|  |  |  | 		$output = array(); | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 		$stack  = array(); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 		while ( $pos < $len ) { | 
					
						
							|  |  |  | 			$next = substr( $str, $pos, 1 ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			switch ( $next ) { | 
					
						
							|  |  |  | 				// Ignore whitespace
 | 
					
						
							|  |  |  | 				case ' ': | 
					
						
							|  |  |  | 				case "\t": | 
					
						
							|  |  |  | 					$pos++; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Variable (n)
 | 
					
						
							|  |  |  | 				case 'n': | 
					
						
							|  |  |  | 					$output[] = array( 'var' ); | 
					
						
							|  |  |  | 					$pos++; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Parentheses
 | 
					
						
							|  |  |  | 				case '(': | 
					
						
							|  |  |  | 					$stack[] = $next; | 
					
						
							|  |  |  | 					$pos++; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case ')': | 
					
						
							|  |  |  | 					$found = false; | 
					
						
							|  |  |  | 					while ( ! empty( $stack ) ) { | 
					
						
							|  |  |  | 						$o2 = $stack[ count( $stack ) - 1 ]; | 
					
						
							|  |  |  | 						if ( $o2 !== '(' ) { | 
					
						
							|  |  |  | 							$output[] = array( 'op', array_pop( $stack ) ); | 
					
						
							|  |  |  | 							continue; | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// Discard open paren.
 | 
					
						
							|  |  |  | 						array_pop( $stack ); | 
					
						
							|  |  |  | 						$found = true; | 
					
						
							|  |  |  | 						break; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					if ( ! $found ) { | 
					
						
							|  |  |  | 						throw new Exception( 'Mismatched parentheses' ); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					$pos++; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Operators
 | 
					
						
							|  |  |  | 				case '|': | 
					
						
							|  |  |  | 				case '&': | 
					
						
							|  |  |  | 				case '>': | 
					
						
							|  |  |  | 				case '<': | 
					
						
							|  |  |  | 				case '!': | 
					
						
							|  |  |  | 				case '=': | 
					
						
							|  |  |  | 				case '%': | 
					
						
							|  |  |  | 				case '?': | 
					
						
							|  |  |  | 					$end_operator = strspn( $str, self::OP_CHARS, $pos ); | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$operator     = substr( $str, $pos, $end_operator ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					if ( ! array_key_exists( $operator, self::$op_precedence ) ) { | 
					
						
							|  |  |  | 						throw new Exception( sprintf( 'Unknown operator "%s"', $operator ) ); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					while ( ! empty( $stack ) ) { | 
					
						
							|  |  |  | 						$o2 = $stack[ count( $stack ) - 1 ]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// Ternary is right-associative in C
 | 
					
						
							|  |  |  | 						if ( $operator === '?:' || $operator === '?' ) { | 
					
						
							|  |  |  | 							if ( self::$op_precedence[ $operator ] >= self::$op_precedence[ $o2 ] ) { | 
					
						
							|  |  |  | 								break; | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						} elseif ( self::$op_precedence[ $operator ] > self::$op_precedence[ $o2 ] ) { | 
					
						
							|  |  |  | 							break; | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						$output[] = array( 'op', array_pop( $stack ) ); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					$stack[] = $operator; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					$pos += $end_operator; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Ternary "else"
 | 
					
						
							|  |  |  | 				case ':': | 
					
						
							|  |  |  | 					$found = false; | 
					
						
							|  |  |  | 					$s_pos = count( $stack ) - 1; | 
					
						
							|  |  |  | 					while ( $s_pos >= 0 ) { | 
					
						
							|  |  |  | 						$o2 = $stack[ $s_pos ]; | 
					
						
							|  |  |  | 						if ( $o2 !== '?' ) { | 
					
						
							|  |  |  | 							$output[] = array( 'op', array_pop( $stack ) ); | 
					
						
							|  |  |  | 							$s_pos--; | 
					
						
							|  |  |  | 							continue; | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// Replace.
 | 
					
						
							|  |  |  | 						$stack[ $s_pos ] = '?:'; | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 						$found           = true; | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 						break; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					if ( ! $found ) { | 
					
						
							|  |  |  | 						throw new Exception( 'Missing starting "?" ternary operator' ); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					$pos++; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// Default - number or invalid
 | 
					
						
							|  |  |  | 				default: | 
					
						
							|  |  |  | 					if ( $next >= '0' && $next <= '9' ) { | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 						$span     = strspn( $str, self::NUM_CHARS, $pos ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 						$output[] = array( 'value', intval( substr( $str, $pos, $span ) ) ); | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 						$pos     += $span; | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 						break; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					throw new Exception( sprintf( 'Unknown symbol "%s"', $next ) ); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		while ( ! empty( $stack ) ) { | 
					
						
							|  |  |  | 			$o2 = array_pop( $stack ); | 
					
						
							|  |  |  | 			if ( $o2 === '(' || $o2 === ')' ) { | 
					
						
							|  |  |  | 				throw new Exception( 'Mismatched parentheses' ); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			$output[] = array( 'op', $o2 ); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		$this->tokens = $output; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Get the plural form for a number. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * Caches the value for repeated calls. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @param int $num Number to get plural form for. | 
					
						
							|  |  |  | 	 * @return int Plural form value. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	public function get( $num ) { | 
					
						
							|  |  |  | 		if ( isset( $this->cache[ $num ] ) ) { | 
					
						
							|  |  |  | 			return $this->cache[ $num ]; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return $this->cache[ $num ] = $this->execute( $num ); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Execute the plural form function. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @since 4.9.0 | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @param int $n Variable "n" to substitute. | 
					
						
							|  |  |  | 	 * @return int Plural form value. | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	public function execute( $n ) { | 
					
						
							|  |  |  | 		$stack = array(); | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 		$i     = 0; | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 		$total = count( $this->tokens ); | 
					
						
							|  |  |  | 		while ( $i < $total ) { | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 			$next = $this->tokens[ $i ]; | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 			$i++; | 
					
						
							|  |  |  | 			if ( $next[0] === 'var' ) { | 
					
						
							|  |  |  | 				$stack[] = $n; | 
					
						
							|  |  |  | 				continue; | 
					
						
							|  |  |  | 			} elseif ( $next[0] === 'value' ) { | 
					
						
							|  |  |  | 				$stack[] = $next[1]; | 
					
						
							|  |  |  | 				continue; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// Only operators left.
 | 
					
						
							|  |  |  | 			switch ( $next[1] ) { | 
					
						
							|  |  |  | 				case '%': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 % $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '||': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 || $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '&&': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 && $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '<': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 < $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '<=': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 <= $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '>': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 > $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '>=': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 >= $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '!=': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 != $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '==': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 == $v2; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				case '?:': | 
					
						
							| 
									
										
										
										
											2019-04-16 20:56:22 +01:00
										 |  |  | 					$v3      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v2      = array_pop( $stack ); | 
					
						
							|  |  |  | 					$v1      = array_pop( $stack ); | 
					
						
							| 
									
										
										
										
											2019-03-12 09:27:46 +00:00
										 |  |  | 					$stack[] = $v1 ? $v2 : $v3; | 
					
						
							|  |  |  | 					break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				default: | 
					
						
							|  |  |  | 					throw new Exception( sprintf( 'Unknown operator "%s"', $next[1] ) ); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if ( count( $stack ) !== 1 ) { | 
					
						
							|  |  |  | 			throw new Exception( 'Too many values remaining on the stack' ); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return (int) $stack[0]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | } |