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