Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
2.60% covered (danger)
2.60%
2 / 77
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Evaluator
2.60% covered (danger)
2.60%
2 / 77
25.00% covered (danger)
25.00%
1 / 4
919.05
0.00% covered (danger)
0.00%
0 / 1
 evaluate
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 compile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 evaluateCompiled
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
90
 doOperation
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
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
9namespace 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 */
17class 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}