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