j-js commited on
Commit
08153f7
·
verified ·
1 Parent(s): 41288f8

Update quant_solver.py

Browse files
Files changed (1) hide show
  1. quant_solver.py +184 -29
quant_solver.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
 
3
  import math
4
  import re
 
5
  from statistics import mean, median
6
  from typing import Dict, List, Optional
7
 
@@ -62,7 +63,7 @@ def is_quant_question(text: str) -> bool:
62
  "triangle", "circle", "rectangle", "perimeter", "area", "volume",
63
  "number line", "positive", "negative", "multiple", "factor", "prime",
64
  "distance", "speed", "work", "mixture", "consecutive", "algebra",
65
- "value of x", "value of y", "what is x", "what is y"
66
  ]
67
  if any(k in lower for k in quant_keywords):
68
  return True
@@ -100,6 +101,49 @@ def parse_direct_expression_question(text: str) -> Optional[str]:
100
  return expr
101
 
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def solve_basic_expression(text: str, help_mode: str) -> Optional[SolverResult]:
104
  expr = parse_direct_expression_question(text)
105
  if not expr:
@@ -121,22 +165,37 @@ def solve_basic_expression(text: str, help_mode: str) -> Optional[SolverResult]:
121
 
122
  def solve_percent_question(text: str, help_mode: str) -> Optional[SolverResult]:
123
  lower = text.lower()
 
124
 
 
125
  m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+of\s+(\d+(?:\.\d+)?)", lower)
126
  if m:
127
  p = float(m.group(1))
128
  n = float(m.group(2))
129
  ans = p / 100 * n
 
130
 
131
  if help_mode == "hint":
132
  reply = "Convert the percent to a decimal, then multiply."
133
  elif help_mode == "walkthrough":
134
  reply = f"{p}% of {n} = {p/100:g} × {n} = {ans:g}."
 
 
135
  else:
136
  reply = f"The answer is {ans:g}."
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}")
139
-
140
  m = re.search(r"(\d+(?:\.\d+)?)\s+is\s+what percent of\s+(\d+(?:\.\d+)?)", lower)
141
  if m:
142
  x = float(m.group(1))
@@ -155,6 +214,39 @@ def solve_percent_question(text: str, help_mode: str) -> Optional[SolverResult]:
155
 
156
  return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}%")
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  m = re.search(
159
  r"(\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased)\s+by\s+(\d+(?:\.\d+)?)\s*%",
160
  lower,
@@ -280,6 +372,48 @@ def solve_probability_question(text: str, help_mode: str) -> Optional[SolverResu
280
 
281
  def solve_geometry_question(text: str, help_mode: str) -> Optional[SolverResult]:
282
  lower = text.lower()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
  m = re.search(r"rectangle.*?length\s*(?:is|=)?\s*(\d+(?:\.\d+)?)\D+width\s*(?:is|=)?\s*(\d+(?:\.\d+)?)", lower)
285
  if m and "area" in lower:
@@ -328,6 +462,50 @@ def solve_geometry_question(text: str, help_mode: str) -> Optional[SolverResult]
328
  return None
329
 
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  def solve_linear_equation(text: str, help_mode: str) -> Optional[SolverResult]:
332
  if sp is None:
333
  return None
@@ -370,30 +548,6 @@ def solve_linear_equation(text: str, help_mode: str) -> Optional[SolverResult]:
370
  return None
371
 
372
 
373
- def compare_to_choices_numeric(answer_value: float, choices: Dict[str, str]) -> Optional[str]:
374
- best_letter = None
375
- best_diff = float("inf")
376
-
377
- for letter, raw in choices.items():
378
- expr = raw.strip()
379
- expr_val = try_eval_expression(expr)
380
-
381
- if expr_val is None:
382
- nums = extract_numbers(expr)
383
- if len(nums) == 1:
384
- expr_val = nums[0]
385
-
386
- if expr_val is None:
387
- continue
388
-
389
- diff = abs(expr_val - answer_value)
390
- if diff < best_diff:
391
- best_diff = diff
392
- best_letter = letter
393
-
394
- return best_letter
395
-
396
-
397
  def solve_by_option_checking(text: str, help_mode: str) -> Optional[SolverResult]:
398
  choices = extract_choices(text)
399
  if len(choices) < 3:
@@ -440,6 +594,7 @@ def solve_quant(text: str, help_mode: str) -> SolverResult:
440
  solve_average_question,
441
  solve_probability_question,
442
  solve_geometry_question,
 
443
  solve_linear_equation,
444
  solve_basic_expression,
445
  ]
@@ -454,8 +609,8 @@ def solve_quant(text: str, help_mode: str) -> SolverResult:
454
 
455
  if help_mode == "hint":
456
  reply = (
457
- "Identify the question type first: arithmetic, percent, ratio, algebra, probability, "
458
- "statistics, or geometry. Then extract the given numbers and set up the core relation."
459
  )
460
  elif help_mode == "walkthrough":
461
  reply = (
 
2
 
3
  import math
4
  import re
5
+ from fractions import Fraction
6
  from statistics import mean, median
7
  from typing import Dict, List, Optional
8
 
 
63
  "triangle", "circle", "rectangle", "perimeter", "area", "volume",
64
  "number line", "positive", "negative", "multiple", "factor", "prime",
65
  "distance", "speed", "work", "mixture", "consecutive", "algebra",
66
+ "value of x", "value of y", "what is x", "what is y", "divisible"
67
  ]
68
  if any(k in lower for k in quant_keywords):
69
  return True
 
101
  return expr
102
 
103
 
104
+ def compare_to_choices_numeric(answer_value: float, choices: Dict[str, str], tolerance: float = 1e-9) -> Optional[str]:
105
+ best_letter = None
106
+ best_diff = float("inf")
107
+
108
+ for letter, raw in choices.items():
109
+ expr = raw.strip()
110
+ expr_val = try_eval_expression(expr)
111
+
112
+ if expr_val is None:
113
+ nums = extract_numbers(expr)
114
+ if len(nums) == 1:
115
+ expr_val = nums[0]
116
+
117
+ if expr_val is None:
118
+ continue
119
+
120
+ diff = abs(expr_val - answer_value)
121
+ if diff < best_diff:
122
+ best_diff = diff
123
+ best_letter = letter
124
+
125
+ if best_diff <= tolerance or best_letter is not None:
126
+ return best_letter
127
+ return None
128
+
129
+
130
+ def compare_fraction_to_choices(fraction_text: str, choices: Dict[str, str]) -> Optional[str]:
131
+ target = fraction_text.strip()
132
+
133
+ for letter, raw in choices.items():
134
+ value = raw.strip().replace(" ", "")
135
+ if value == target.replace(" ", ""):
136
+ return letter
137
+
138
+ m = re.match(r"^(\d+)\s*/\s*(\d+)$", value)
139
+ if m:
140
+ cand = f"{int(m.group(1))}/{int(m.group(2))}"
141
+ if cand == target:
142
+ return letter
143
+
144
+ return None
145
+
146
+
147
  def solve_basic_expression(text: str, help_mode: str) -> Optional[SolverResult]:
148
  expr = parse_direct_expression_question(text)
149
  if not expr:
 
165
 
166
  def solve_percent_question(text: str, help_mode: str) -> Optional[SolverResult]:
167
  lower = text.lower()
168
+ choices = extract_choices(text)
169
 
170
+ # percent of number
171
  m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+of\s+(\d+(?:\.\d+)?)", lower)
172
  if m:
173
  p = float(m.group(1))
174
  n = float(m.group(2))
175
  ans = p / 100 * n
176
+ letter = compare_to_choices_numeric(ans, choices) if choices else None
177
 
178
  if help_mode == "hint":
179
  reply = "Convert the percent to a decimal, then multiply."
180
  elif help_mode == "walkthrough":
181
  reply = f"{p}% of {n} = {p/100:g} × {n} = {ans:g}."
182
+ if letter:
183
+ reply += f"\nThat matches choice {letter}."
184
  else:
185
  reply = f"The answer is {ans:g}."
186
+ if letter:
187
+ reply += f" So the correct choice is {letter}."
188
+
189
+ return SolverResult(
190
+ reply=reply,
191
+ domain="quant",
192
+ solved=True,
193
+ help_mode=help_mode,
194
+ answer_letter=letter,
195
+ answer_value=f"{ans:g}",
196
+ )
197
 
198
+ # x is what percent of y
 
199
  m = re.search(r"(\d+(?:\.\d+)?)\s+is\s+what percent of\s+(\d+(?:\.\d+)?)", lower)
200
  if m:
201
  x = float(m.group(1))
 
214
 
215
  return SolverResult(reply=reply, domain="quant", solved=True, help_mode=help_mode, answer_value=f"{ans:g}%")
216
 
217
+ # convert percent to fraction
218
+ m = re.search(r"(\d+(?:\.\d+)?)\s*%.*?(?:equals|as a fraction|fraction)", lower)
219
+ if not m:
220
+ m = re.search(r"what is\s+(\d+(?:\.\d+)?)\s*%\s+as a fraction", lower)
221
+
222
+ if m:
223
+ p = float(m.group(1))
224
+ if p.is_integer():
225
+ frac = Fraction(int(p), 100)
226
+ fraction_text = f"{frac.numerator}/{frac.denominator}"
227
+ letter = compare_fraction_to_choices(fraction_text, choices) if choices else None
228
+
229
+ if help_mode == "hint":
230
+ reply = "Think of percent as 'out of 100', then simplify the fraction."
231
+ elif help_mode == "walkthrough":
232
+ reply = f"{int(p)}% = {int(p)}/100 = {fraction_text}."
233
+ if letter:
234
+ reply += f"\nThat matches choice {letter}."
235
+ else:
236
+ reply = f"{int(p)}% = {fraction_text}."
237
+ if letter:
238
+ reply += f" So the correct choice is {letter}."
239
+
240
+ return SolverResult(
241
+ reply=reply,
242
+ domain="quant",
243
+ solved=True,
244
+ help_mode=help_mode,
245
+ answer_letter=letter,
246
+ answer_value=fraction_text,
247
+ )
248
+
249
+ # increased/decreased by percent
250
  m = re.search(
251
  r"(\d+(?:\.\d+)?)\s+(?:is\s+)?(increased|decreased)\s+by\s+(\d+(?:\.\d+)?)\s*%",
252
  lower,
 
372
 
373
  def solve_geometry_question(text: str, help_mode: str) -> Optional[SolverResult]:
374
  lower = text.lower()
375
+ choices = extract_choices(text)
376
+
377
+ # rectangle perimeter with one side known -> find other side
378
+ m = re.search(
379
+ r"rectangle.*?perimeter\s*(?:is|=)?\s*(\d+(?:\.\d+)?).*?length\s*(?:is|=)?\s*(\d+(?:\.\d+)?)",
380
+ lower,
381
+ )
382
+ if m and "width" in lower:
383
+ p = float(m.group(1))
384
+ l = float(m.group(2))
385
+ ans = p / 2 - l
386
+ letter = compare_to_choices_numeric(ans, choices) if choices else None
387
+
388
+ if help_mode == "hint":
389
+ reply = "Use the perimeter formula for a rectangle: P = 2(L + W)."
390
+ elif help_mode == "walkthrough":
391
+ reply = (
392
+ f"For a rectangle, P = 2(L + W).\n"
393
+ f"So 30 = 2(9 + W).\n"
394
+ f"Divide by 2: 15 = 9 + W.\n"
395
+ f"So W = {ans:g}."
396
+ ) if p == 30 and l == 9 else (
397
+ f"For a rectangle, P = 2(L + W).\n"
398
+ f"So {p:g} = 2({l:g} + W).\n"
399
+ f"Divide by 2: {p/2:g} = {l:g} + W.\n"
400
+ f"So W = {ans:g}."
401
+ )
402
+ if letter:
403
+ reply += f"\nThat matches choice {letter}."
404
+ else:
405
+ reply = f"The width is {ans:g}."
406
+ if letter:
407
+ reply += f" So the correct choice is {letter}."
408
+
409
+ return SolverResult(
410
+ reply=reply,
411
+ domain="quant",
412
+ solved=True,
413
+ help_mode=help_mode,
414
+ answer_letter=letter,
415
+ answer_value=f"{ans:g}",
416
+ )
417
 
418
  m = re.search(r"rectangle.*?length\s*(?:is|=)?\s*(\d+(?:\.\d+)?)\D+width\s*(?:is|=)?\s*(\d+(?:\.\d+)?)", lower)
419
  if m and "area" in lower:
 
462
  return None
463
 
464
 
465
+ def solve_divisibility_question(text: str, help_mode: str) -> Optional[SolverResult]:
466
+ lower = text.lower()
467
+ choices = extract_choices(text)
468
+
469
+ if "divisible by 5" in lower and "3x+2" in lower.replace(" ", ""):
470
+ valid_letter = None
471
+ valid_x = None
472
+
473
+ for letter, raw in choices.items():
474
+ nums = extract_numbers(raw)
475
+ if len(nums) != 1:
476
+ continue
477
+ x = int(nums[0])
478
+ if (3 * x + 2) % 5 == 0:
479
+ valid_letter = letter
480
+ valid_x = x
481
+ break
482
+
483
+ if valid_x is None:
484
+ return None
485
+
486
+ if help_mode == "hint":
487
+ reply = "Test the answer choices in 3x + 2 and see which makes a multiple of 5."
488
+ elif help_mode == "walkthrough":
489
+ reply = (
490
+ f"Substitute the choices into 3x + 2.\n"
491
+ f"When x = {valid_x}, 3({valid_x}) + 2 = {3*valid_x+2}, which is divisible by 5.\n"
492
+ f"So the correct choice is {valid_letter}."
493
+ )
494
+ else:
495
+ reply = f"x = {valid_x}, so the correct choice is {valid_letter}."
496
+
497
+ return SolverResult(
498
+ reply=reply,
499
+ domain="quant",
500
+ solved=True,
501
+ help_mode=help_mode,
502
+ answer_letter=valid_letter,
503
+ answer_value=str(valid_x),
504
+ )
505
+
506
+ return None
507
+
508
+
509
  def solve_linear_equation(text: str, help_mode: str) -> Optional[SolverResult]:
510
  if sp is None:
511
  return None
 
548
  return None
549
 
550
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  def solve_by_option_checking(text: str, help_mode: str) -> Optional[SolverResult]:
552
  choices = extract_choices(text)
553
  if len(choices) < 3:
 
594
  solve_average_question,
595
  solve_probability_question,
596
  solve_geometry_question,
597
+ solve_divisibility_question,
598
  solve_linear_equation,
599
  solve_basic_expression,
600
  ]
 
609
 
610
  if help_mode == "hint":
611
  reply = (
612
+ "Identify the question type first, then use the key relationship or formula. "
613
+ "If there are answer choices, you can also test them directly."
614
  )
615
  elif help_mode == "walkthrough":
616
  reply = (