generate/rate_comprehension/units.py: BaseUnit + RateUnit (numerator/denominator) + the three single-rate compositions — rate_times_time (mile/hour × hour = mile; refuses mile/hour × minute), rate_from_quantity_over_time (mile ÷ hour = mile/hour), time_from_quantity_over_rate (mile ÷ mile/hour = hour; refuses dollar ÷ mile/hour). A non-composing op raises UnitError (the wrong=0 dimensional gate), never fabricates a unit. Deliberately tiny: bare-name units, no generic dimensional universe, no semantic unit-class typing, no unit conversion, no compound-of-compound. Fresh off-serving organ (generate/rate_comprehension/). 10 tests incl. all three refusals + consistent composition.
90 lines
3.6 KiB
Python
90 lines
3.6 KiB
Python
"""Compound-unit algebra for the R3 single-rate organ (R3a).
|
||
|
||
The genuinely new substrate of R3: a **rate carries a compound unit** — ``mile/hour`` is
|
||
``quantity / time`` — and the three single-rate operations must verify that units *compose*:
|
||
|
||
```text
|
||
rate × time -> quantity (mile/hour × hour = mile) [time must be the rate's denominator]
|
||
quantity ÷ time -> rate (mile ÷ hour = mile/hour)
|
||
quantity ÷ rate -> time (mile ÷ mile/hour = hour) [quantity must be the rate's numerator]
|
||
```
|
||
|
||
A non-composing operation REFUSES (``UnitError``) rather than producing a nonsense unit —
|
||
``mile/hour × minute``, ``students/hour × mile``, ``dollar ÷ mile/hour``. This is the wrong=0
|
||
boundary for R3: the dimensional check refuses before any arithmetic runs.
|
||
|
||
Deliberately tiny. Units are bare names; there is **no** generic dimensional-analysis universe
|
||
and **no** semantic unit-class typing (v1 cannot itself know "dollar is not a time" — the gold and
|
||
reader only admit sensible single-rate problems). Compound-of-compound and rate/rate are simply
|
||
not representable (``RateUnit`` is one numerator over one denominator). No unit conversion (v1
|
||
never turns minutes into hours). Deterministic; no clock, no randomness.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
|
||
|
||
class UnitError(ValueError):
|
||
"""A non-composing unit operation — refuse, never fabricate a unit."""
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class BaseUnit:
|
||
"""A simple unit of quantity or time — ``mile``, ``hour``, ``dollar``, ``widget``."""
|
||
|
||
name: str
|
||
|
||
def __post_init__(self) -> None:
|
||
if not self.name or not isinstance(self.name, str):
|
||
raise UnitError(f"BaseUnit needs a non-empty name; got {self.name!r}")
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class RateUnit:
|
||
"""A compound rate unit ``numerator / denominator`` — ``mile/hour``, ``dollar/hour``."""
|
||
|
||
numerator: str
|
||
denominator: str
|
||
|
||
def __post_init__(self) -> None:
|
||
if not self.numerator or not self.denominator:
|
||
raise UnitError(f"RateUnit needs non-empty units; got {self!r}")
|
||
if self.numerator == self.denominator:
|
||
raise UnitError(f"a rate's numerator and denominator must differ; got {self}")
|
||
|
||
def __str__(self) -> str:
|
||
return f"{self.numerator}/{self.denominator}"
|
||
|
||
|
||
def rate_times_time(rate: RateUnit, time: BaseUnit) -> BaseUnit:
|
||
"""``rate × time -> quantity`` (``mile/hour × hour = mile``). The time unit MUST be the rate's
|
||
denominator, else REFUSE (``mile/hour × minute``)."""
|
||
if time.name != rate.denominator:
|
||
raise UnitError(f"{rate} × {time.name}: the time unit must be {rate.denominator!r}")
|
||
return BaseUnit(rate.numerator)
|
||
|
||
|
||
def rate_from_quantity_over_time(quantity: BaseUnit, time: BaseUnit) -> RateUnit:
|
||
"""``quantity ÷ time -> rate`` (``mile ÷ hour = mile/hour``)."""
|
||
if quantity.name == time.name:
|
||
raise UnitError(f"{quantity.name} ÷ {time.name}: quantity and time units must differ")
|
||
return RateUnit(quantity.name, time.name)
|
||
|
||
|
||
def time_from_quantity_over_rate(quantity: BaseUnit, rate: RateUnit) -> BaseUnit:
|
||
"""``quantity ÷ rate -> time`` (``mile ÷ mile/hour = hour``). The quantity unit MUST be the
|
||
rate's numerator, else REFUSE (``dollar ÷ mile/hour``)."""
|
||
if quantity.name != rate.numerator:
|
||
raise UnitError(f"{quantity.name} ÷ {rate}: the quantity unit must be {rate.numerator!r}")
|
||
return BaseUnit(rate.denominator)
|
||
|
||
|
||
__all__ = [
|
||
"BaseUnit",
|
||
"RateUnit",
|
||
"UnitError",
|
||
"rate_from_quantity_over_time",
|
||
"rate_times_time",
|
||
"time_from_quantity_over_rate",
|
||
]
|