Option A (text-faithful): RateProblem gains time_unit (the duration's ORIGINAL unit; time stays int; defaults to the rate denominator). The SOLVER converts via convert_time -> exact Fraction -> int-or-refuse(non_integer); Fraction never leaves the solver, no floats. The reader accepts a convertible (minute<->hour) duration mismatch (keeps original time_unit); a non-convertible one still refuses rate_unit_mismatch. Signature includes time_unit ('30 minutes' != '30 hours').
Gold: r3-09 flips reader_refuses -> SOLVED (60 mph for 30 min = 30 mile, exact 1/2 hour; distractor 1800 rejected); new r3-13 non-convertible (3 gallons) stays rate_unit_mismatch. R3 gold 13/13; reader 9/0/0 -> answers 7/0/6 (setup_correct 8->9). Failure-family: rate_unit_mismatch now non-convertible-only -> still unsupported_rate_duration proposal surface; router-hygiene invariant stays green; R1 7/0/3, R2 10/0/0 unchanged. 119-test smoke green incl. invariants. Off-serving, no float path.
52 lines
2.1 KiB
Python
52 lines
2.1 KiB
Python
"""Exact rational time-unit conversion for R3.2 (single-rate only).
|
||
|
||
The single new capability of R3.2: a duration whose unit differs from the rate's denominator may be
|
||
**converted** when both are known time units (``minute`` ↔ ``hour``), instead of refusing. The
|
||
conversion is **exact rational** (``fractions.Fraction``) — never a float, never a decimal:
|
||
|
||
```text
|
||
30 minutes -> Fraction(1, 2) hour
|
||
90 minutes -> Fraction(3, 2) hour
|
||
2 hours -> Fraction(120) minute
|
||
```
|
||
|
||
So ``60 mile/hour × 30 minute`` becomes ``60 × 1/2 = 30 mile`` (exact). A non-time / non-convertible
|
||
pair (``minute`` ↔ ``mile``) raises ``ConversionError`` and the caller still refuses
|
||
``rate_unit_mismatch`` — convertibility is the only thing R3.2 adds.
|
||
|
||
Deliberately tiny: ONLY ``minute`` ↔ ``hour`` in v1. No length (mile↔km), no currency
|
||
(dollar↔cent), no compound conversions, no clock-time intervals. Deterministic.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from fractions import Fraction
|
||
|
||
|
||
class ConversionError(ValueError):
|
||
"""The units are not a known convertible pair — the caller must refuse."""
|
||
|
||
|
||
#: Each known time unit as an exact rational number of the base unit (one hour).
|
||
_TIME_IN_HOURS: dict[str, Fraction] = {
|
||
"hour": Fraction(1),
|
||
"minute": Fraction(1, 60),
|
||
}
|
||
|
||
|
||
def is_convertible(from_unit: str, to_unit: str) -> bool:
|
||
"""Whether a conversion exists — the identity (same unit, any kind) or a known time-unit pair."""
|
||
return from_unit == to_unit or (from_unit in _TIME_IN_HOURS and to_unit in _TIME_IN_HOURS)
|
||
|
||
|
||
def convert_time(value: int, from_unit: str, to_unit: str) -> Fraction:
|
||
"""Exact rational conversion of *value* ``from_unit`` into *to_unit*. Identity for the same unit
|
||
(any kind, e.g. ``box``); a known time-unit pair otherwise; refuses anything else."""
|
||
if from_unit == to_unit:
|
||
return Fraction(value)
|
||
if from_unit not in _TIME_IN_HOURS or to_unit not in _TIME_IN_HOURS:
|
||
raise ConversionError(f"no exact conversion {from_unit!r} -> {to_unit!r}")
|
||
return Fraction(value) * _TIME_IN_HOURS[from_unit] / _TIME_IN_HOURS[to_unit]
|
||
|
||
|
||
__all__ = ["ConversionError", "convert_time", "is_convertible"]
|