• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

FormulasQuestion / moodle-qtype_formulas / 13217446514

08 Feb 2025 04:37PM UTC coverage: 76.899% (+1.9%) from 75.045%
13217446514

Pull #62

github

web-flow
Merge b36f9931f into acd272945
Pull Request #62: Rewrite the parser

2547 of 3139 new or added lines in 22 files covered. (81.14%)

146 existing lines in 6 files now uncovered.

3006 of 3909 relevant lines covered (76.9%)

438.31 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.19
/classes/local/token.php
1
<?php
2
// This file is part of Moodle - https://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <https://www.gnu.org/licenses/>.
16

17
namespace qtype_formulas\local;
18
use Exception;
19

20
/**
21
 * Class for individual tokens
22
 *
23
 * @package    qtype_formulas
24
 * @copyright  2022 Philipp Imhof
25
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27
class token {
28

29
    /** @var int all literals (string or number) will have their 1-bit set */
30
    const ANY_LITERAL = 1;
31

32
    /** @var int used to designate a token storing a number */
33
    const NUMBER = 3;
34

35
    /** @var int used to designate a token storing a string literal */
36
    const STRING = 5;
37

38
    /**
39
     * Parentheses are organised in groups, allowing for bitwise comparison.
40
     * examples: CLOSING_PAREN & ANY_PAREN = ANY_PAREN
41
     *           CLOSING_PAREN & ANY_CLOSING_PAREN = ANY_CLOSING_PAREN
42
     *           CLOSING_PAREN & OPEN_OR_CLOSE_PAREN = OPEN_OR_CLOSE_PAREN
43
     *           CLOSING_PAREN & CLOSING_BRACKET = ANY_PAREN | ANY_CLOSING_PAREN
44
     *           OPENING_* ^ CLOSING_COUNTER_PART = ANY_CLOSING_PAREN | ANY_OPENING_PAREN
45
     *
46
     *
47
     * @var int all parentheses have their 8-bit set
48
     **/
49
    const ANY_PAREN = 8;
50

51
    /** @var int all opening parentheses have their 16-bit set */
52
    const ANY_OPENING_PAREN = 16;
53

54
    /** @var int all closing parentheses have their 32-bit set */
55
    const ANY_CLOSING_PAREN = 32;
56

57
    /** @var int round opening or closing parens have their 64-bit set */
58
    const OPEN_OR_CLOSE_PAREN = 64;
59

60
    /** @var int opening or closing brackets have their 128-bit set */
61
    const OPEN_OR_CLOSE_BRACKET = 128;
62

63
    /** @var int opening or closing braces have their 256-bit set */
64
    const OPEN_OR_CLOSE_BRACE = 256;
65

66
    /** @var int an opening paren must be 8 (any paren) + 16 (opening) + 64 (round paren) = 88 */
67
    const OPENING_PAREN = 88;
68

69
    /** @var int a closing paren must be 8 (any paren) + 32 (closing) + 64 (round paren) = 104 */
70
    const CLOSING_PAREN = 104;
71

72
    /** @var int an opening bracket must be 8 (any paren) + 16 (opening) + 128 (bracket) = 152 */
73
    const OPENING_BRACKET = 152;
74

75
    /** @var int a closing bracket must be 8 (any paren) + 32 (closing) + 128 (bracket) = 168 */
76
    const CLOSING_BRACKET = 168;
77

78
    /** @var int an opening brace must be 8 (any paren) + 16 (opening) + 256 (brace) = 280 */
79
    const OPENING_BRACE = 280;
80

81
    /** @var int a closing brace must be 8 (any paren) + 32 (closing) + 256 (brace) = 296 */
82
    const CLOSING_BRACE = 296;
83

84
    /** @var int identifiers will have their 512-bit set */
85
    const IDENTIFIER = 512;
86

87
    /** @var int function tokens are 512 (identifier) + 1024 = 1536 */
88
    const FUNCTION = 1536;
89

90
    /** @var int variable tokens are 512 (identifier) + 2048 = 2560 */
91
    const VARIABLE = 2560;
92

93
    /** @var int used to designate a token storing the prefix operator */
94
    const PREFIX = 4096;
95

96
    /** @var int used to designate a token storing a constant */
97
    const CONSTANT = 8192;
98

99
    /** @var int used to designate a token storing an operator */
100
    const OPERATOR = 16384;
101

102
    /** @var int used to designate a token storing an argument separator (comma) */
103
    const ARG_SEPARATOR = 32768;
104

105
    /** @var int used to designate a token storing a range separator (colon) */
106
    const RANGE_SEPARATOR = 65536;
107

108
    /** @var int used to designate a token storing an end-of-statement marker (semicolon) */
109
    const END_OF_STATEMENT = 131072;
110

111
    /** @var int used to designate a token storing a reserved word (e. g. for) */
112
    const RESERVED_WORD = 262144;
113

114
    /** @var int used to designate a token storing a list */
115
    const LIST = 524288;
116

117
    /** @var int used to designate a token storing a set */
118
    const SET = 1048576;
119

120
    /** @var int used to designate a token storing a start-of-group marker (opening brace) */
121
    const START_GROUP = 2097152;
122

123
    /** @var int used to designate a token storing an end-of-group marker (closing brace) */
124
    const END_GROUP = 4194304;
125

126
    /** @var mixed the token's content, will be the name for identifiers */
127
    public $value;
128

129
    /** @var int token type, e.g. number or string */
130
    public int $type;
131

132
    /** @var int row in which the token starts */
133
    public int $row;
134

135
    /** @var int column in which the token starts */
136
    public int $column;
137

138
    /**
139
     * Constructor.
140
     *
141
     * @param int $type the type of the token
142
     * @param mixed $value the value (e.g. name of identifier, string content, number value, operator)
143
     * @param int $row row where the token starts in the input stream
144
     * @param int $column column where the token starts in the input stream
145
     */
146
    public function __construct(int $type, $value, int $row = -1, int $column = -1) {
147
        $this->value = $value;
153✔
148
        $this->type = $type;
153✔
149
        $this->row = $row;
153✔
150
        $this->column = $column;
153✔
151
    }
152

153
    /**
154
     * Convert token to a string.
155
     *
156
     * @return string
157
     */
158
    public function __toString() {
159
        // Arrays are printed in their [...] form, sets are printed as {...}.
160
        if (gettype($this->value) === 'array') {
238✔
161
            $result = self::stringify_array($this->value);
170✔
162
            if ($this->type === self::SET) {
170✔
163
                return '{' . substr($result, 1, -1) . '}';
68✔
164
            }
165
            return $result;
102✔
166
        }
167

168
        // For everything else, we use PHP's string conversion.
169
        return strval($this->value);
153✔
170
    }
171

172
    /**
173
     * Wrap a given value (e. g. a number) into a token. If no specific type is requested, the
174
     * token type will be derived from the value.
175
     *
176
     * @param mixed $value value to be wrapped
177
     * @param int $type if desired, type of the resulting token (use pre-defined constants)
178
     * @return token
179
     */
180
    public static function wrap($value, $type = null): token {
181
        // If the value is already a token, we do nothing.
182
        if ($value instanceof token) {
238✔
183
            return $value;
51✔
184
        }
185
        // If a NUMBER token is requested, we check whether the value is numeric. If
186
        // it is, we convert it to float. Otherwise, we throw an error.
187
        if ($type == self::NUMBER) {
204✔
188
            if (!is_numeric(($value))) {
34✔
189
                throw new Exception(get_string('error_wrapnumber', 'qtype_formulas'));
17✔
190
            }
191
            $value = floatval($value);
17✔
192
        }
193
        // If a STRING token is requested, we make sure the value is a string. If that is not
194
        // possible, throw an error.
195
        if ($type == self::STRING) {
187✔
196
            try {
197
                // We do not allow implicit conversion of array to string.
198
                if (gettype($value) === 'array') {
34✔
199
                    throw new Exception(get_string('error_wrapstring', 'qtype_formulas'));
17✔
200
                }
201
                $value = strval($value);
17✔
202
            } catch (Exception $e) {
17✔
203
                throw new Exception(get_string('error_wrapstring', 'qtype_formulas'));
17✔
204
            }
205
        }
206
        // If a specific type is requested, we return a token with that type.
207
        if ($type !== null) {
170✔
208
            return new token($type, $value);
34✔
209
        }
210
        // Otherwise, we choose the appropriate type ourselves.
211
        if (is_string($value)) {
136✔
212
            $type = self::STRING;
34✔
213
        } else if (is_float($value) || is_int($value)) {
102✔
214
            $type = self::NUMBER;
68✔
215
        } else if (is_array($value)) {
51✔
216
            $type = self::LIST;
34✔
217
            // Values must be wrapped recursively.
218
            foreach ($value as &$val) {
34✔
219
                $val = self::wrap($val);
34✔
220
            }
221
        } else if (is_bool($value)) {
17✔
222
            // Some PHP functions (e. g. is_nan and similar) will return a boolean value. For backwards
223
            // compatibility, we will convert this into a number with TRUE = 1 and FALSE = 0.
NEW
224
            $type = self::NUMBER;
×
NEW
225
            $value = ($value ? 1 : 0);
×
226
        } else {
227
            if (is_null($value)) {
17✔
228
                $value = 'null';
17✔
229
            }
230
            throw new Exception(get_string('error_tokenconversion', 'qtype_formulas', $value));
17✔
231
        }
232
        return new token($type, $value);
119✔
233
    }
234

235
    /**
236
     * Extract the value from a token.
237
     *
238
     * @param token $token
239
     * @return mixed
240
     */
241
    public static function unpack($token) {
242
        // For convenience, we also accept elementary types instead of tokens, e.g. literals.
243
        // In that case, we have nothing to do, we just return the value. Unless it is an
244
        // array, because those might contain tokens.
NEW
245
        if (!($token instanceof token)) {
×
NEW
246
            if (is_array($token)) {
×
NEW
247
                $result = [];
×
NEW
248
                foreach ($token as $value) {
×
NEW
249
                    $result[] = self::unpack($value);
×
250
                }
NEW
251
                return $result;
×
252
            }
NEW
253
            return $token;
×
254
        }
255
        // If the token value is a literal (number or string), return it directly.
NEW
256
        if (in_array($token->type, [self::NUMBER, self::STRING])) {
×
NEW
257
            return $token->value;
×
258
        }
259

260
        // If the token is a list or set, we have to unpack all elements separately and recursively.
NEW
261
        if (in_array($token->type, [self::LIST, self::SET])) {
×
NEW
262
            $result = [];
×
NEW
263
            foreach ($token->value as $value) {
×
NEW
264
                $result[] = self::unpack($value);
×
265
            }
266
        }
NEW
267
        return $result;
×
268
    }
269

270
    /**
271
     * Recursively convert an array to a string.
272
     *
273
     * @param array $arr the array to be converted
274
     */
275
    public static function stringify_array($arr): string {
276
        $result = '[';
170✔
277
        foreach ($arr as $element) {
170✔
278
            if (gettype($element) === 'array') {
170✔
279
                $result .= self::stringify_array($element);
68✔
280
            } else {
281
                $result .= strval($element);
170✔
282
            }
283
            $result .= ', ';
170✔
284
        }
285
        $result .= ']';
170✔
286
        $result = str_replace(', ]', ']', $result);
170✔
287
        return $result;
170✔
288
    }
289

290
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc