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

FormulasQuestion / moodle-qtype_formulas / 24044503888

06 Apr 2026 06:21PM UTC coverage: 97.22% (-0.3%) from 97.498%
24044503888

Pull #264

github

web-flow
Merge d552dab53 into f8206cd28
Pull Request #264: Allow parts to have empty fields

80 of 92 new or added lines in 11 files covered. (86.96%)

3 existing lines in 1 file now uncovered.

4652 of 4785 relevant lines covered (97.22%)

959.31 hits per line

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

96.39
/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

19
use Exception;
20
use qtype_formulas;
21

22
defined('MOODLE_INTERNAL') || die();
×
23

24
require_once($CFG->dirroot . '/question/type/formulas/questiontype.php');
×
25

26

27
/**
28
 * Class for individual tokens
29
 *
30
 * @package    qtype_formulas
31
 * @copyright  2022 Philipp Imhof
32
 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
33
 */
34
class token {
35
    /** @var int all literals (string or number) will have their 1-bit set */
36
    const ANY_LITERAL = 1;
37

38
    /** @var int used to designate a token storing a number */
39
    const NUMBER = 3;
40

41
    /** @var int used to designate a token storing a string literal */
42
    const STRING = 5;
43

44
    /** @var int used to designate the special token used for empty answers */
45
    const EMPTY = 7;
46

47
    /**
48
     * Parentheses are organised in groups, allowing for bitwise comparison.
49
     * examples: CLOSING_PAREN & ANY_PAREN = ANY_PAREN
50
     *           CLOSING_PAREN & ANY_CLOSING_PAREN = ANY_CLOSING_PAREN
51
     *           CLOSING_PAREN & OPEN_OR_CLOSE_PAREN = OPEN_OR_CLOSE_PAREN
52
     *           CLOSING_PAREN & CLOSING_BRACKET = ANY_PAREN | ANY_CLOSING_PAREN
53
     *           OPENING_* ^ CLOSING_COUNTER_PART = ANY_CLOSING_PAREN | ANY_OPENING_PAREN
54
     *
55
     *
56
     * @var int all parentheses have their 8-bit set
57
     **/
58
    const ANY_PAREN = 8;
59

60
    /** @var int all opening parentheses have their 16-bit set */
61
    const ANY_OPENING_PAREN = 16;
62

63
    /** @var int all closing parentheses have their 32-bit set */
64
    const ANY_CLOSING_PAREN = 32;
65

66
    /** @var int round opening or closing parens have their 64-bit set */
67
    const OPEN_OR_CLOSE_PAREN = 64;
68

69
    /** @var int opening or closing brackets have their 128-bit set */
70
    const OPEN_OR_CLOSE_BRACKET = 128;
71

72
    /** @var int opening or closing braces have their 256-bit set */
73
    const OPEN_OR_CLOSE_BRACE = 256;
74

75
    /** @var int an opening paren must be 8 (any paren) + 16 (opening) + 64 (round paren) = 88 */
76
    const OPENING_PAREN = 88;
77

78
    /** @var int a closing paren must be 8 (any paren) + 32 (closing) + 64 (round paren) = 104 */
79
    const CLOSING_PAREN = 104;
80

81
    /** @var int an opening bracket must be 8 (any paren) + 16 (opening) + 128 (bracket) = 152 */
82
    const OPENING_BRACKET = 152;
83

84
    /** @var int a closing bracket must be 8 (any paren) + 32 (closing) + 128 (bracket) = 168 */
85
    const CLOSING_BRACKET = 168;
86

87
    /** @var int an opening brace must be 8 (any paren) + 16 (opening) + 256 (brace) = 280 */
88
    const OPENING_BRACE = 280;
89

90
    /** @var int a closing brace must be 8 (any paren) + 32 (closing) + 256 (brace) = 296 */
91
    const CLOSING_BRACE = 296;
92

93
    /** @var int identifiers will have their 512-bit set */
94
    const IDENTIFIER = 512;
95

96
    /** @var int function tokens are 512 (identifier) + 1024 = 1536 */
97
    const FUNCTION = 1536;
98

99
    /** @var int variable tokens are 512 (identifier) + 2048 = 2560 */
100
    const VARIABLE = 2560;
101

102
    /** @var int used to designate a token storing the prefix operator */
103
    const PREFIX = 4096;
104

105
    /** @var int used to designate a token storing a constant */
106
    const CONSTANT = 8192;
107

108
    /** @var int used to designate a token storing an operator */
109
    const OPERATOR = 16384;
110

111
    /** @var int used to designate a token storing an argument separator (comma) */
112
    const ARG_SEPARATOR = 32768;
113

114
    /** @var int used to designate a token storing a range separator (colon) */
115
    const RANGE_SEPARATOR = 65536;
116

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

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

123
    /** @var int used to designate a token storing a list */
124
    const LIST = 524288;
125

126
    /** @var int used to designate a token storing a set */
127
    const SET = 1048576;
128

129
    /** @var int used to designate a token storing a range */
130
    const RANGE = 1572864;
131

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

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

138
    /** @var int used to designate a token storing a unit */
139
    const UNIT = 8388608;
140

141
    /** @var mixed the token's content, will be the name for identifiers */
142
    public $value;
143

144
    /** @var mixed additional information, e. g. the form how a number was entered */
145
    public $metadata;
146

147
    /** @var int token type, e.g. number or string */
148
    public int $type;
149

150
    /** @var int row in which the token starts */
151
    public int $row;
152

153
    /** @var int column in which the token starts */
154
    public int $column;
155

156
    /**
157
     * Constructor.
158
     *
159
     * @param int $type the type of the token
160
     * @param mixed $value the value (e.g. name of identifier, string content, number value, operator)
161
     * @param int $row row where the token starts in the input stream
162
     * @param int $column column where the token starts in the input stream
163
     * @param mixed $metadata additional information (e.g. the form how a number was entered)
164
     */
165
    public function __construct(int $type, $value, int $row = -1, int $column = -1, $metadata = null) {
166
        $this->value = $value;
165✔
167
        $this->metadata = $metadata;
165✔
168
        $this->type = $type;
165✔
169
        $this->row = $row;
165✔
170
        $this->column = $column;
165✔
171
    }
172

173
    /**
174
     * Convert token to a string.
175
     *
176
     * @return string
177
     */
178
    public function __toString() {
179
        // Arrays are printed in their [...] form, sets are printed as {...}.
180
        if (gettype($this->value) === 'array') {
154✔
181
            $result = self::stringify_array($this->value);
110✔
182
            if ($this->type === self::SET) {
110✔
183
                return '{' . substr($result, 1, -1) . '}';
44✔
184
            }
185
            return $result;
66✔
186
        }
187

188
        // For everything else, we use PHP's string conversion.
189
        return strval($this->value);
99✔
190
    }
191

192
    /**
193
     * Wrap a given value (e. g. a number) into a token. If no specific type is requested, the
194
     * token type will be derived from the value.
195
     *
196
     * @param mixed $value value to be wrapped
197
     * @param int $type if desired, type of the resulting token (use pre-defined constants)
198
     * @param int $carry intermediate count, useful when recursively wrapping arrays
199
     * @return token
200
     */
201
    public static function wrap($value, $type = null, $carry = 0): token {
202
        // If the value is already a token, we do nothing.
203
        if ($value instanceof token) {
220✔
204
            return $value;
33✔
205
        }
206
        // If a NUMBER token is requested, we check whether the value is numeric. If
207
        // it is, we convert it to float. Otherwise, we throw an error.
208
        if ($type == self::NUMBER) {
198✔
209
            if (!is_numeric(($value))) {
22✔
210
                throw new Exception(get_string('error_wrapnumber', 'qtype_formulas'));
11✔
211
            }
212
            $value = floatval($value);
11✔
213
        }
214
        // If a STRING token is requested, we make sure the value is a string. If that is not
215
        // possible, throw an error.
216
        if ($type == self::STRING) {
187✔
217
            try {
218
                // We do not allow implicit conversion of array to string.
219
                if (gettype($value) === 'array') {
22✔
220
                    throw new Exception(get_string('error_wrapstring', 'qtype_formulas'));
11✔
221
                }
222
                $value = strval($value);
11✔
223
            } catch (Exception $e) {
11✔
224
                throw new Exception(get_string('error_wrapstring', 'qtype_formulas'));
11✔
225
            }
226
        }
227
        // If a specific type is requested, we return a token with that type.
228
        if ($type !== null) {
176✔
229
            return new token($type, $value);
22✔
230
        }
231
        // Otherwise, we choose the appropriate type ourselves.
232
        if (is_string($value)) {
154✔
233
            $type = self::STRING;
22✔
234
        } else if (is_float($value) || is_int($value)) {
132✔
235
            $type = self::NUMBER;
77✔
236
        } else if (is_array($value)) {
99✔
237
            $type = self::LIST;
55✔
238
            $count = $carry;
55✔
239
            // Values must be wrapped recursively.
240
            foreach ($value as &$val) {
55✔
241
                if (is_array($val)) {
55✔
242
                    $count += count($val);
33✔
243
                } else {
244
                    $count++;
55✔
245
                }
246

247
                if ($count > qtype_formulas::MAX_LIST_SIZE) {
55✔
248
                    throw new Exception(get_string('error_list_too_large', 'qtype_formulas', qtype_formulas::MAX_LIST_SIZE));
33✔
249
                }
250

251
                $val = self::wrap($val, null, $count);
55✔
252
            }
253
        } else if (is_bool($value)) {
44✔
254
            // Some PHP functions (e. g. is_nan and similar) will return a boolean value. For backwards
255
            // compatibility, we will convert this into a number with TRUE = 1 and FALSE = 0.
256
            $type = self::NUMBER;
22✔
257
            $value = ($value ? 1 : 0);
22✔
258
        } else if ($value instanceof lazylist) {
22✔
259
            $type = self::SET;
11✔
260
        } else {
261
            if (is_null($value)) {
11✔
262
                $value = 'null';
11✔
263
            }
264
            throw new Exception(get_string('error_tokenconversion', 'qtype_formulas', $value));
11✔
265
        }
266
        return new token($type, $value);
143✔
267
    }
268

269
    /**
270
     * Extract the value from a token.
271
     *
272
     * @param token $token
273
     * @return mixed
274
     */
275
    public static function unpack($token) {
276
        // For convenience, we also accept elementary types instead of tokens, e.g. literals.
277
        // In that case, we have nothing to do, we just return the value. Unless it is an
278
        // array, because those might contain tokens.
279
        if (!($token instanceof token)) {
165✔
280
            if (is_array($token)) {
77✔
281
                $result = [];
11✔
282
                foreach ($token as $value) {
11✔
283
                    $result[] = self::unpack($value);
11✔
284
                }
285
                return $result;
11✔
286
            }
287
            return $token;
77✔
288
        }
289
        // If the token value is a literal (number or string), return it directly.
290
        if (in_array($token->type, [self::NUMBER, self::STRING])) {
88✔
291
            return $token->value;
88✔
292
        }
293
        // If the token is the $EMPTY token, return the string '$EMPTY'.
294
        if ($token->type === self::EMPTY) {
33✔
NEW
295
            return '$EMPTY';
×
296
        }
297

298
        // If the token is a list or set, we have to unpack all elements separately and recursively.
299
        if (in_array($token->type, [self::LIST, self::SET])) {
33✔
300
            $result = [];
33✔
301
            foreach ($token->value as $value) {
33✔
302
                $result[] = self::unpack($value);
33✔
303
            }
304
        }
305
        return $result;
33✔
306
    }
307

308
    /**
309
     * Recursively convert an array to a string.
310
     *
311
     * @param array $arr the array to be converted
312
     */
313
    public static function stringify_array($arr): string {
314
        $result = '[';
110✔
315
        foreach ($arr as $element) {
110✔
316
            if (gettype($element) === 'array') {
110✔
317
                $result .= self::stringify_array($element);
44✔
318
            } else {
319
                $result .= strval($element);
110✔
320
            }
321
            $result .= ', ';
110✔
322
        }
323
        $result .= ']';
110✔
324
        $result = str_replace(', ]', ']', $result);
110✔
325
        return $result;
110✔
326
    }
327

328
    /**
329
     * Recursively count the tokens inside this token. This is useful for nested lists only.
330
     *
331
     * @param token $token the token to be counted
332
     * @return int
333
     */
334
    public static function recursive_count(token $token): int {
335
        $count = 0;
44✔
336

337
        // Literals consist of one single token.
338
        if (in_array($token->type, [self::NUMBER, self::STRING])) {
44✔
339
            return 1;
44✔
340
        }
341

342
        // For lists, we recursively count all tokens.
343
        if ($token->type === self::LIST) {
22✔
344
            $elements = $token->value;
22✔
345
            foreach ($elements as $element) {
22✔
346
                $count += self::recursive_count($element);
22✔
347
            }
348
        }
349

350
        return $count;
22✔
351
    }
352
}
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

© 2026 Coveralls, Inc