mathjson-solver 1.6.1__tar.gz → 1.8.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.6.1
3
+ Version: 1.8.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.6.1
3
+ version = 1.8.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.6.1",
8
+ version="1.8.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
@@ -1,8 +1,11 @@
1
1
  import numbers
2
+ from typing import Union
2
3
  from functools import reduce
3
4
  import math
4
5
  from copy import deepcopy
5
6
  from statistics import median
7
+ import logging
8
+ import traceback
6
9
 
7
10
 
8
11
  class MathJSONException(Exception):
@@ -58,36 +61,52 @@ def create_mathjson_solver(solver_parameters):
58
61
  def Arr(s):
59
62
  return s
60
63
 
61
- @requires_array
64
+ # @requires_array
62
65
  def Max(s):
63
- return max([f(x, c) for x in s[1][1:]])
66
+ if isinstance(s[1], str):
67
+ return max([f(x, c) for x in f(s[1], c) if is_numeric(f(x, c))])
68
+ else:
69
+ return max([f(x, c) for x in s[1][1:] if is_numeric(f(x, c))])
64
70
 
65
- @requires_array
71
+ # @requires_array
66
72
  def Min(s):
67
- return min([f(x, c) for x in s[1][1:]])
73
+ if isinstance(s[1], str):
74
+ return min([f(x, c) for x in f(s[1], c) if is_numeric(f(x, c))])
75
+ else:
76
+ return min([f(x, c) for x in s[1][1:] if is_numeric(f(x, c))])
68
77
 
69
- @requires_array
70
78
  def Average(s):
71
- s_ = [float(f(x, c)) for x in s[1][1:] if is_numeric(f(x, c))]
79
+ if isinstance(s[1], str):
80
+ # A reference to "answer" has been passed
81
+ s_ = [float(f(x, c)) for x in f(s[1], c) if is_numeric(f(x, c))]
82
+ else:
83
+ s_ = [float(f(x, c)) for x in s[1][1:] if is_numeric(f(x, c))]
72
84
  # print(f"{s_} {sum(s_)}/{len(s_)}")
73
85
  try:
74
86
  return sum(s_) / len(s_)
75
87
  except ZeroDivisionError:
76
88
  return None
77
89
 
78
- @requires_array
90
+ # @requires_array
79
91
  def Median(s):
80
- return median([f(x, c) for x in s[1][1:]])
92
+ if isinstance(s[1], str):
93
+ return median([f(x, c) for x in f(s[1], c) if is_numeric(f(x, c))])
94
+ else:
95
+ return median([f(x, c) for x in s[1][1:] if is_numeric(f(x, c))])
81
96
 
82
- @requires_array
97
+ # @requires_array
83
98
  def Length(s):
84
- return len([f(x, c) for x in s[1][1:]])
99
+ if isinstance(s[1], str):
85
100
 
86
- @requires_array
101
+ return len([x for x in f(s[1], c)][1:])
102
+ else:
103
+ return len([x for x in s[1][1:]])
104
+
105
+ # @requires_array
87
106
  def Any(s):
88
107
  return any([f(x, c) for x in s[1][1:]])
89
108
 
90
- @requires_array
109
+ # @requires_array
91
110
  def All(s):
92
111
  return all([f(x, c) for x in s[1][1:]])
93
112
 
@@ -110,7 +129,7 @@ def create_mathjson_solver(solver_parameters):
110
129
  for x in s[3:]:
111
130
  if len(x) != 2:
112
131
  raise ValueError(
113
- f"Case of 'Switch' should have exactly two parameters"
132
+ "Case of 'Switch' should have exactly two parameters"
114
133
  )
115
134
  if expression == f(x[0], c):
116
135
  return f(x[1], c)
@@ -119,39 +138,50 @@ def create_mathjson_solver(solver_parameters):
119
138
 
120
139
  def If(s):
121
140
  if len(s) < 3:
122
- raise ValueError(f"Wrong parameters for 'If'")
141
+ raise ValueError("Wrong parameters for 'If'")
123
142
  for x in s[1:-1]:
124
143
  if len(x) != 2:
125
- raise ValueError(f"Wrong if or elif in 'If'")
126
- if f(x[0], c):
127
- return f(x[1], c)
144
+ raise ValueError("Wrong if or elif in 'If'")
145
+ try:
146
+ if f(x[0], c):
147
+ try:
148
+ return f(x[1], c)
149
+ except MathJSONException:
150
+ logging.error(
151
+ "MathJSONException: %s", traceback.format_exc()
152
+ )
153
+ continue
154
+ except MathJSONException:
155
+ logging.error("MathJSONException: %s", traceback.format_exc())
156
+ return f(s[-1], c) # return default value (else)
157
+
128
158
  return f(s[-1], c)
129
159
 
130
160
  def In(s):
131
161
  if len(s) != 3:
132
- raise ValueError(f"Wrong parameters for 'In'")
133
- if type(s[2]) == list and s[2][0] == "Array":
162
+ raise ValueError("Wrong parameters for 'In'")
163
+ if isinstance(s[2], list) and s[2][0] == "Array":
134
164
  return f(s[1], c) in [f(x, c) for x in s[2][1:]]
135
165
 
136
- elif type(s[2]) == str:
166
+ elif isinstance(s[2], str):
137
167
  return f(s[1], c) in f(s[2], c)
138
168
  else:
139
169
  raise ValueError(
140
- f"Wrong parameters for 'In'. Parameter 2 must be a list."
170
+ "Wrong parameters for 'In'. Parameter 2 must be a list."
141
171
  )
142
172
 
143
173
  def Not_in(s):
144
174
  return not In(s)
145
175
 
146
176
  def Contains_any_of(s):
147
- if type(s[1]) == list and s[1][0] == "Array":
177
+ if isinstance(s[1], list) and s[1][0] == "Array":
148
178
  list1 = [f(x, c) for x in s[1][1:]]
149
- elif type(s[1]) == str:
179
+ elif isinstance(s[1], str):
150
180
  list1 = f(s[1], c)
151
181
 
152
- if type(s[2]) == list and s[2][0] == "Array":
182
+ if isinstance(s[2], list) and s[2][0] == "Array":
153
183
  list2 = [f(x, c) for x in s[2][1:]]
154
- elif type(s[2]) == str:
184
+ elif isinstance(s[2], str):
155
185
  list2 = f(s[2], c)
156
186
 
157
187
  if any(x in list1 for x in list2):
@@ -159,14 +189,14 @@ def create_mathjson_solver(solver_parameters):
159
189
  return False
160
190
 
161
191
  def Contains_all_of(s):
162
- if type(s[1]) == list and s[1][0] == "Array":
192
+ if isinstance(s[1], list) and s[1][0] == "Array":
163
193
  list1 = [f(x, c) for x in s[1][1:]]
164
- elif type(s[1]) == str:
194
+ elif isinstance(s[1], str):
165
195
  list1 = f(s[1], c)
166
196
 
167
- if type(s[2]) == list and s[2][0] == "Array":
197
+ if isinstance(s[2], list) and s[2][0] == "Array":
168
198
  list2 = [f(x, c) for x in s[2][1:]]
169
- elif type(s[2]) == str:
199
+ elif isinstance(s[2], str):
170
200
  list2 = f(s[2], c)
171
201
 
172
202
  if all(x in list1 for x in list2):
@@ -178,7 +208,7 @@ def create_mathjson_solver(solver_parameters):
178
208
 
179
209
  def Str(s):
180
210
  if len(s) < 2:
181
- raise ValueError(f"Wrong parameters for 'Str'")
211
+ raise ValueError("Wrong parameters for 'Str'")
182
212
  return f"{f(s[1])}"
183
213
 
184
214
  def Not(s):
@@ -213,9 +243,11 @@ def create_mathjson_solver(solver_parameters):
213
243
  "LessEqual": lambda s: f(s[1], c) <= f(s[2], c),
214
244
  "NotEqual": lambda s: f(s[1], c) != f(s[2], c),
215
245
  "Abs": lambda s: abs(f(s[1], c)),
216
- "Round": lambda s: round(f(s[1], c), f(s[2], c))
217
- if len(s) == 3
218
- else int(round(f(s[1], c))),
246
+ "Round": lambda s: (
247
+ round(f(s[1], c), f(s[2], c))
248
+ if len(s) == 3
249
+ else int(round(f(s[1], c)))
250
+ ),
219
251
  "Max": Max,
220
252
  "Min": Min,
221
253
  "Average": Average,
@@ -237,6 +269,7 @@ def create_mathjson_solver(solver_parameters):
237
269
  "Float": Float,
238
270
  "Str": Str,
239
271
  "Not": Not,
272
+ "IsDefined": lambda s: s[1] in c,
240
273
  }
241
274
  if s[0] in constructs:
242
275
  try:
@@ -253,6 +286,77 @@ def create_mathjson_solver(solver_parameters):
253
286
  elif s in c:
254
287
  return f(c[s], c)
255
288
  else:
289
+ # raise KeyError(f"Parameter '{s}' is not defined")
256
290
  return s
257
291
 
258
292
  return f
293
+
294
+
295
+ def extract_variables(s: Union[list, int, float, str], li: set, ignore_list: set):
296
+ constructs = [
297
+ "Add",
298
+ "Sum",
299
+ "Subtract",
300
+ "Constants",
301
+ "Switch",
302
+ "If",
303
+ "Multiply",
304
+ "Divide",
305
+ "Negate",
306
+ "Power",
307
+ "Root",
308
+ "Sqrt",
309
+ "Square",
310
+ "Exp",
311
+ "Log",
312
+ "Log2",
313
+ "Log10",
314
+ "Equal",
315
+ "Greater",
316
+ "GreaterEqual",
317
+ "Less",
318
+ "LessEqual",
319
+ "NotEqual",
320
+ "Abs",
321
+ "Round",
322
+ "Max",
323
+ "Min",
324
+ "Average",
325
+ "Median",
326
+ "Length",
327
+ "Any",
328
+ "All",
329
+ "Array",
330
+ "In",
331
+ "Not_in",
332
+ "Contains_any_of",
333
+ "Contains_all_of",
334
+ "Contains_none_of",
335
+ "NotIn",
336
+ "ContainsAnyOf",
337
+ "ContainsAllOf",
338
+ "ContainsNoneOf",
339
+ "Int",
340
+ "Float",
341
+ "Str",
342
+ "Not",
343
+ "IsDefined",
344
+ ]
345
+ if isinstance(s, str):
346
+ if s in ignore_list:
347
+ return li
348
+ if s not in constructs:
349
+ li.add(s)
350
+ return li
351
+ elif isinstance(s, list):
352
+ if s[0] == "Constants":
353
+ for x in s[1:-1]:
354
+ ignore_list.add(x[0])
355
+ li.update(extract_variables(x[1], li, ignore_list))
356
+ li.update(extract_variables(x[-1], li, ignore_list))
357
+ else:
358
+ for x in s[1:]:
359
+ li.update(extract_variables(x, li, ignore_list))
360
+ return li
361
+ else:
362
+ return li
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mathjson-solver
3
- Version: 1.6.1
3
+ Version: 1.8.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(
@@ -45,10 +45,18 @@ from mathjson_solver import create_solver, MathJSONException
45
45
  ({"color": "green"}, ["Switch", "color", 0, ["blue", 10], ["red", 30]], 0),
46
46
  ({"color": "green"}, ["Switch", "undefined", 0, ["blue", 10], ["red", 30]], 0),
47
47
  ({}, ["If", [["Equal", 1, 0], 10], [["Equal", 2, 2], 20], 9000], 20),
48
+ ({"a": 10, "b": 10}, ["If", [["Equal", "a", "b"], 10], 9000], 10),
49
+ ({"a": 10, "b": 20}, ["If", [["Equal", "a", "b"], 10], 9000], 9000),
50
+ ({"a": 10}, ["If", [["Equal", "a", "b"], 10], 9000], 9000),
51
+ ({"a": 10}, ["If", [["Equal", "a", ["Sum", 1, "b"]], 10], 9000], 9000),
48
52
  ({}, ["Array", 1, 2, 3, 5, 2], ["Array", 1, 2, 3, 5, 2]),
49
53
  ({}, ["Max", ["Array", 1, 2, 3, 5, 2]], 5),
50
54
  ({}, ["Max", ["Array", 1, 2, ["Sum", 2, 4, 3], 5, 2]], 9),
55
+ ({"a": ["Array", 1, 2, 3, 5, 2]}, ["Max", "a"], 5),
56
+ ({}, ["Min", ["Array", 1, 2, 3, 5, 2]], 1),
57
+ ({"a": ["Array", 2, 1, 3, 5, 2]}, ["Min", "a"], 1),
51
58
  ({}, ["Median", ["Array", 1, 2, 3, 5, 2]], 2),
59
+ ({"a": ["Array", 1, 2, 3, 5, 2]}, ["Median", "a"], 2),
52
60
  ({}, ["Average", ["Array", 1, 2, 3, 5, 2]], 2.6),
53
61
  (
54
62
  {},
@@ -76,8 +84,10 @@ from mathjson_solver import create_solver, MathJSONException
76
84
  5.0,
77
85
  ),
78
86
  ({}, ["Average", ["Array"]], None),
87
+ ({"a": ["Array", 2, 8]}, ["Average", "a"], 5),
79
88
  ({"a": 10, "b": 20}, ["Average", ["Array", "a", "b"]], 15),
80
89
  ({}, ["Length", ["Array", 1, 2, 3, 5, 2, 9]], 6),
90
+ ({"a": ["Array", 1, 2, 3, 5, 2, 9]}, ["Length", "a"], 6),
81
91
  ({}, ["Length", ["Array"]], 0),
82
92
  ({}, ["Int", "12"], 12),
83
93
  ({}, ["Int", "12.2"], 12),
@@ -202,3 +212,26 @@ def test_handle_exception():
202
212
  assert True
203
213
  else:
204
214
  assert False
215
+
216
+
217
+ @pytest.mark.parametrize(
218
+ "parameters, expression, expected_result",
219
+ [
220
+ ({}, ["Add", 2, 4, 3], set()),
221
+ ({}, ["Sum", 2, "a", 3], set(["a"])),
222
+ ({"a": 5}, ["Sum", 2, "a", 3], set()),
223
+ ({"a": 5, "b": 6}, ["Sum", 2, "a", "c"], set(["c"])),
224
+ ({}, ["Constants", ["c", 1], ["d", ["Add", 2, "a"]], "d"], set(["a"])),
225
+ # Now with "ugly" variables that mimic the ones used for deep referencing.
226
+ (
227
+ {},
228
+ ["Add", "[slug1][0][question1]", "y", 4, "[slug1][-3:-1][question1]"],
229
+ set(["[slug1][0][question1]", "y", "[slug1][-3:-1][question1]"]),
230
+ ),
231
+ ],
232
+ )
233
+ def test_extract_variables(parameters, expression, expected_result):
234
+ assert (
235
+ extract_variables(expression, set(), set([x for x in parameters.keys()]))
236
+ == expected_result
237
+ )
File without changes