commit d82d4a21c8010a39d982560fd6096eef8c0dce62
parent 117e865fc88083e3ffc81f258297bbe6f8139088
Author: triesap <tyson@radroots.org>
Date: Mon, 21 Sep 2026 12:55:52 +0000
core: add unit dimensions and conversion rules
Diffstat:
2 files changed, 106 insertions(+), 0 deletions(-)
diff --git a/src/hyf_core/domain/units.mojo b/src/hyf_core/domain/units.mojo
@@ -0,0 +1,73 @@
+from hyf_core.domain.quantity import Quantity, new_quantity
+
+
+@fieldwise_init
+struct ConversionRule(Copyable, Movable):
+ var from_unit: String
+ var to_unit: String
+ var numerator: Int
+ var denominator: Int
+ var version: String
+
+
+def unit_dimension(unit: String) -> String:
+ var lowered = unit.lower()
+ if lowered == "kg" or lowered == "lb" or lowered == "g" or lowered == "oz":
+ return "mass"
+ if lowered == "l" or lowered == "ml":
+ return "volume"
+ if lowered == "each" or lowered == "box" or lowered == "bunch":
+ return "count"
+ return "unknown"
+
+
+def _power_of_ten_exponent(denominator: Int) raises -> Int:
+ if denominator <= 0:
+ raise Error("conversion denominator must be positive")
+ var remaining = denominator
+ var exponent = 0
+ while remaining > 1:
+ if remaining % 10 != 0:
+ raise Error(
+ "unsupported conversion denominator (must be a power of ten)"
+ )
+ remaining //= 10
+ exponent += 1
+ return exponent
+
+
+def conversion_rule(
+ from_unit: String,
+ to_unit: String,
+ numerator: Int,
+ denominator: Int,
+ version: String,
+) raises -> ConversionRule:
+ if from_unit.strip() == "" or to_unit.strip() == "":
+ raise Error("conversion requires from_unit and to_unit")
+ if numerator <= 0:
+ raise Error("conversion numerator must be positive")
+ if unit_dimension(from_unit) != unit_dimension(to_unit):
+ raise Error("conversion rule crosses incompatible dimensions")
+ _ = _power_of_ten_exponent(denominator)
+ return ConversionRule(
+ from_unit=String(from_unit),
+ to_unit=String(to_unit),
+ numerator=numerator,
+ denominator=denominator,
+ version=String(version),
+ )
+
+
+def apply_conversion(quantity: Quantity, rule: ConversionRule) raises -> Quantity:
+ if quantity.unit != rule.from_unit:
+ raise Error("conversion rule does not apply to unit " + quantity.unit)
+ if quantity.dimension != unit_dimension(rule.to_unit):
+ raise Error("conversion rule crosses incompatible dimensions")
+ var exponent = _power_of_ten_exponent(rule.denominator)
+ var result = quantity.copy()
+ result.value = quantity.value * rule.numerator
+ result.scale = quantity.scale + exponent
+ result.unit = String(rule.to_unit)
+ result.dimension = String(unit_dimension(rule.to_unit))
+ return result^
diff --git a/tests/test_domain.mojo b/tests/test_domain.mojo
@@ -130,3 +130,36 @@ def test_quantity_add_overflow_and_approximation() raises:
_ = quantity_add(huge, exact)
var adjustment = new_quantity(-5, 0, "kg", "mass", "exact")
assert_true(quantity_is_negative(adjustment))
+
+
+from hyf_core.domain.units import (
+ apply_conversion,
+ conversion_rule,
+ unit_dimension,
+)
+
+
+def test_unit_dimensions_and_exact_mass_conversion() raises:
+ assert_equal(unit_dimension("lb"), "mass")
+ assert_equal(unit_dimension("ml"), "volume")
+ assert_equal(unit_dimension("widget"), "unknown")
+
+ var pounds = new_quantity(80, 0, "lb", "mass", "exact")
+ var rule = conversion_rule("lb", "kg", 45359237, 100000000, "test-1")
+ var kilograms = apply_conversion(pounds, rule)
+ assert_equal(kilograms.unit, "kg")
+ assert_equal(kilograms.dimension, "mass")
+ assert_equal(kilograms.value, 3628738960)
+ assert_equal(kilograms.scale, 8)
+
+
+def test_unit_conversion_rejects_dimension_mismatch_and_unknown_denominator() raises:
+ with assert_raises():
+ _ = conversion_rule("lb", "l", 1, 1, "v1")
+ with assert_raises():
+ _ = conversion_rule("lb", "kg", 1, 3, "v1")
+ var pounds = new_quantity(1, 0, "lb", "mass", "exact")
+ var kg_rule = conversion_rule("lb", "kg", 45359237, 100000000, "test-1")
+ with assert_raises():
+ _ = apply_conversion(new_quantity(1, 0, "kg", "mass", "exact"), kg_rule)
+ _ = pounds