Passed
Push — 6.0 ( 4ac4e1...87e1d7 )
by Olivier
01:56
created

Relation   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 157
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 51
dl 0
loc 157
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A from() 0 5 1
A precision_from() 0 3 1
A parse_x_expression() 0 25 4
A parse_relation() 0 13 3
A resolve_x() 0 21 2
A evaluate() 0 10 2
A parse_range_list() 0 15 3
A __construct() 0 4 1
A unwind_range() 0 18 3
1
<?php
2
3
namespace ICanBoogie\CLDR\Supplemental\Plurals;
4
5
use ICanBoogie\CLDR\Numbers\Number;
6
7
use function array_merge;
8
use function explode;
9
use function extract;
10
use function implode;
11
use function is_numeric;
12
use function is_string;
13
use function rtrim;
14
use function str_repeat;
15
use function strlen;
16
use function strpos;
17
use function substr;
18
use function trim;
19
20
/**
21
 * Representation of a plural rule relation.
22
 *
23
 * @internal
24
 *
25
 * @link https://www.unicode.org/reports/tr35/tr35-72/tr35-numbers.html#Relations
26
 */
27
final class Relation
28
{
29
    public const RANGE_SEPARATOR = '..';
30
    public const MODULUS_SIGN = '%';
31
32
    public static function from(string $relation): Relation
33
    {
34
        return RelationCache::get(
35
            $relation,
36
            static fn(): Relation => new self(...self::parse_relation($relation))
37
        );
38
    }
39
40
    /**
41
     * @return array{ 0: string|null, 1: string }
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
42
     *     Where `0` is the x_expressions and `1` the range.
43
     */
44
    private static function parse_relation(string $relation): array
45
    {
46
        [ $x_expression, $range_list ] = explode('= ', $relation) + [ 1 => null ];
47
        assert(is_string($x_expression));
48
        [ $x_expression, $negative ] = self::parse_x_expression($x_expression);
49
50
        $range = $range_list ? self::parse_range_list($range_list) : '($x == 0)';
51
52
        if ($negative) {
53
            $range = "!($range)";
54
        }
55
56
        return [ $x_expression, $range ];
57
    }
58
59
    /**
60
     * @return array{ 0: string|null, 1: bool}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{ at position 2 could not be parsed: the token is null at position 2.
Loading history...
61
     *     Where `0` is the x_expressions and `1` whether it's negative.
62
     */
63
    private static function parse_x_expression(string $x_expression): array
64
    {
65
        if (!$x_expression) {
66
            return [ null, false ];
67
        }
68
69
        $negative = false;
70
71
        if ($x_expression[strlen($x_expression) - 1] === '!') {
72
            $negative = true;
73
74
            $x_expression = substr($x_expression, 0, -1);
75
        }
76
77
        $x_expression = '$' . rtrim($x_expression);
78
79
        if (str_contains($x_expression, self::MODULUS_SIGN)) {
80
            [ $operand, $modulus ] = explode(self::MODULUS_SIGN, $x_expression);
81
82
            $operand = trim($operand);
83
84
            $x_expression = "fmod($operand, $modulus)";
85
        }
86
87
        return [ $x_expression, $negative ];
88
    }
89
90
    /**
91
     * @return string A PHP statement.
92
     */
93
    private static function parse_range_list(string $range_list): string
94
    {
95
        $ranges = [];
96
97
        foreach (explode(',', $range_list) as $range) {
98
            if (strpos($range, self::RANGE_SEPARATOR)) {
99
                $ranges = array_merge($ranges, self::unwind_range($range));
100
101
                continue;
102
            }
103
104
            $ranges[] = "(\$x == $range)";
105
        }
106
107
        return implode(' || ', $ranges);
108
    }
109
110
    /**
111
     * @return string[]
112
     *     Where _value_ is PHP code to evaluate.
113
     */
114
    private static function unwind_range(string $range): array
115
    {
116
        [ $start, $end ] = explode(self::RANGE_SEPARATOR, $range);
117
118
        assert(is_numeric($start));
119
        assert(is_numeric($end));
120
121
        $precision = self::precision_from($start) ?: self::precision_from($end);
122
        $step = 1 / (int)('1' . str_repeat('0', $precision));
123
        $end = (int)$end + $step;
124
125
        $ranges = [];
126
127
        for (; $start < $end; $start += $step) {
128
            $ranges[] = "(\$x == $start)";
129
        }
130
131
        return $ranges;
132
    }
133
134
    /**
135
     * @param float|int|numeric-string $number
0 ignored issues
show
Documentation Bug introduced by
The doc comment float|int|numeric-string at position 4 could not be parsed: Unknown type name 'numeric-string' at position 4 in float|int|numeric-string.
Loading history...
136
     */
137
    private static function precision_from(float|int|string $number): int
138
    {
139
        return Number::precision_from($number);
140
    }
141
142
    private function __construct(
143
        private readonly ?string $x_expression,
144
        private readonly string $conditions
145
    ) {
146
    }
147
148
    public function resolve_x(Operands $operands): float|int|null
149
    {
150
        if ($this->x_expression === null) {
151
            return null;
152
        }
153
154
        $operands = $operands->to_array();
155
156
        extract($operands);
157
158
        /**
159
         * @var float|int $n
160
         * @var int $i
161
         * @var int $v
162
         * @var int $w
163
         * @var int $f
164
         * @var int $t
165
         * @var int $e
166
         */
167
168
        return eval("return ($this->x_expression);");
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
169
    }
170
171
    /**
172
     * Evaluate operands
173
     */
174
    public function evaluate(Operands $operands): bool
175
    {
176
        if ($this->x_expression === null) {
177
            return true;
178
        }
179
180
        // $x is typecasted as a string because `fmod(4.3, 3) != 1.3` BUT `(string) fmod(4.3, 3) == 1.3
181
        $x = (string)$this->resolve_x($operands);
0 ignored issues
show
Unused Code introduced by
The assignment to $x is dead and can be removed.
Loading history...
182
183
        return eval("return $this->conditions;");
0 ignored issues
show
introduced by
The use of eval() is discouraged.
Loading history...
184
    }
185
}
186