Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
2.60% |
2 / 77 |
|
25.00% |
1 / 4 |
CRAP | |
0.00% |
0 / 1 |
Evaluator | |
2.60% |
2 / 77 |
|
25.00% |
1 / 4 |
919.05 | |
0.00% |
0 / 1 |
evaluate | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
compile | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
evaluateCompiled | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
90 | |||
doOperation | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
380 |
1 | <?php |
2 | /** |
3 | * @author Tim Starling |
4 | * @author Niklas Laxström |
5 | * @license GPL-2.0-or-later |
6 | * @file |
7 | */ |
8 | |
9 | namespace CLDRPluralRuleParser; |
10 | |
11 | /** |
12 | * Parse and evaluate a plural rule. |
13 | * |
14 | * UTS #35 Revision 33 |
15 | * http://www.unicode.org/reports/tr35/tr35-33/tr35-numbers.html#Language_Plural_Rules |
16 | */ |
17 | class Evaluator { |
18 | /** |
19 | * Evaluate a number against a set of plural rules. If a rule passes, |
20 | * return the index of plural rule. |
21 | * |
22 | * @param int $number The number to be evaluated against the rules |
23 | * @param array $rules The associative array of plural rules in pluralform => rule format. |
24 | * @return int The index of the plural form which passed the evaluation |
25 | */ |
26 | public static function evaluate( $number, array $rules ) { |
27 | $rules = self::compile( $rules ); |
28 | |
29 | return self::evaluateCompiled( $number, $rules ); |
30 | } |
31 | |
32 | /** |
33 | * Convert a set of rules to a compiled form which is optimised for |
34 | * fast evaluation. The result will be an array of strings, and may be cached. |
35 | * |
36 | * @param array $rules The rules to compile |
37 | * @return array An array of compile rules. |
38 | */ |
39 | public static function compile( array $rules ): array { |
40 | // We can't use array_map() for this because it generates a warning if |
41 | // there is an exception. |
42 | foreach ( $rules as &$rule ) { |
43 | $rule = Converter::convert( $rule ); |
44 | } |
45 | |
46 | return $rules; |
47 | } |
48 | |
49 | /** |
50 | * Evaluate a compiled set of rules returned by compile(). Do not allow |
51 | * the user to edit the compiled form, or else PHP errors may result. |
52 | * |
53 | * @param string|int $number The number to be evaluated against the rules, in English, or it |
54 | * may be a type convertible to string. |
55 | * @param array $rules The associative array of plural rules in pluralform => rule format. |
56 | * @return int The index of the plural form which passed the evaluation |
57 | */ |
58 | public static function evaluateCompiled( $number, array $rules ): int { |
59 | // Calculate the values of the operand symbols |
60 | $number = strval( $number ); |
61 | if ( !preg_match( '/^ -? ( ([0-9]+) (?: \. ([0-9]+) )? )$/x', $number, $m ) ) { |
62 | return count( $rules ); |
63 | } |
64 | if ( !isset( $m[3] ) ) { |
65 | $operandSymbols = [ |
66 | 'n' => intval( $m[1] ), |
67 | 'i' => intval( $m[1] ), |
68 | 'v' => 0, |
69 | 'w' => 0, |
70 | 'f' => 0, |
71 | 't' => 0 |
72 | ]; |
73 | } else { |
74 | $absValStr = $m[1]; |
75 | $intStr = $m[2]; |
76 | $fracStr = $m[3]; |
77 | $operandSymbols = [ |
78 | 'n' => floatval( $absValStr ), |
79 | 'i' => intval( $intStr ), |
80 | 'v' => strlen( $fracStr ), |
81 | 'w' => strlen( rtrim( $fracStr, '0' ) ), |
82 | 'f' => intval( $fracStr ), |
83 | 't' => intval( rtrim( $fracStr, '0' ) ), |
84 | ]; |
85 | } |
86 | |
87 | // The compiled form is RPN, with tokens strictly delimited by |
88 | // spaces, so this is a simple RPN evaluator. |
89 | foreach ( $rules as $i => $rule ) { |
90 | $stack = []; |
91 | $zero = ord( '0' ); |
92 | $nine = ord( '9' ); |
93 | |
94 | foreach ( explode( ' ', $rule ) as $token ) { |
95 | $ord = ord( $token ); |
96 | if ( isset( $operandSymbols[$token] ) ) { |
97 | $stack[] = $operandSymbols[$token]; |
98 | } elseif ( $ord >= $zero && $ord <= $nine ) { |
99 | $stack[] = intval( $token ); |
100 | } else { |
101 | $right = array_pop( $stack ); |
102 | $left = array_pop( $stack ); |
103 | $result = self::doOperation( $token, $left, $right ); |
104 | $stack[] = $result; |
105 | } |
106 | } |
107 | if ( $stack[0] ) { |
108 | return $i; |
109 | } |
110 | } |
111 | // None of the provided rules match. The number belongs to category |
112 | // 'other', which comes last. |
113 | return count( $rules ); |
114 | } |
115 | |
116 | /** |
117 | * Do a single operation |
118 | * |
119 | * @param string $token The token string |
120 | * @param mixed $left The left operand. If it is an object, its state may be destroyed. |
121 | * @param mixed $right The right operand |
122 | * @throws Error |
123 | * @return mixed The operation result |
124 | */ |
125 | private static function doOperation( $token, $left, $right ) { |
126 | if ( in_array( $token, [ 'in', 'not-in', 'within', 'not-within' ] ) ) { |
127 | if ( !$right instanceof Range ) { |
128 | $right = new Range( $right ); |
129 | } |
130 | } |
131 | switch ( $token ) { |
132 | case 'or': |
133 | return $left || $right; |
134 | case 'and': |
135 | return $left && $right; |
136 | case 'is': |
137 | return $left == $right; |
138 | case 'is-not': |
139 | return $left != $right; |
140 | case 'in': |
141 | return $right->isNumberIn( $left ); |
142 | case 'not-in': |
143 | return !$right->isNumberIn( $left ); |
144 | case 'within': |
145 | return $right->isNumberWithin( $left ); |
146 | case 'not-within': |
147 | return !$right->isNumberWithin( $left ); |
148 | case 'mod': |
149 | if ( is_int( $left ) ) { |
150 | return (int)fmod( $left, $right ); |
151 | } |
152 | |
153 | return fmod( $left, $right ); |
154 | case ',': |
155 | if ( $left instanceof Range ) { |
156 | $range = $left; |
157 | } else { |
158 | $range = new Range( $left ); |
159 | } |
160 | $range->add( $right ); |
161 | |
162 | return $range; |
163 | case '..': |
164 | return new Range( $left, $right ); |
165 | default: |
166 | throw new Error( "Invalid RPN token" ); |
167 | } |
168 | } |
169 | } |