j-js commited on
Commit
ae8227a
·
verified ·
1 Parent(s): 5f98033

Update solver_distance_rate_time.py

Browse files
Files changed (1) hide show
  1. solver_distance_rate_time.py +553 -36
solver_distance_rate_time.py CHANGED
@@ -1,64 +1,581 @@
1
  from __future__ import annotations
2
 
 
3
  import re
4
- from typing import Optional
5
 
6
  from models import SolverResult
7
 
8
 
9
- def solve_distance_rate_time(text: str) -> Optional[SolverResult]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- lower = text.lower()
12
 
13
- if "speed" not in lower and "distance" not in lower:
 
 
14
  return None
15
 
16
- nums = [float(x) for x in re.findall(r"\d+(?:\.\d+)?", lower)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
- if len(nums) < 2:
 
19
  return None
20
 
21
- # simple pattern: speed and time given
22
- if "speed" in lower and "time" in lower:
 
23
 
24
- speed = nums[0]
25
- time = nums[1]
 
 
 
 
26
 
27
- distance = speed * time
 
 
 
 
 
 
28
 
29
- return SolverResult(
30
- domain="quant",
31
- solved=True,
32
- topic="distance_rate_time",
33
- answer_value=f"{distance:g}",
34
- internal_answer=f"{distance:g}",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  steps=[
36
- "Use the formula distance = speed × time.",
37
- f"Multiply {speed} by {time}."
38
- ]
 
 
39
  )
40
 
41
- # simple pattern: distance and speed given
42
- if "distance" in lower and "speed" in lower:
43
 
44
- distance = nums[0]
45
- speed = nums[1]
46
 
47
- if speed == 0:
48
- return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- time = distance / speed
51
 
52
- return SolverResult(
53
- domain="quant",
54
- solved=True,
55
- topic="distance_rate_time",
56
- answer_value=f"{time:g}",
57
- internal_answer=f"{time:g}",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  steps=[
59
- "Use the formula time = distance ÷ speed.",
60
- f"Divide {distance} by {speed}."
61
- ]
 
 
 
62
  )
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  return None
 
1
  from __future__ import annotations
2
 
3
+ import math
4
  import re
5
+ from typing import Optional, List, Tuple
6
 
7
  from models import SolverResult
8
 
9
 
10
+ _NUMBER = r"(-?\d+(?:\.\d+)?)"
11
+
12
+
13
+ def _clean(text: str) -> str:
14
+ t = text or ""
15
+ t = t.replace("×", "x").replace("–", "-").replace("—", "-")
16
+ t = t.replace("hrs", "hours").replace("hr", "hour")
17
+ t = t.replace("mins", "minutes").replace("min", "minute")
18
+ t = t.replace("secs", "seconds").replace("sec", "second")
19
+ t = t.replace("kms", "km").replace("kilometers", "km").replace("kilometres", "km")
20
+ t = t.replace("miles per hour", "mph")
21
+ t = t.replace("kilometers per hour", "kmph")
22
+ t = t.replace("kilometres per hour", "kmph")
23
+ return t.strip()
24
+
25
+
26
+ def _to_float(s: str) -> float:
27
+ return float(s)
28
+
29
+
30
+ def _fmt_num(x: float) -> str:
31
+ if abs(x - round(x)) < 1e-9:
32
+ return str(int(round(x)))
33
+ return f"{x:.6g}"
34
+
35
+
36
+ def _safe_positive(x: float) -> bool:
37
+ return math.isfinite(x) and x > 0
38
+
39
+
40
+ def _contains_any(text: str, phrases: List[str]) -> bool:
41
+ return any(p in text for p in phrases)
42
+
43
+
44
+ def _extract_number_before_unit(text: str, unit_pattern: str) -> Optional[float]:
45
+ m = re.search(rf"{_NUMBER}\s*{unit_pattern}", text)
46
+ if m:
47
+ return _to_float(m.group(1))
48
+ return None
49
+
50
+
51
+ def _extract_all_number_unit_pairs(text: str, unit_pattern: str) -> List[float]:
52
+ return [_to_float(x) for x in re.findall(rf"{_NUMBER}\s*{unit_pattern}", text)]
53
+
54
+
55
+ def _extract_speeds(text: str) -> List[float]:
56
+ speeds = []
57
+ speeds += _extract_all_number_unit_pairs(text, r"mph\b")
58
+ speeds += _extract_all_number_unit_pairs(text, r"kmph\b")
59
+ speeds += _extract_all_number_unit_pairs(text, r"m/s\b")
60
+ speeds += _extract_all_number_unit_pairs(text, r"ft/s\b")
61
+ return speeds
62
+
63
+
64
+ def _extract_times_in_hours(text: str) -> List[float]:
65
+ times = []
66
+ times += _extract_all_number_unit_pairs(text, r"hours?\b")
67
+ times += [x / 60.0 for x in _extract_all_number_unit_pairs(text, r"minutes?\b")]
68
+ times += [x / 3600.0 for x in _extract_all_number_unit_pairs(text, r"seconds?\b")]
69
+ return times
70
+
71
+
72
+ def _extract_distances(text: str) -> List[Tuple[float, str]]:
73
+ out: List[Tuple[float, str]] = []
74
+ for val in re.findall(rf"{_NUMBER}\s*(miles?|mi\b)", text):
75
+ out.append((_to_float(val[0] if isinstance(val, tuple) else val), "mile"))
76
+ for val in re.findall(rf"{_NUMBER}\s*(km\b)", text):
77
+ out.append((_to_float(val[0] if isinstance(val, tuple) else val), "km"))
78
+ for val in re.findall(rf"{_NUMBER}\s*(meters?|m\b)", text):
79
+ out.append((_to_float(val[0] if isinstance(val, tuple) else val), "m"))
80
+ for val in re.findall(rf"{_NUMBER}\s*(feet|foot|ft\b)", text):
81
+ out.append((_to_float(val[0] if isinstance(val, tuple) else val), "ft"))
82
+ return out
83
+
84
+
85
+ def _all_numbers(text: str) -> List[float]:
86
+ return [_to_float(x) for x in re.findall(_NUMBER, text)]
87
+
88
+
89
+ def _question_asks_time(text: str) -> bool:
90
+ return _contains_any(
91
+ text,
92
+ [
93
+ "how long", "what is the time", "find the time", "time taken",
94
+ "how many hours", "how many minutes", "when will", "time will"
95
+ ],
96
+ )
97
+
98
+
99
+ def _question_asks_speed(text: str) -> bool:
100
+ return _contains_any(
101
+ text,
102
+ [
103
+ "what is the speed", "find the speed", "how fast",
104
+ "average speed", "rate of", "what speed"
105
+ ],
106
+ )
107
+
108
+
109
+ def _question_asks_distance(text: str) -> bool:
110
+ return _contains_any(
111
+ text,
112
+ [
113
+ "what is the distance", "find the distance", "how far",
114
+ "distance traveled", "distance travelled"
115
+ ],
116
+ )
117
+
118
+
119
+ def _make_result(
120
+ internal_answer: str,
121
+ steps: List[str],
122
+ solved: bool = True,
123
+ topic: str = "distance_rate_time",
124
+ ) -> SolverResult:
125
+ return SolverResult(
126
+ domain="quant",
127
+ solved=solved,
128
+ topic=topic,
129
+ answer_value=None, # keep final answer hidden from user-facing reply
130
+ internal_answer=internal_answer,
131
+ steps=steps,
132
+ )
133
+
134
+
135
+ def _basic_formula_solver(text: str) -> Optional[SolverResult]:
136
+ speeds = _extract_speeds(text)
137
+ times = _extract_times_in_hours(text)
138
+ distances = _extract_distances(text)
139
+
140
+ # distance = speed * time
141
+ if _question_asks_distance(text) and speeds and times:
142
+ s = speeds[0]
143
+ t = times[0]
144
+ d = s * t
145
+ return _make_result(
146
+ internal_answer=_fmt_num(d),
147
+ steps=[
148
+ "Identify the two known quantities: speed and time.",
149
+ "Use the relationship distance = speed × time.",
150
+ f"Substitute the known values: distance = { _fmt_num(s) } × { _fmt_num(t) }.",
151
+ "Multiply to get the travel distance.",
152
+ ],
153
+ topic="distance_rate_time_basic",
154
+ )
155
+
156
+ # time = distance / speed
157
+ if _question_asks_time(text) and distances and speeds:
158
+ d = distances[0][0]
159
+ s = speeds[0]
160
+ if not _safe_positive(s):
161
+ return None
162
+ t = d / s
163
+ return _make_result(
164
+ internal_answer=_fmt_num(t),
165
+ steps=[
166
+ "Identify the known distance and speed.",
167
+ "Use the relationship time = distance ÷ speed.",
168
+ f"Substitute: time = { _fmt_num(d) } ÷ { _fmt_num(s) }.",
169
+ "Divide to get the travel time.",
170
+ ],
171
+ topic="distance_rate_time_basic",
172
+ )
173
+
174
+ # speed = distance / time
175
+ if _question_asks_speed(text) and distances and times:
176
+ d = distances[0][0]
177
+ t = times[0]
178
+ if not _safe_positive(t):
179
+ return None
180
+ s = d / t
181
+ return _make_result(
182
+ internal_answer=_fmt_num(s),
183
+ steps=[
184
+ "Identify the known distance and time.",
185
+ "Use the relationship speed = distance ÷ time.",
186
+ f"Substitute: speed = { _fmt_num(d) } ÷ { _fmt_num(t) }.",
187
+ "Divide to get the speed.",
188
+ ],
189
+ topic="distance_rate_time_basic",
190
+ )
191
+
192
+ return None
193
+
194
+
195
+ def _average_speed_total_trip_solver(text: str) -> Optional[SolverResult]:
196
+ if "average speed" not in text and "averaged" not in text:
197
+ return None
198
+
199
+ distances = _extract_distances(text)
200
+ times = _extract_times_in_hours(text)
201
+
202
+ # If total distance and total time are explicitly present
203
+ if _question_asks_speed(text) and distances and times:
204
+ d = sum(x[0] for x in distances)
205
+ t = sum(times)
206
+ if not _safe_positive(t):
207
+ return None
208
+ avg = d / t
209
+ return _make_result(
210
+ internal_answer=_fmt_num(avg),
211
+ steps=[
212
+ "For average speed, do not average the speeds directly.",
213
+ "First find total distance and total time.",
214
+ "Use average speed = total distance ÷ total time.",
215
+ f"Here that means average speed = { _fmt_num(d) } ÷ { _fmt_num(t) }.",
216
+ ],
217
+ topic="distance_rate_time_average_speed",
218
+ )
219
+
220
+ # equal-distance round trip average speed from two speeds
221
+ speeds = _extract_speeds(text)
222
+ if len(speeds) >= 2 and _contains_any(text, ["round trip", "same distance", "back", "there and back"]):
223
+ s1, s2 = speeds[0], speeds[1]
224
+ if _safe_positive(s1) and _safe_positive(s2):
225
+ avg = 2 * s1 * s2 / (s1 + s2)
226
+ return _make_result(
227
+ internal_answer=_fmt_num(avg),
228
+ steps=[
229
+ "This is an equal-distance average-speed setup.",
230
+ "When the distances are the same, average speed is not the arithmetic mean.",
231
+ "Use harmonic-mean logic: average speed = 2ab ÷ (a + b).",
232
+ f"Set a = { _fmt_num(s1) } and b = { _fmt_num(s2) }.",
233
+ ],
234
+ topic="distance_rate_time_average_speed",
235
+ )
236
 
237
+ return None
238
 
239
+
240
+ def _meeting_opposite_direction_solver(text: str) -> Optional[SolverResult]:
241
+ if not _contains_any(text, ["opposite", "toward each other", "towards each other", "meet", "meeting"]):
242
  return None
243
 
244
+ distances = _extract_distances(text)
245
+ speeds = _extract_speeds(text)
246
+
247
+ # Basic meet: total distance / sum of speeds
248
+ if distances and len(speeds) >= 2 and _question_asks_time(text):
249
+ total_distance = distances[0][0]
250
+ s1, s2 = speeds[0], speeds[1]
251
+ if _safe_positive(s1 + s2):
252
+ t = total_distance / (s1 + s2)
253
+ return _make_result(
254
+ internal_answer=_fmt_num(t),
255
+ steps=[
256
+ "For motion toward each other, the distances add but the meeting time is the same.",
257
+ "So use relative speed = speed₁ + speed₂.",
258
+ f"Relative speed = { _fmt_num(s1) } + { _fmt_num(s2) }.",
259
+ "Then use time = total distance ÷ relative speed.",
260
+ ],
261
+ topic="distance_rate_time_meeting",
262
+ )
263
+
264
+ # If asking distance to meeting point from one side
265
+ if distances and len(speeds) >= 2 and _question_asks_distance(text):
266
+ total_distance = distances[0][0]
267
+ s1, s2 = speeds[0], speeds[1]
268
+ if _safe_positive(s1 + s2):
269
+ t = total_distance / (s1 + s2)
270
+ d1 = s1 * t
271
+ return _make_result(
272
+ internal_answer=_fmt_num(d1),
273
+ steps=[
274
+ "At the meeting point, both travelers have moved for the same amount of time.",
275
+ "First find the shared meeting time using total distance ÷ (sum of speeds).",
276
+ "Then multiply that time by the speed of the traveler asked about.",
277
+ ],
278
+ topic="distance_rate_time_meeting",
279
+ )
280
+
281
+ return None
282
+
283
 
284
+ def _overtake_same_direction_solver(text: str) -> Optional[SolverResult]:
285
+ if not _contains_any(text, ["overtake", "overtakes", "catch up", "catches up", "same direction"]):
286
  return None
287
 
288
+ speeds = _extract_speeds(text)
289
+ times = _extract_times_in_hours(text)
290
+ nums = _all_numbers(text)
291
 
292
+ # Explicit delayed start and catch-up time with speed difference
293
+ # Example: freight is 20 mph slower, passenger overtakes in 3 hours, freight had 2-hour head start.
294
+ slower_by = None
295
+ m = re.search(rf"{_NUMBER}\s*(?:mph|kmph|m/s|ft/s)?\s*slower", text)
296
+ if m:
297
+ slower_by = _to_float(m.group(1))
298
 
299
+ if slower_by is not None and len(times) >= 2 and _question_asks_speed(text):
300
+ # Usually the smaller time is catch-up time; larger is head start or total.
301
+ t_small = min(times)
302
+ t_large = max(times)
303
+ # Need head start and catch-up time; if text says "2 hours after" and "in 3 hours"
304
+ head_start = None
305
+ catch_time = None
306
 
307
+ after_match = re.search(rf"{_NUMBER}\s*hours?\s*after", text)
308
+ in_match = re.search(rf"in\s*{_NUMBER}\s*hours?", text)
309
+ if after_match:
310
+ head_start = _to_float(after_match.group(1))
311
+ if in_match:
312
+ catch_time = _to_float(in_match.group(1))
313
+
314
+ if head_start is None or catch_time is None:
315
+ if len(times) >= 2:
316
+ head_start = min(times)
317
+ catch_time = max(times)
318
+
319
+ if head_start is not None and catch_time is not None:
320
+ # x * catch_time = (x - slower_by) * (catch_time + head_start)
321
+ denom = head_start
322
+ if abs(denom) < 1e-12:
323
+ return None
324
+ x = slower_by * (catch_time + head_start) / head_start
325
+ if _safe_positive(x):
326
+ return _make_result(
327
+ internal_answer=_fmt_num(x),
328
+ steps=[
329
+ "In an overtaking problem, the distances are equal at the catch-up point.",
330
+ "Let the faster speed be the unknown.",
331
+ "Express the slower speed using the stated difference.",
332
+ "Use time carefully: the earlier starter travels longer because of the head start.",
333
+ "Set distance of faster traveler equal to distance of slower traveler and solve.",
334
+ ],
335
+ topic="distance_rate_time_overtake",
336
+ )
337
+
338
+ # Generic relative-speed catch-up: time = lead distance / (faster - slower)
339
+ if len(speeds) >= 2 and distances and _question_asks_time(text):
340
+ faster = max(speeds[0], speeds[1])
341
+ slower = min(speeds[0], speeds[1])
342
+ lead_distance = distances[0][0]
343
+ if faster <= slower:
344
+ return None
345
+ t = lead_distance / (faster - slower)
346
+ return _make_result(
347
+ internal_answer=_fmt_num(t),
348
  steps=[
349
+ "For same-direction catch-up, use relative speed = faster speed - slower speed.",
350
+ f"Relative speed = { _fmt_num(faster) } - { _fmt_num(slower) }.",
351
+ "Then use time = lead distance ÷ relative speed.",
352
+ ],
353
+ topic="distance_rate_time_overtake",
354
  )
355
 
356
+ return None
 
357
 
 
 
358
 
359
+ def _round_trip_solver(text: str) -> Optional[SolverResult]:
360
+ if not _contains_any(text, ["round trip", "returns", "back", "same distance", "there and back"]):
361
+ return None
362
+
363
+ speeds = _extract_speeds(text)
364
+ times = _extract_times_in_hours(text)
365
+ distances = _extract_distances(text)
366
+
367
+ # Boat/current round trip where current speed given and times given; ask calm-water speed
368
+ if _contains_any(text, ["current", "downstream", "upstream", "against the current", "with the current"]):
369
+ current = None
370
+ m = re.search(rf"current(?: of)?\s*{_NUMBER}\s*(?:mph|kmph|m/s|ft/s)?", text)
371
+ if m:
372
+ current = _to_float(m.group(1))
373
+ else:
374
+ m = re.search(rf"{_NUMBER}\s*(?:mph|kmph|m/s|ft/s)\s*(?:current)", text)
375
+ if m:
376
+ current = _to_float(m.group(1))
377
+
378
+ if current is not None and len(times) >= 2 and _question_asks_speed(text):
379
+ t1, t2 = times[0], times[1]
380
+ # equal distances: (b + c)t1 = (b - c)t2
381
+ denom = t2 - t1
382
+ if abs(denom) < 1e-12:
383
+ return None
384
+ b = current * (t1 + t2) / denom
385
+ if _safe_positive(b) and b > current:
386
+ return _make_result(
387
+ internal_answer=_fmt_num(b),
388
+ steps=[
389
+ "This is a same-distance out-and-back problem.",
390
+ "So the downstream distance equals the upstream distance.",
391
+ "Let the calm-water speed be the unknown.",
392
+ "Then downstream speed = boat speed + current and upstream speed = boat speed - current.",
393
+ "Set the two distances equal and solve.",
394
+ ],
395
+ topic="distance_rate_time_stream_current",
396
+ )
397
+
398
+ # Same-distance, two different speeds, total time, ask one leg or distance
399
+ if len(speeds) >= 2 and times and _question_asks_distance(text):
400
+ total_time = sum(times)
401
+ s1, s2 = speeds[0], speeds[1]
402
+ if _safe_positive(s1) and _safe_positive(s2):
403
+ one_way_distance = total_time / (1 / s1 + 1 / s2)
404
+ return _make_result(
405
+ internal_answer=_fmt_num(one_way_distance),
406
+ steps=[
407
+ "For a round trip, the outbound and return distances are the same.",
408
+ "Write time for each leg as distance ÷ speed.",
409
+ "Add the two times and match that to the total time.",
410
+ "Then solve for the one-way distance.",
411
+ ],
412
+ topic="distance_rate_time_round_trip",
413
+ )
414
+
415
+ # If total distance of round trip and total time given, ask average speed
416
+ if distances and times and _question_asks_speed(text):
417
+ total_distance = sum(x[0] for x in distances)
418
+ total_time = sum(times)
419
+ if _safe_positive(total_time):
420
+ avg = total_distance / total_time
421
+ return _make_result(
422
+ internal_answer=_fmt_num(avg),
423
+ steps=[
424
+ "For average speed on a round trip, use total distance ÷ total time.",
425
+ "Do not average the two speeds directly unless the times are equal, which is not guaranteed.",
426
+ "Compute the total distance and total time first.",
427
+ ],
428
+ topic="distance_rate_time_round_trip",
429
+ )
430
+
431
+ return None
432
+
433
+
434
+ def _stream_current_direct_solver(text: str) -> Optional[SolverResult]:
435
+ if not _contains_any(text, ["current", "stream", "downstream", "upstream"]):
436
+ return None
437
+
438
+ speeds = _extract_speeds(text)
439
+
440
+ # direct downstream/upstream speeds given -> ask calm water or current
441
+ if len(speeds) >= 2 and _question_asks_speed(text):
442
+ s1, s2 = speeds[0], speeds[1]
443
+
444
+ if _contains_any(text, ["calm water", "still water", "boat's speed"]):
445
+ calm = (s1 + s2) / 2
446
+ return _make_result(
447
+ internal_answer=_fmt_num(calm),
448
+ steps=[
449
+ "For boat/current setups:",
450
+ "downstream speed = boat speed + current",
451
+ "upstream speed = boat speed - current",
452
+ "Add the two equations to isolate twice the calm-water speed.",
453
+ ],
454
+ topic="distance_rate_time_stream_current",
455
+ )
456
+
457
+ if _contains_any(text, ["speed of the current", "current speed", "stream speed"]):
458
+ current = abs(s1 - s2) / 2
459
+ return _make_result(
460
+ internal_answer=_fmt_num(current),
461
+ steps=[
462
+ "For boat/current setups:",
463
+ "downstream speed = boat speed + current",
464
+ "upstream speed = boat speed - current",
465
+ "Subtract the two equations to isolate twice the current speed.",
466
+ ],
467
+ topic="distance_rate_time_stream_current",
468
+ )
469
 
470
+ return None
471
 
472
+
473
+ def _average_speed_trick_solver(text: str) -> Optional[SolverResult]:
474
+ # famous impossible target-average setup
475
+ if "average" not in text:
476
+ return None
477
+
478
+ lap_match = re.search(rf"{_NUMBER}\s*(?:mile|mi|km|m)\b.*?(?:track|lap)", text)
479
+ target_match = re.search(rf"average\s*{_NUMBER}\s*(?:mph|kmph|m/s|ft/s)", text)
480
+ first_leg_match = re.search(rf"first\s*(?:lap|mile|part).*?{_NUMBER}\s*(?:mph|kmph|m/s|ft/s)", text)
481
+
482
+ if not (lap_match and target_match and first_leg_match):
483
+ return None
484
+
485
+ dist_each = _to_float(lap_match.group(1))
486
+ target = _to_float(target_match.group(1))
487
+ first_speed = _to_float(first_leg_match.group(1))
488
+
489
+ if not (_safe_positive(dist_each) and _safe_positive(target) and _safe_positive(first_speed)):
490
+ return None
491
+
492
+ total_distance = 2 * dist_each
493
+ allowed_total_time = total_distance / target
494
+ first_leg_time = dist_each / first_speed
495
+
496
+ if first_leg_time >= allowed_total_time - 1e-12:
497
+ return _make_result(
498
+ internal_answer="impossible",
499
  steps=[
500
+ "This is a target-average-speed trap.",
501
+ "Convert the target average into the maximum total time allowed for the full trip.",
502
+ "Then compare that allowed total time with the time already used on the first leg.",
503
+ "If the first leg already uses all available time (or more), no finite second-leg speed can fix it.",
504
+ ],
505
+ topic="distance_rate_time_average_speed_trick",
506
  )
507
 
508
+ return None
509
+
510
+
511
+ def _component_sum_solver(text: str) -> Optional[SolverResult]:
512
+ # Two-part journey where total distance and total time given, one segment distance asked
513
+ if not (_question_asks_distance(text) and _contains_any(text, ["entire distance", "entire trip", "total distance", "total trip"])):
514
+ return None
515
+
516
+ speeds = _extract_speeds(text)
517
+ distances = _extract_distances(text)
518
+ times = _extract_times_in_hours(text)
519
+
520
+ if len(speeds) >= 2 and distances and times:
521
+ total_distance = distances[0][0]
522
+ total_time = times[0] if len(times) == 1 else sum(times)
523
+
524
+ # Let x be the second segment distance:
525
+ # (total_distance - x)/s1 + x/s2 = total_time
526
+ s1, s2 = speeds[0], speeds[1]
527
+ denom = (1 / s2) - (1 / s1)
528
+ rhs = total_time - (total_distance / s1)
529
+ if abs(denom) < 1e-12:
530
+ return None
531
+ x = rhs / denom
532
+ if math.isfinite(x):
533
+ return _make_result(
534
+ internal_answer=_fmt_num(x),
535
+ steps=[
536
+ "Break the trip into components.",
537
+ "Let the unknown segment distance be x, so the other segment is total distance - x.",
538
+ "Write time for each segment as distance ÷ speed.",
539
+ "Add the segment times and set that equal to the total trip time.",
540
+ "Solve the resulting linear equation.",
541
+ ],
542
+ topic="distance_rate_time_components",
543
+ )
544
+
545
+ return None
546
+
547
+
548
+ def solve_distance_rate_time(text: str) -> Optional[SolverResult]:
549
+ raw = _clean(text)
550
+ lower = raw.lower()
551
+
552
+ if not _contains_any(
553
+ lower,
554
+ [
555
+ "distance", "speed", "time", "rate", "mph", "kmph", "m/s", "ft/s",
556
+ "meet", "meeting", "overtake", "catch up", "current", "stream",
557
+ "upstream", "downstream", "round trip", "average speed", "lap"
558
+ ],
559
+ ):
560
+ return None
561
+
562
+ solvers = [
563
+ _average_speed_trick_solver,
564
+ _stream_current_direct_solver,
565
+ _round_trip_solver,
566
+ _meeting_opposite_direction_solver,
567
+ _overtake_same_direction_solver,
568
+ _component_sum_solver,
569
+ _average_speed_total_trip_solver,
570
+ _basic_formula_solver,
571
+ ]
572
+
573
+ for solver in solvers:
574
+ try:
575
+ result = solver(lower)
576
+ if result is not None:
577
+ return result
578
+ except Exception:
579
+ continue
580
+
581
  return None