core/generate/rate_comprehension/conversion.py
Shay 89321e617d feat(rate): R3.2 exact-rational time-unit conversion primitive (R3.2a)
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.
2026-06-08 05:30:57 -07:00

49 lines
1.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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"]