|
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 } |