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

FormulasQuestion / moodle-qtype_formulas / 16524949567

25 Jul 2025 02:52PM UTC coverage: 97.565% (+0.02%) from 97.55%
16524949567

push

github

web-flow
allow large reservoirs for random variables (#241)

133 of 135 new or added lines in 5 files covered. (98.52%)

1 existing line in 1 file now uncovered.

4248 of 4354 relevant lines covered (97.57%)

1560.28 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

148
    /** @var int row in which the token starts */
149
    public int $row;
150

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

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

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

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

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

245
                if ($count > qtype_formulas::MAX_LIST_SIZE) {
105✔
246
                    throw new Exception(get_string('error_list_too_large', 'qtype_formulas', qtype_formulas::MAX_LIST_SIZE));
63✔
247
                }
248

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

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

292
        // If the token is a list or set, we have to unpack all elements separately and recursively.
293
        if (in_array($token->type, [self::LIST, self::SET])) {
63✔
294
            $result = [];
63✔
295
            foreach ($token->value as $value) {
63✔
296
                $result[] = self::unpack($value);
63✔
297
            }
298
        }
299
        return $result;
63✔
300
    }
301

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

322
    /**
323
     * Recursively count the tokens inside this token. This is useful for nested lists only.
324
     *
325
     * @param token $token the token to be counted
326
     * @return int
327
     */
328
    public static function recursive_count(token $token): int {
329
        $count = 0;
84✔
330

331
        // Literals consist of one single token.
332
        if (in_array($token->type, [self::NUMBER, self::STRING])) {
84✔
333
            return 1;
84✔
334
        }
335

336
        // For lists, we recursively count all tokens.
337
        if ($token->type === self::LIST) {
42✔
338
            $elements = $token->value;
42✔
339
            foreach ($elements as $element) {
42✔
340
                $count += self::recursive_count($element);
42✔
341
            }
342
        }
343

344
        return $count;
42✔
345
    }
346

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