| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 | <?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(		'%'  => 6,		'<'  => 5,		'<=' => 5,		'>'  => 5,		'>=' => 5,		'==' => 4,		'!=' => 4,		'&&' => 3,		'||' => 2,		'?:' => 1,		'?'  => 1,		'('  => 0,		')'  => 0,	);	/**	 * 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();		$stack  = array();		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 );					$operator     = substr( $str, $pos, $end_operator );					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 ] = '?:';						$found           = true;						break;					}					if ( ! $found ) {						throw new Exception( 'Missing starting "?" ternary operator' );					}					$pos++;					break;				// Default - number or invalid				default:					if ( $next >= '0' && $next <= '9' ) {						$span     = strspn( $str, self::NUM_CHARS, $pos );						$output[] = array( 'value', intval( substr( $str, $pos, $span ) ) );						$pos     += $span;						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 ];		}		$this->cache[ $num ] = $this->execute( $num );		return $this->cache[ $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();		$i     = 0;		$total = count( $this->tokens );		while ( $i < $total ) {			$next = $this->tokens[ $i ];			$i++;			if ( $next[0] === 'var' ) {				$stack[] = $n;				continue;			} elseif ( $next[0] === 'value' ) {				$stack[] = $next[1];				continue;			}			// Only operators left.			switch ( $next[1] ) {				case '%':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 % $v2;					break;				case '||':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 || $v2;					break;				case '&&':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 && $v2;					break;				case '<':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 < $v2;					break;				case '<=':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 <= $v2;					break;				case '>':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 > $v2;					break;				case '>=':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 >= $v2;					break;				case '!=':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 != $v2;					break;				case '==':					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$stack[] = $v1 == $v2;					break;				case '?:':					$v3      = array_pop( $stack );					$v2      = array_pop( $stack );					$v1      = array_pop( $stack );					$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];	}}
 |