wp/wp-includes/pomo/plural-forms.php
changeset 7 cf61fcea0001
child 9 177826044cd9
equal deleted inserted replaced
6:490d5cc509ed 7:cf61fcea0001
       
     1 <?php
       
     2 
       
     3 /**
       
     4  * A gettext Plural-Forms parser.
       
     5  *
       
     6  * @since 4.9.0
       
     7  */
       
     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 	/**
       
    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 }