generate/rate_comprehension/conversion.py: convert_time(value, from_unit, to_unit) -> Fraction for known time units (minute<->hour), exact rational (fractions.Fraction), NO floats. is_convertible gates it. 30 minute -> Fraction(1,2) hour; non-time/non-convertible (minute<->mile, dollar<->hour) raises ConversionError so the caller still refuses rate_unit_mismatch. Tiny: only minute<->hour in v1 (length/currency/compound deferred). 6 tests incl. never-a-float guard.
49 lines
1.9 KiB
Python
49 lines
1.9 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 *from_unit* and *to_unit* are both known time units (so a conversion exists)."""
|
||
return 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*. Refuses unknown units."""
|
||
if not is_convertible(from_unit, to_unit):
|
||
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"]
|