ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

plural-forms.php (7612B)


      1 <?php
      2 
      3 /**
      4  * A gettext Plural-Forms parser.
      5  *
      6  * @since 4.9.0
      7  */
      8 if ( ! class_exists( 'Plural_Forms', false ) ) :
      9 	class Plural_Forms {
     10 		/**
     11 		 * Operator characters.
     12 		 *
     13 		 * @since 4.9.0
     14 		 * @var string OP_CHARS Operator characters.
     15 		 */
     16 		const OP_CHARS = '|&><!=%?:';
     17 
     18 		/**
     19 		 * Valid number characters.
     20 		 *
     21 		 * @since 4.9.0
     22 		 * @var string NUM_CHARS Valid number characters.
     23 		 */
     24 		const NUM_CHARS = '0123456789';
     25 
     26 		/**
     27 		 * Operator precedence.
     28 		 *
     29 		 * Operator precedence from highest to lowest. Higher numbers indicate
     30 		 * higher precedence, and are executed first.
     31 		 *
     32 		 * @see https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence
     33 		 *
     34 		 * @since 4.9.0
     35 		 * @var array $op_precedence Operator precedence from highest to lowest.
     36 		 */
     37 		protected static $op_precedence = array(
     38 			'%'  => 6,
     39 
     40 			'<'  => 5,
     41 			'<=' => 5,
     42 			'>'  => 5,
     43 			'>=' => 5,
     44 
     45 			'==' => 4,
     46 			'!=' => 4,
     47 
     48 			'&&' => 3,
     49 
     50 			'||' => 2,
     51 
     52 			'?:' => 1,
     53 			'?'  => 1,
     54 
     55 			'('  => 0,
     56 			')'  => 0,
     57 		);
     58 
     59 		/**
     60 		 * Tokens generated from the string.
     61 		 *
     62 		 * @since 4.9.0
     63 		 * @var array $tokens List of tokens.
     64 		 */
     65 		protected $tokens = array();
     66 
     67 		/**
     68 		 * Cache for repeated calls to the function.
     69 		 *
     70 		 * @since 4.9.0
     71 		 * @var array $cache Map of $n => $result
     72 		 */
     73 		protected $cache = array();
     74 
     75 		/**
     76 		 * Constructor.
     77 		 *
     78 		 * @since 4.9.0
     79 		 *
     80 		 * @param string $str Plural function (just the bit after `plural=` from Plural-Forms)
     81 		 */
     82 		public function __construct( $str ) {
     83 			$this->parse( $str );
     84 		}
     85 
     86 		/**
     87 		 * Parse a Plural-Forms string into tokens.
     88 		 *
     89 		 * Uses the shunting-yard algorithm to convert the string to Reverse Polish
     90 		 * Notation tokens.
     91 		 *
     92 		 * @since 4.9.0
     93 		 *
     94 		 * @throws Exception If there is a syntax or parsing error with the string.
     95 		 *
     96 		 * @param string $str String to parse.
     97 		 */
     98 		protected function parse( $str ) {
     99 			$pos = 0;
    100 			$len = strlen( $str );
    101 
    102 			// Convert infix operators to postfix using the shunting-yard algorithm.
    103 			$output = array();
    104 			$stack  = array();
    105 			while ( $pos < $len ) {
    106 				$next = substr( $str, $pos, 1 );
    107 
    108 				switch ( $next ) {
    109 					// Ignore whitespace.
    110 					case ' ':
    111 					case "\t":
    112 						$pos++;
    113 						break;
    114 
    115 					// Variable (n).
    116 					case 'n':
    117 						$output[] = array( 'var' );
    118 						$pos++;
    119 						break;
    120 
    121 					// Parentheses.
    122 					case '(':
    123 						$stack[] = $next;
    124 						$pos++;
    125 						break;
    126 
    127 					case ')':
    128 						$found = false;
    129 						while ( ! empty( $stack ) ) {
    130 							$o2 = $stack[ count( $stack ) - 1 ];
    131 							if ( '(' !== $o2 ) {
    132 								$output[] = array( 'op', array_pop( $stack ) );
    133 								continue;
    134 							}
    135 
    136 							// Discard open paren.
    137 							array_pop( $stack );
    138 							$found = true;
    139 							break;
    140 						}
    141 
    142 						if ( ! $found ) {
    143 							throw new Exception( 'Mismatched parentheses' );
    144 						}
    145 
    146 						$pos++;
    147 						break;
    148 
    149 					// Operators.
    150 					case '|':
    151 					case '&':
    152 					case '>':
    153 					case '<':
    154 					case '!':
    155 					case '=':
    156 					case '%':
    157 					case '?':
    158 						$end_operator = strspn( $str, self::OP_CHARS, $pos );
    159 						$operator     = substr( $str, $pos, $end_operator );
    160 						if ( ! array_key_exists( $operator, self::$op_precedence ) ) {
    161 							throw new Exception( sprintf( 'Unknown operator "%s"', $operator ) );
    162 						}
    163 
    164 						while ( ! empty( $stack ) ) {
    165 							$o2 = $stack[ count( $stack ) - 1 ];
    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 ] ) {
    173 								break;
    174 							}
    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;
    198 							break;
    199 						}
    200 
    201 						if ( ! $found ) {
    202 							throw new Exception( 'Missing starting "?" ternary operator' );
    203 						}
    204 						$pos++;
    205 						break;
    206 
    207 					// Default - number or invalid.
    208 					default:
    209 						if ( $next >= '0' && $next <= '9' ) {
    210 							$span     = strspn( $str, self::NUM_CHARS, $pos );
    211 							$output[] = array( 'value', intval( substr( $str, $pos, $span ) ) );
    212 							$pos     += $span;
    213 							break;
    214 						}
    215 
    216 						throw new Exception( sprintf( 'Unknown symbol "%s"', $next ) );
    217 				}
    218 			}
    219 
    220 			while ( ! empty( $stack ) ) {
    221 				$o2 = array_pop( $stack );
    222 				if ( '(' === $o2 || ')' === $o2 ) {
    223 					throw new Exception( 'Mismatched parentheses' );
    224 				}
    225 
    226 				$output[] = array( 'op', $o2 );
    227 			}
    228 
    229 			$this->tokens = $output;
    230 		}
    231 
    232 		/**
    233 		 * Get the plural form for a number.
    234 		 *
    235 		 * Caches the value for repeated calls.
    236 		 *
    237 		 * @since 4.9.0
    238 		 *
    239 		 * @param int $num Number to get plural form for.
    240 		 * @return int Plural form value.
    241 		 */
    242 		public function get( $num ) {
    243 			if ( isset( $this->cache[ $num ] ) ) {
    244 				return $this->cache[ $num ];
    245 			}
    246 			$this->cache[ $num ] = $this->execute( $num );
    247 			return $this->cache[ $num ];
    248 		}
    249 
    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 		}
    349 	}
    350 endif;