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

FormulasQuestion / moodle-qtype_formulas / 17019138792

17 Aug 2025 09:01AM UTC coverage: 97.399% (-0.2%) from 97.629%
17019138792

Pull #264

github

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

78 of 92 new or added lines in 10 files covered. (84.78%)

12 existing lines in 5 files now uncovered.

4381 of 4498 relevant lines covered (97.4%)

1618.81 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

36
    /** @var int all literals (string or number) will have their 1-bit set */
37
    const ANY_LITERAL = 1;
38

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

248
                if ($count > qtype_formulas::MAX_LIST_SIZE) {
105✔
249
                    throw new Exception(get_string('error_list_too_large', 'qtype_formulas', qtype_formulas::MAX_LIST_SIZE));
63✔
250
                }
251

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

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

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

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

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

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

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

351
        return $count;
42✔
352
    }
353

354
}
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