mathjson-solver 1.7.0__tar.gz → 1.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mathjson-solver
3
- Version: 1.7.0
3
+ Version: 1.9.0
4
4
  Summary: Utilities for MathJSON evaluation
5
5
  Home-page: https://github.com/LongenesisLtd/mathjson-solver
6
6
  Author: Martins Mednis
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = mathjson-solver
3
- version = 1.7.0
3
+ version = 1.9.0
4
4
  author = Martins Mednis
5
5
  author_email = mrt@mednis.info
6
6
  description = Utilities for MathJSON evaluation
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="mathjson-solver",
8
- version="1.7.0",
8
+ version="1.9.0",
9
9
  author="Martins Mednis",
10
10
  author_email="mrt@mednis.info",
11
11
  description="Utilities for MathJSON evaluation",
@@ -1,2 +1,2 @@
1
1
  from .__main__ import create_mathjson_solver as create_solver
2
- from .__main__ import MathJSONException
2
+ from .__main__ import MathJSONException, extract_variables
@@ -0,0 +1,495 @@
1
+ import numbers
2
+ from typing import Union
3
+ from functools import reduce
4
+ import math
5
+ from copy import deepcopy
6
+ from statistics import median
7
+ import logging
8
+ import traceback
9
+
10
+
11
+ class MathJSONException(Exception):
12
+ """Exception for MathJSON processing issues"""
13
+
14
+ def __init__(self, e, expr, *args, **kwargs):
15
+ super().__init__(args)
16
+ self.e = e
17
+ self.expr = expr
18
+ self.construct = kwargs.get("mathjson_construct", "MathJSON")
19
+
20
+ def __str__(self):
21
+ if hasattr(self.e, "message"):
22
+ m = self.e.message
23
+ else:
24
+ m = str(self.e)
25
+ return f"Problem in {self.construct}. {self.expr}. {m}"
26
+
27
+
28
+ def requires_array(func):
29
+ def inner1(*args, **kwargs):
30
+ try:
31
+ if args[0][1][0] == "Array" and len(args[0][1]) > 0:
32
+ return func(*args, **kwargs)
33
+ else:
34
+ raise ValueError(f"'{func.__name__}' should receive a list")
35
+ except TypeError:
36
+ raise ValueError(f"'{func.__name__}' really should receive a list")
37
+
38
+ return inner1
39
+
40
+
41
+ def is_numeric(x):
42
+ try:
43
+ float(x)
44
+ except ValueError:
45
+ return False
46
+ else:
47
+ return True
48
+
49
+
50
+ def has_matching_sublist(
51
+ *,
52
+ my_list: list,
53
+ required_match_count: int,
54
+ position: int,
55
+ contiguous: bool,
56
+ conditions: list[bool],
57
+ ) -> bool:
58
+ if contiguous:
59
+ # Check for contiguous matches based on position
60
+ if position == 0:
61
+ # Check if the beginning of the list matches
62
+ count = sum(
63
+ 1
64
+ for i in range(min(required_match_count, len(my_list)))
65
+ if conditions[i]
66
+ )
67
+ return count == required_match_count
68
+ elif position > 0:
69
+ # Skip the first `position` elements
70
+ count = sum(
71
+ 1
72
+ for i in range(position, position + required_match_count)
73
+ if i < len(my_list) and conditions[i]
74
+ )
75
+ return count == required_match_count
76
+ elif position == -1:
77
+ # Check if the end of the list matches
78
+ count = sum(
79
+ 1
80
+ for i in range(len(my_list) - required_match_count, len(my_list))
81
+ if conditions[i]
82
+ )
83
+ return count == required_match_count
84
+ elif position < -1:
85
+ # Skip the last `abs(position)` elements
86
+ count = sum(1 for i in range(len(my_list) + position) if conditions[i])
87
+ return count == required_match_count
88
+ else:
89
+ # Check for non-contiguous matches
90
+ count = sum(1 for i in range(len(my_list)) if conditions[i])
91
+ return count >= required_match_count
92
+
93
+
94
+ def has_sublist2(
95
+ *,
96
+ my_list: list,
97
+ required_match_count: int,
98
+ position: int,
99
+ contiguous: bool,
100
+ condition: callable,
101
+ ) -> bool:
102
+ if contiguous:
103
+ # Check for contiguous matches based on position
104
+ if position == 0:
105
+ # Check if the beginning of the list matches
106
+ count = sum(1 for x in my_list[:required_match_count] if condition(x))
107
+ return count == required_match_count
108
+ elif position > 0:
109
+ # Skip the first `position` elements
110
+ count = sum(
111
+ 1
112
+ for x in my_list[position : position + required_match_count]
113
+ if condition(x)
114
+ )
115
+ return count == required_match_count
116
+ elif position == -1:
117
+ # Check if the end of the list matches
118
+ count = sum(1 for x in my_list[-required_match_count:] if condition(x))
119
+ return count == required_match_count
120
+ elif position < -1:
121
+ # Skip the last `abs(position)` elements
122
+ count = sum(1 for x in my_list[:position] if condition(x))
123
+ return count == required_match_count
124
+ else:
125
+ # Check for non-contiguous matches
126
+ count = sum(1 for x in my_list if condition(x))
127
+ return count >= required_match_count
128
+
129
+
130
+ def create_mathjson_solver(solver_parameters):
131
+ def f(s, *args):
132
+ if args:
133
+ c = deepcopy(args[0])
134
+ else:
135
+ c = {}
136
+ # c = deepcopy(kwargs.get("c", {}))
137
+ if isinstance(s, numbers.Number):
138
+ return s
139
+ if isinstance(s, list):
140
+
141
+ def Arr(s):
142
+ return s
143
+
144
+ def Sum(s):
145
+ l_res = []
146
+ for x in s[1:]:
147
+ res = f(x, c)
148
+ if isinstance(res, list):
149
+ l_res.append(sum([xx for xx in res[1:]]))
150
+ else:
151
+ l_res.append(res)
152
+ return sum(l_res)
153
+
154
+ # @requires_array
155
+ def Max(s):
156
+ if isinstance(s[1], str):
157
+ return max([f(x, c) for x in f(s[1], c) if is_numeric(f(x, c))])
158
+ else:
159
+ return max([f(x, c) for x in s[1][1:] if is_numeric(f(x, c))])
160
+
161
+ # @requires_array
162
+ def Min(s):
163
+ if isinstance(s[1], str):
164
+ return min([f(x, c) for x in f(s[1], c) if is_numeric(f(x, c))])
165
+ else:
166
+ return min([f(x, c) for x in s[1][1:] if is_numeric(f(x, c))])
167
+
168
+ def Average(s):
169
+ if isinstance(s[1], str):
170
+ # A reference to "answer" has been passed
171
+ s_ = [float(f(x, c)) for x in f(s[1], c) if is_numeric(f(x, c))]
172
+ else:
173
+ s_ = [float(f(x, c)) for x in s[1][1:] if is_numeric(f(x, c))]
174
+ # print(f"{s_} {sum(s_)}/{len(s_)}")
175
+ try:
176
+ return sum(s_) / len(s_)
177
+ except ZeroDivisionError:
178
+ return None
179
+
180
+ # @requires_array
181
+ def Median(s):
182
+ if isinstance(s[1], str):
183
+ return median([f(x, c) for x in f(s[1], c) if is_numeric(f(x, c))])
184
+ else:
185
+ return median([f(x, c) for x in s[1][1:] if is_numeric(f(x, c))])
186
+
187
+ # @requires_array
188
+ def Length(s):
189
+ if isinstance(s[1], str):
190
+
191
+ return len([x for x in f(s[1], c)][1:])
192
+ else:
193
+ return len([x for x in s[1][1:]])
194
+
195
+ # @requires_array
196
+ def Any(s):
197
+ return any([f(x, c) for x in s[1][1:]])
198
+
199
+ # @requires_array
200
+ def All(s):
201
+ return all([f(x, c) for x in s[1][1:]])
202
+
203
+ def Int(s):
204
+ try:
205
+ return int(f(s[1], c))
206
+ except ValueError:
207
+ return int(float(f(s[1], c)))
208
+
209
+ def Float(s):
210
+ return float(f(s[1], c))
211
+
212
+ def Constants(s):
213
+ for x in s[1:-1]:
214
+ c[x[0]] = f(x[1], c)
215
+ return f(s[-1], c)
216
+
217
+ def Switch(s):
218
+ expression = f(s[1], c)
219
+ for x in s[3:]:
220
+ if len(x) != 2:
221
+ raise ValueError(
222
+ "Case of 'Switch' should have exactly two parameters"
223
+ )
224
+ if expression == f(x[0], c):
225
+ return f(x[1], c)
226
+ else:
227
+ return f(s[2], c)
228
+
229
+ def If(s):
230
+ if len(s) < 3:
231
+ raise ValueError("Wrong parameters for 'If'")
232
+ for x in s[1:-1]:
233
+ if len(x) != 2:
234
+ raise ValueError("Wrong if or elif in 'If'")
235
+ try:
236
+ if f(x[0], c):
237
+ try:
238
+ return f(x[1], c)
239
+ except MathJSONException:
240
+ logging.error(
241
+ "MathJSONException: %s", traceback.format_exc()
242
+ )
243
+ continue
244
+ except MathJSONException:
245
+ logging.error("MathJSONException: %s", traceback.format_exc())
246
+ return f(s[-1], c) # return default value (else)
247
+
248
+ return f(s[-1], c)
249
+
250
+ def In(s):
251
+ if len(s) != 3:
252
+ raise ValueError("Wrong parameters for 'In'")
253
+ if isinstance(s[2], list) and s[2][0] == "Array":
254
+ return f(s[1], c) in [f(x, c) for x in s[2][1:]]
255
+
256
+ elif isinstance(s[2], str):
257
+ return f(s[1], c) in f(s[2], c)
258
+ else:
259
+ raise ValueError(
260
+ "Wrong parameters for 'In'. Parameter 2 must be a list."
261
+ )
262
+
263
+ def Not_in(s):
264
+ return not In(s)
265
+
266
+ def Contains_any_of(s):
267
+ if isinstance(s[1], list) and s[1][0] == "Array":
268
+ list1 = [f(x, c) for x in s[1][1:]]
269
+ elif isinstance(s[1], str):
270
+ list1 = f(s[1], c)
271
+
272
+ if isinstance(s[2], list) and s[2][0] == "Array":
273
+ list2 = [f(x, c) for x in s[2][1:]]
274
+ elif isinstance(s[2], str):
275
+ list2 = f(s[2], c)
276
+
277
+ if any(x in list1 for x in list2):
278
+ return True
279
+ return False
280
+
281
+ def Contains_all_of(s):
282
+ if isinstance(s[1], list) and s[1][0] == "Array":
283
+ list1 = [f(x, c) for x in s[1][1:]]
284
+ elif isinstance(s[1], str):
285
+ list1 = f(s[1], c)
286
+
287
+ if isinstance(s[2], list) and s[2][0] == "Array":
288
+ list2 = [f(x, c) for x in s[2][1:]]
289
+ elif isinstance(s[2], str):
290
+ list2 = f(s[2], c)
291
+
292
+ if all(x in list1 for x in list2):
293
+ return True
294
+ return False
295
+
296
+ def Contains_none_of(s):
297
+ return not Contains_any_of(s)
298
+
299
+ def Str(s):
300
+ if len(s) < 2:
301
+ raise ValueError("Wrong parameters for 'Str'")
302
+ return f"{f(s[1])}"
303
+
304
+ def Not(s):
305
+ return not f(s[1])
306
+
307
+ def Map(s):
308
+ """
309
+ ["Map", list, function, more parameters]
310
+ The `function` must accept at least one parameter. That is for the current loop element.
311
+ The `more parameters` are for any additional parameters that function might have.
312
+ """
313
+ z = f(s[1], c)
314
+ if isinstance(z, list):
315
+ retlist = ["Array"]
316
+ for x in z[1:]:
317
+ the_function_name = s[2][0]
318
+ ss = [the_function_name, x] + s[3:]
319
+ retlist.append(f(ss, c))
320
+ return retlist
321
+
322
+ def HasMatchingSublist(s):
323
+ """
324
+ ["HasMatchingSublist", list, required_match_count, position, contiguous, function, more parameters]
325
+ """
326
+ the_list = f(s[1], c)[1:]
327
+ required_match_count = f(s[2], c)
328
+ position = f(s[3], c)
329
+ contiguous = f(s[4], c)
330
+ conditions = []
331
+
332
+ for i, x in enumerate(the_list):
333
+ the_function_name = s[5][0]
334
+ ss = [the_function_name, x] + s[6:]
335
+ conditions.append(f(ss, c))
336
+ pass
337
+
338
+ return has_matching_sublist(
339
+ my_list=the_list,
340
+ required_match_count=required_match_count,
341
+ position=position,
342
+ contiguous=contiguous,
343
+ conditions=conditions,
344
+ )
345
+
346
+ constructs = {
347
+ "Sum": Sum,
348
+ "Add": Sum,
349
+ "Subtract": lambda s: reduce(
350
+ lambda a, b: a - b, [f(x, c) for x in s[1:]]
351
+ ),
352
+ "Constants": Constants,
353
+ "Switch": Switch,
354
+ "If": If,
355
+ "Multiply": lambda s: reduce(
356
+ lambda a, b: float(a) * float(b), [f(x, c) for x in s[1:]]
357
+ ),
358
+ "Divide": lambda s: f(s[1], c) / f(s[2], c),
359
+ "Negate": lambda s: -f(s[1], c),
360
+ "Power": lambda s: pow(f(s[1], c), f(s[2], c)),
361
+ "Root": lambda s: pow(f(s[1], c), 1.0 / f(s[2], c)),
362
+ "Sqrt": lambda s: pow(f(s[1], c), 1.0 / 2),
363
+ "Square": lambda s: pow(f(s[1], c), 2),
364
+ "Exp": lambda s: math.exp(f(s[1], c)),
365
+ "Log": lambda s: math.log(f(s[1], c)),
366
+ "Log2": lambda s: math.log2(f(s[1], c)),
367
+ "Log10": lambda s: math.log10(f(s[1], c)),
368
+ "Equal": lambda s: f(s[1], c) == f(s[2], c),
369
+ "Greater": lambda s: f(s[1], c) > f(s[2], c),
370
+ "GreaterEqual": lambda s: f(s[1], c) >= f(s[2], c),
371
+ "Less": lambda s: f(s[1], c) < f(s[2], c),
372
+ "LessEqual": lambda s: f(s[1], c) <= f(s[2], c),
373
+ "NotEqual": lambda s: f(s[1], c) != f(s[2], c),
374
+ "Abs": lambda s: abs(f(s[1], c)),
375
+ "Round": lambda s: (
376
+ round(f(s[1], c), f(s[2], c))
377
+ if len(s) == 3
378
+ else int(round(f(s[1], c)))
379
+ ),
380
+ "Max": Max,
381
+ "Min": Min,
382
+ "Average": Average,
383
+ "Median": Median,
384
+ "Length": Length,
385
+ "Any": Any,
386
+ "All": All,
387
+ "Array": Arr,
388
+ "In": In,
389
+ "Not_in": Not_in,
390
+ "Contains_any_of": Contains_any_of,
391
+ "Contains_all_of": Contains_all_of,
392
+ "Contains_none_of": Contains_none_of,
393
+ "NotIn": Not_in,
394
+ "ContainsAnyOf": Contains_any_of,
395
+ "ContainsAllOf": Contains_all_of,
396
+ "ContainsNoneOf": Contains_none_of,
397
+ "Int": Int,
398
+ "Float": Float,
399
+ "Str": Str,
400
+ "Not": Not,
401
+ "IsDefined": lambda s: s[1] in c,
402
+ "Map": Map,
403
+ "HasMatchingSublist": HasMatchingSublist,
404
+ }
405
+ if s[0] in constructs:
406
+ try:
407
+ return constructs[s[0]](s)
408
+ except Exception as e:
409
+ raise MathJSONException(e, s, mathjson_construct=s[0])
410
+ else:
411
+ # raise MathJSONException(
412
+ # NotImplementedError(f"'{s[0]}' is not supported"), s
413
+ # )
414
+ return s
415
+ elif s in solver_parameters:
416
+ return f(solver_parameters[s], c)
417
+ elif s in c:
418
+ return f(c[s], c)
419
+ else:
420
+ # raise KeyError(f"Parameter '{s}' is not defined")
421
+ return s
422
+
423
+ return f
424
+
425
+
426
+ def extract_variables(s: Union[list, int, float, str], li: set, ignore_list: set):
427
+ constructs = [
428
+ "Add",
429
+ "Sum",
430
+ "Subtract",
431
+ "Constants",
432
+ "Switch",
433
+ "If",
434
+ "Multiply",
435
+ "Divide",
436
+ "Negate",
437
+ "Power",
438
+ "Root",
439
+ "Sqrt",
440
+ "Square",
441
+ "Exp",
442
+ "Log",
443
+ "Log2",
444
+ "Log10",
445
+ "Equal",
446
+ "Greater",
447
+ "GreaterEqual",
448
+ "Less",
449
+ "LessEqual",
450
+ "NotEqual",
451
+ "Abs",
452
+ "Round",
453
+ "Max",
454
+ "Min",
455
+ "Average",
456
+ "Median",
457
+ "Length",
458
+ "Any",
459
+ "All",
460
+ "Array",
461
+ "In",
462
+ "Not_in",
463
+ "Contains_any_of",
464
+ "Contains_all_of",
465
+ "Contains_none_of",
466
+ "NotIn",
467
+ "ContainsAnyOf",
468
+ "ContainsAllOf",
469
+ "ContainsNoneOf",
470
+ "Int",
471
+ "Float",
472
+ "Str",
473
+ "Not",
474
+ "IsDefined",
475
+ "Map",
476
+ "HasMatchingSublist",
477
+ ]
478
+ if isinstance(s, str):
479
+ if s in ignore_list:
480
+ return li
481
+ if s not in constructs:
482
+ li.add(s)
483
+ return li
484
+ elif isinstance(s, list):
485
+ if s[0] == "Constants":
486
+ for x in s[1:-1]:
487
+ ignore_list.add(x[0])
488
+ li.update(extract_variables(x[1], li, ignore_list))
489
+ li.update(extract_variables(x[-1], li, ignore_list))
490
+ else:
491
+ for x in s[1:]:
492
+ li.update(extract_variables(x, li, ignore_list))
493
+ return li
494
+ else:
495
+ return li
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mathjson-solver
3
- Version: 1.7.0
3
+ Version: 1.9.0
4
4
  Summary: Utilities for MathJSON evaluation
5
5
  Home-page: https://github.com/LongenesisLtd/mathjson-solver
6
6
  Author: Martins Mednis
@@ -5,7 +5,7 @@ import re
5
5
 
6
6
  sys.path.append(os.path.join(os.path.dirname(__file__), "../src/"))
7
7
 
8
- from mathjson_solver import create_solver, MathJSONException
8
+ from mathjson_solver import create_solver, MathJSONException, extract_variables
9
9
 
10
10
 
11
11
  @pytest.mark.parametrize(
@@ -13,6 +13,10 @@ from mathjson_solver import create_solver, MathJSONException
13
13
  [
14
14
  ({}, ["Add", 2, 4, 3], 9),
15
15
  ({}, ["Sum", 2, 4, 3], 9),
16
+ ({}, ["Sum", ["Array", 1, 1], 4, 3], 9),
17
+ ({}, ["Sum", ["Array", 2, 4, 3]], 9),
18
+ ({"a": 2}, ["Sum", "a", 4, 3], 9),
19
+ ({"a": ["Array", 1, 1]}, ["Sum", "a", 4, 3], 9),
16
20
  ({}, ["Subtract", 10, 5, 2], 3),
17
21
  ({}, ["Add", 5, 4, ["Negate", 3]], 6),
18
22
  ({}, ["Multiply", 2, 3, 4], 24),
@@ -52,7 +56,11 @@ from mathjson_solver import create_solver, MathJSONException
52
56
  ({}, ["Array", 1, 2, 3, 5, 2], ["Array", 1, 2, 3, 5, 2]),
53
57
  ({}, ["Max", ["Array", 1, 2, 3, 5, 2]], 5),
54
58
  ({}, ["Max", ["Array", 1, 2, ["Sum", 2, 4, 3], 5, 2]], 9),
59
+ ({"a": ["Array", 1, 2, 3, 5, 2]}, ["Max", "a"], 5),
60
+ ({}, ["Min", ["Array", 1, 2, 3, 5, 2]], 1),
61
+ ({"a": ["Array", 2, 1, 3, 5, 2]}, ["Min", "a"], 1),
55
62
  ({}, ["Median", ["Array", 1, 2, 3, 5, 2]], 2),
63
+ ({"a": ["Array", 1, 2, 3, 5, 2]}, ["Median", "a"], 2),
56
64
  ({}, ["Average", ["Array", 1, 2, 3, 5, 2]], 2.6),
57
65
  (
58
66
  {},
@@ -80,8 +88,10 @@ from mathjson_solver import create_solver, MathJSONException
80
88
  5.0,
81
89
  ),
82
90
  ({}, ["Average", ["Array"]], None),
91
+ ({"a": ["Array", 2, 8]}, ["Average", "a"], 5),
83
92
  ({"a": 10, "b": 20}, ["Average", ["Array", "a", "b"]], 15),
84
93
  ({}, ["Length", ["Array", 1, 2, 3, 5, 2, 9]], 6),
94
+ ({"a": ["Array", 1, 2, 3, 5, 2, 9]}, ["Length", "a"], 6),
85
95
  ({}, ["Length", ["Array"]], 0),
86
96
  ({}, ["Int", "12"], 12),
87
97
  ({}, ["Int", "12.2"], 12),
@@ -162,6 +172,93 @@ from mathjson_solver import create_solver, MathJSONException
162
172
  ({}, ["Not", True], False),
163
173
  ({}, ["Not", 0], True),
164
174
  ({}, ["Not", ["In", 2, ["Array", 1, 2, 3]]], False),
175
+ ({}, ["Array", 2, 4, 3], ["Array", 2, 4, 3]),
176
+ ({}, ["Map", ["Array", 1, 2, 3], ["Square"]], ["Array", 1, 4, 9]),
177
+ ({}, ["Map", ["Array", 1, 2, 3], ["Power"], 2], ["Array", 1, 4, 9]),
178
+ (
179
+ {},
180
+ ["Map", ["Array", 1, 2, 3], ["GreaterEqual"], 2],
181
+ ["Array", False, True, True],
182
+ ),
183
+ # ["HasMatchingSublist", list, required_match_count, position, contiguous, function, more parameters]
184
+ (
185
+ {},
186
+ [
187
+ "HasMatchingSublist",
188
+ ["Array", 1, 2, 3, 4, 5, 6],
189
+ 3,
190
+ 0,
191
+ True,
192
+ ["GreaterEqual"],
193
+ 1,
194
+ ],
195
+ True, # first 3 elements are greater than 3
196
+ ),
197
+ (
198
+ {},
199
+ [
200
+ "HasMatchingSublist",
201
+ ["Array", 1, 2, 3, 4, 5, 6],
202
+ 3,
203
+ 0,
204
+ True,
205
+ ["GreaterEqual"],
206
+ 2,
207
+ ],
208
+ False, # first 3 elements are greater than 3 - False
209
+ ),
210
+ (
211
+ {},
212
+ [
213
+ "HasMatchingSublist",
214
+ ["Array", 1, 2, 3, 4, 5, 6],
215
+ 3,
216
+ 0,
217
+ False, # anywhere
218
+ ["GreaterEqual"],
219
+ 4,
220
+ ],
221
+ True,
222
+ ),
223
+ (
224
+ {},
225
+ [
226
+ "HasMatchingSublist",
227
+ ["Array", 1, 2, 3, 4, 5, 6],
228
+ 4,
229
+ 0,
230
+ False, # anywhere
231
+ ["GreaterEqual"],
232
+ 4,
233
+ ],
234
+ False,
235
+ ),
236
+ (
237
+ {},
238
+ [
239
+ "HasMatchingSublist",
240
+ ["Array", 1, 2, 3, 4, 5, 6],
241
+ 3,
242
+ -1,
243
+ True,
244
+ ["GreaterEqual"],
245
+ 4,
246
+ ],
247
+ True,
248
+ ),
249
+ (
250
+ {},
251
+ [
252
+ "HasMatchingSublist",
253
+ ["Array", 1, 2, 3, 4, 5, 6],
254
+ 5,
255
+ -1,
256
+ True,
257
+ ["GreaterEqual"],
258
+ 4,
259
+ ],
260
+ False,
261
+ ),
165
262
  ],
166
263
  )
167
264
  def test_solver_simple(parameters, expression, expected_result):
@@ -206,3 +303,26 @@ def test_handle_exception():
206
303
  assert True
207
304
  else:
208
305
  assert False
306
+
307
+
308
+ @pytest.mark.parametrize(
309
+ "parameters, expression, expected_result",
310
+ [
311
+ ({}, ["Add", 2, 4, 3], set()),
312
+ ({}, ["Sum", 2, "a", 3], set(["a"])),
313
+ ({"a": 5}, ["Sum", 2, "a", 3], set()),
314
+ ({"a": 5, "b": 6}, ["Sum", 2, "a", "c"], set(["c"])),
315
+ ({}, ["Constants", ["c", 1], ["d", ["Add", 2, "a"]], "d"], set(["a"])),
316
+ # Now with "ugly" variables that mimic the ones used for deep referencing.
317
+ (
318
+ {},
319
+ ["Add", "[slug1][0][question1]", "y", 4, "[slug1][-3:-1][question1]"],
320
+ set(["[slug1][0][question1]", "y", "[slug1][-3:-1][question1]"]),
321
+ ),
322
+ ],
323
+ )
324
+ def test_extract_variables(parameters, expression, expected_result):
325
+ assert (
326
+ extract_variables(expression, set(), set([x for x in parameters.keys()]))
327
+ == expected_result
328
+ )
@@ -1,273 +0,0 @@
1
- import numbers
2
- from functools import reduce
3
- import math
4
- from copy import deepcopy
5
- from statistics import median
6
- import logging
7
- import traceback
8
-
9
-
10
- class MathJSONException(Exception):
11
- """Exception for MathJSON processing issues"""
12
-
13
- def __init__(self, e, expr, *args, **kwargs):
14
- super().__init__(args)
15
- self.e = e
16
- self.expr = expr
17
- self.construct = kwargs.get("mathjson_construct", "MathJSON")
18
-
19
- def __str__(self):
20
- if hasattr(self.e, "message"):
21
- m = self.e.message
22
- else:
23
- m = str(self.e)
24
- return f"Problem in {self.construct}. {self.expr}. {m}"
25
-
26
-
27
- def requires_array(func):
28
- def inner1(*args, **kwargs):
29
- try:
30
- if args[0][1][0] == "Array" and len(args[0][1]) > 0:
31
- return func(*args, **kwargs)
32
- else:
33
- raise ValueError(f"'{func.__name__}' should receive a list")
34
- except TypeError:
35
- raise ValueError(f"'{func.__name__}' really should receive a list")
36
-
37
- return inner1
38
-
39
-
40
- def is_numeric(x):
41
- try:
42
- float(x)
43
- except ValueError:
44
- return False
45
- else:
46
- return True
47
-
48
-
49
- def create_mathjson_solver(solver_parameters):
50
- def f(s, *args):
51
- if args:
52
- c = deepcopy(args[0])
53
- else:
54
- c = {}
55
- # c = deepcopy(kwargs.get("c", {}))
56
- if isinstance(s, numbers.Number):
57
- return s
58
- if isinstance(s, list):
59
-
60
- def Arr(s):
61
- return s
62
-
63
- @requires_array
64
- def Max(s):
65
- return max([f(x, c) for x in s[1][1:]])
66
-
67
- @requires_array
68
- def Min(s):
69
- return min([f(x, c) for x in s[1][1:]])
70
-
71
- @requires_array
72
- def Average(s):
73
- s_ = [float(f(x, c)) for x in s[1][1:] if is_numeric(f(x, c))]
74
- # print(f"{s_} {sum(s_)}/{len(s_)}")
75
- try:
76
- return sum(s_) / len(s_)
77
- except ZeroDivisionError:
78
- return None
79
-
80
- @requires_array
81
- def Median(s):
82
- return median([f(x, c) for x in s[1][1:]])
83
-
84
- @requires_array
85
- def Length(s):
86
- return len([f(x, c) for x in s[1][1:]])
87
-
88
- @requires_array
89
- def Any(s):
90
- return any([f(x, c) for x in s[1][1:]])
91
-
92
- @requires_array
93
- def All(s):
94
- return all([f(x, c) for x in s[1][1:]])
95
-
96
- def Int(s):
97
- try:
98
- return int(f(s[1], c))
99
- except ValueError:
100
- return int(float(f(s[1], c)))
101
-
102
- def Float(s):
103
- return float(f(s[1], c))
104
-
105
- def Constants(s):
106
- for x in s[1:-1]:
107
- c[x[0]] = f(x[1], c)
108
- return f(s[-1], c)
109
-
110
- def Switch(s):
111
- expression = f(s[1], c)
112
- for x in s[3:]:
113
- if len(x) != 2:
114
- raise ValueError(
115
- f"Case of 'Switch' should have exactly two parameters"
116
- )
117
- if expression == f(x[0], c):
118
- return f(x[1], c)
119
- else:
120
- return f(s[2], c)
121
-
122
- def If(s):
123
- if len(s) < 3:
124
- raise ValueError(f"Wrong parameters for 'If'")
125
- for x in s[1:-1]:
126
- if len(x) != 2:
127
- raise ValueError(f"Wrong if or elif in 'If'")
128
- try:
129
- if f(x[0], c):
130
- try:
131
- return f(x[1], c)
132
- except MathJSONException as e:
133
- logging.error(
134
- "MathJSONException: %s", traceback.format_exc()
135
- )
136
- continue
137
- except MathJSONException as e:
138
- logging.error("MathJSONException: %s", traceback.format_exc())
139
- return f(s[-1], c) # return default value (else)
140
-
141
- return f(s[-1], c)
142
-
143
- def In(s):
144
- if len(s) != 3:
145
- raise ValueError(f"Wrong parameters for 'In'")
146
- if type(s[2]) == list and s[2][0] == "Array":
147
- return f(s[1], c) in [f(x, c) for x in s[2][1:]]
148
-
149
- elif type(s[2]) == str:
150
- return f(s[1], c) in f(s[2], c)
151
- else:
152
- raise ValueError(
153
- f"Wrong parameters for 'In'. Parameter 2 must be a list."
154
- )
155
-
156
- def Not_in(s):
157
- return not In(s)
158
-
159
- def Contains_any_of(s):
160
- if type(s[1]) == list and s[1][0] == "Array":
161
- list1 = [f(x, c) for x in s[1][1:]]
162
- elif type(s[1]) == str:
163
- list1 = f(s[1], c)
164
-
165
- if type(s[2]) == list and s[2][0] == "Array":
166
- list2 = [f(x, c) for x in s[2][1:]]
167
- elif type(s[2]) == str:
168
- list2 = f(s[2], c)
169
-
170
- if any(x in list1 for x in list2):
171
- return True
172
- return False
173
-
174
- def Contains_all_of(s):
175
- if type(s[1]) == list and s[1][0] == "Array":
176
- list1 = [f(x, c) for x in s[1][1:]]
177
- elif type(s[1]) == str:
178
- list1 = f(s[1], c)
179
-
180
- if type(s[2]) == list and s[2][0] == "Array":
181
- list2 = [f(x, c) for x in s[2][1:]]
182
- elif type(s[2]) == str:
183
- list2 = f(s[2], c)
184
-
185
- if all(x in list1 for x in list2):
186
- return True
187
- return False
188
-
189
- def Contains_none_of(s):
190
- return not Contains_any_of(s)
191
-
192
- def Str(s):
193
- if len(s) < 2:
194
- raise ValueError(f"Wrong parameters for 'Str'")
195
- return f"{f(s[1])}"
196
-
197
- def Not(s):
198
- return not f(s[1])
199
-
200
- constructs = {
201
- "Add": lambda s: sum([f(x, c) for x in s[1:]]),
202
- "Sum": lambda s: sum([f(x, c) for x in s[1:]]),
203
- "Subtract": lambda s: reduce(
204
- lambda a, b: a - b, [f(x, c) for x in s[1:]]
205
- ),
206
- "Constants": Constants,
207
- "Switch": Switch,
208
- "If": If,
209
- "Multiply": lambda s: reduce(
210
- lambda a, b: float(a) * float(b), [f(x, c) for x in s[1:]]
211
- ),
212
- "Divide": lambda s: f(s[1], c) / f(s[2], c),
213
- "Negate": lambda s: -f(s[1], c),
214
- "Power": lambda s: pow(f(s[1], c), f(s[2], c)),
215
- "Root": lambda s: pow(f(s[1], c), 1.0 / f(s[2], c)),
216
- "Sqrt": lambda s: pow(f(s[1], c), 1.0 / 2),
217
- "Square": lambda s: pow(f(s[1], c), 2),
218
- "Exp": lambda s: math.exp(f(s[1], c)),
219
- "Log": lambda s: math.log(f(s[1], c)),
220
- "Log2": lambda s: math.log2(f(s[1], c)),
221
- "Log10": lambda s: math.log10(f(s[1], c)),
222
- "Equal": lambda s: f(s[1], c) == f(s[2], c),
223
- "Greater": lambda s: f(s[1], c) > f(s[2], c),
224
- "GreaterEqual": lambda s: f(s[1], c) >= f(s[2], c),
225
- "Less": lambda s: f(s[1], c) < f(s[2], c),
226
- "LessEqual": lambda s: f(s[1], c) <= f(s[2], c),
227
- "NotEqual": lambda s: f(s[1], c) != f(s[2], c),
228
- "Abs": lambda s: abs(f(s[1], c)),
229
- "Round": lambda s: round(f(s[1], c), f(s[2], c))
230
- if len(s) == 3
231
- else int(round(f(s[1], c))),
232
- "Max": Max,
233
- "Min": Min,
234
- "Average": Average,
235
- "Median": Median,
236
- "Length": Length,
237
- "Any": Any,
238
- "All": All,
239
- "Array": Arr,
240
- "In": In,
241
- "Not_in": Not_in,
242
- "Contains_any_of": Contains_any_of,
243
- "Contains_all_of": Contains_all_of,
244
- "Contains_none_of": Contains_none_of,
245
- "NotIn": Not_in,
246
- "ContainsAnyOf": Contains_any_of,
247
- "ContainsAllOf": Contains_all_of,
248
- "ContainsNoneOf": Contains_none_of,
249
- "Int": Int,
250
- "Float": Float,
251
- "Str": Str,
252
- "Not": Not,
253
- "IsDefined": lambda s: s[1] in c,
254
- }
255
- if s[0] in constructs:
256
- try:
257
- return constructs[s[0]](s)
258
- except Exception as e:
259
- raise MathJSONException(e, s, mathjson_construct=s[0])
260
- else:
261
- # raise MathJSONException(
262
- # NotImplementedError(f"'{s[0]}' is not supported"), s
263
- # )
264
- return s
265
- elif s in solver_parameters:
266
- return f(solver_parameters[s], c)
267
- elif s in c:
268
- return f(c[s], c)
269
- else:
270
- # raise KeyError(f"Parameter '{s}' is not defined")
271
- return s
272
-
273
- return f
File without changes