From 72bd692dc529e0255ea30c994c51ab6a3aaa0ec1 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 12:52:15 +0000 Subject: [PATCH 01/11] Implement ArbitraryUnit class --- sasdata/quantities/_units_base.py | 109 ++++++++++++++++++++++++++++++ sasdata/quantities/accessors.py | 8 +++ sasdata/quantities/units.py | 109 ++++++++++++++++++++++++++++++ test/quantities/utest_units.py | 19 +++++- 4 files changed, 244 insertions(+), 1 deletion(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 5543f1d4e..881973872 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -316,6 +316,115 @@ def startswith(self, prefix: str) -> bool: or (self.ascii_symbol is not None and self.ascii_symbol.lower().startswith(prefix)) \ or (self.symbol is not None and self.symbol.lower().startswith(prefix)) + +class ArbitraryUnit(NamedUnit): + """A unit for an unknown quantity + + While this library attempts to handle all known SI units, it is + likely that users will want to express quantities of arbitrary + units (for example, calculating donuts per person for a meeting). + The arbitrary unit allows for these unforseeable quantities.""" + + def __init__(self, numerator: str | list[str], denominator: None | list[str] = None): + match numerator: + case str(name): + self._numerator = [name] + case _: + self._numerator = numerator + match denominator: + case None: + self._denominator = [] + case _: + self._denominator = denominator + self._unit = Unit(1, Dimensions()) # Unitless + print("Made") + + super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) + print("Make more") + + def _name(self): + match (self._numerator, self._denominator): + case ([], []): + return "" + case (_, []): + return " ".join(self._numerator) + case ([], _): + return "1 / " + " ".join(self._denominator) + case _: + return " ".join(self._numerator) + " / " + " ".join(self._denominator) + + def __eq__(self, other): + match other: + case ArbitraryUnit(): + return self._numerator == other._numerator and self._denominator == other._denominator and self._unit == other._unit + case Unit(): + return not self._numerator and not self._denominator and self._unit == other + + + def __mul__(self: Self, other: "Unit"): + # if isinstance(other, Unit): + # return Unit(self.scale * other.scale, self.dimensions * other.dimensions) + # elif isinstance(other, (int, float)): + # return Unit(other * self.scale, self.dimensions) + return NotImplemented + + def __truediv__(self: Self, other: "Unit"): + # if isinstance(other, Unit): + # return Unit(self.scale / other.scale, self.dimensions / other.dimensions) + # elif isinstance(other, (int, float)): + # return Unit(self.scale / other, self.dimensions) + # else: + return NotImplemented + + def __rtruediv__(self: Self, other: "Unit"): + # if isinstance(other, Unit): + # return Unit(other.scale / self.scale, other.dimensions / self.dimensions) + # elif isinstance(other, (int, float)): + # return Unit(other / self.scale, self.dimensions ** -1) + # else: + return NotImplemented + + def __pow__(self, power: int | float): + # if not isinstance(power, int | float): + # return NotImplemented + + return Unit(self.scale**power, self.dimensions**power) + + + def equivalent(self: Self, other: "Unit"): + match other: + case ArbitraryUnit(): + return self._unit.equivalent(other._unit) and sorted(self._numerator) == sorted(other._numerator) and sorted(self._denominator) == sorted(other._denominator) + case _: + return False + + def __eq__(self: Self, other: "Unit"): + """FIXME: TODO""" + return self.equivalent(other) and np.abs(np.log(self.scale/other.scale)) < 1e-5 + + def si_equivalent(self): + """ Get the SI unit corresponding to this unit""" + """FIXME: TODO""" + return Unit(1, self.dimensions) + + def _format_unit(self, format_process: list["UnitFormatProcessor"]): + """FIXME: TODO""" + for processor in format_process: + pass + + def __repr__(self): + """FIXME: TODO""" + if self.scale == 1: + # We're in SI + return self.dimensions.si_repr() + + else: + return f"Unit[{self.scale}, {self.dimensions}]" + + @staticmethod + def parse(unit_string: str) -> "Unit": + """FIXME: TODO""" + pass # # Parsing plan: # Require unknown amounts of units to be explicitly positive or negative? diff --git a/sasdata/quantities/accessors.py b/sasdata/quantities/accessors.py index d23268544..159d33d49 100644 --- a/sasdata/quantities/accessors.py +++ b/sasdata/quantities/accessors.py @@ -9479,6 +9479,14 @@ def radians(self) -> T: else: return quantity.in_units_of(units.radians) + @property + def rotations(self) -> T: + quantity = self.quantity + if quantity is None: + return None + else: + return quantity.in_units_of(units.rotations) + class SolidangleAccessor[T](QuantityAccessor[T]): diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index fe840ab85..94479bddd 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -401,6 +401,115 @@ def startswith(self, prefix: str) -> bool: or (self.ascii_symbol is not None and self.ascii_symbol.lower().startswith(prefix)) \ or (self.symbol is not None and self.symbol.lower().startswith(prefix)) + +class ArbitraryUnit(NamedUnit): + """A unit for an unknown quantity + + While this library attempts to handle all known SI units, it is + likely that users will want to express quantities of arbitrary + units (for example, calculating donuts per person for a meeting). + The arbitrary unit allows for these unforseeable quantities.""" + + def __init__(self, numerator: str | list[str], denominator: None | list[str] = None): + match numerator: + case str(name): + self._numerator = [name] + case _: + self._numerator = numerator + match denominator: + case None: + self._denominator = [] + case _: + self._denominator = denominator + self._unit = Unit(1, Dimensions()) # Unitless + print("Made") + + super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) + print("Make more") + + def _name(self): + match (self._numerator, self._denominator): + case ([], []): + return "" + case (_, []): + return " ".join(self._numerator) + case ([], _): + return "1 / " + " ".join(self._denominator) + case _: + return " ".join(self._numerator) + " / " + " ".join(self._denominator) + + def __eq__(self, other): + match other: + case ArbitraryUnit(): + return self._numerator == other._numerator and self._denominator == other._denominator and self._unit == other._unit + case Unit(): + return not self._numerator and not self._denominator and self._unit == other + + + def __mul__(self: Self, other: "Unit"): + # if isinstance(other, Unit): + # return Unit(self.scale * other.scale, self.dimensions * other.dimensions) + # elif isinstance(other, (int, float)): + # return Unit(other * self.scale, self.dimensions) + return NotImplemented + + def __truediv__(self: Self, other: "Unit"): + # if isinstance(other, Unit): + # return Unit(self.scale / other.scale, self.dimensions / other.dimensions) + # elif isinstance(other, (int, float)): + # return Unit(self.scale / other, self.dimensions) + # else: + return NotImplemented + + def __rtruediv__(self: Self, other: "Unit"): + # if isinstance(other, Unit): + # return Unit(other.scale / self.scale, other.dimensions / self.dimensions) + # elif isinstance(other, (int, float)): + # return Unit(other / self.scale, self.dimensions ** -1) + # else: + return NotImplemented + + def __pow__(self, power: int | float): + # if not isinstance(power, int | float): + # return NotImplemented + + return Unit(self.scale**power, self.dimensions**power) + + + def equivalent(self: Self, other: "Unit"): + match other: + case ArbitraryUnit(): + return self._unit.equivalent(other._unit) and sorted(self._numerator) == sorted(other._numerator) and sorted(self._denominator) == sorted(other._denominator) + case _: + return False + + def __eq__(self: Self, other: "Unit"): + """FIXME: TODO""" + return self.equivalent(other) and np.abs(np.log(self.scale/other.scale)) < 1e-5 + + def si_equivalent(self): + """ Get the SI unit corresponding to this unit""" + """FIXME: TODO""" + return Unit(1, self.dimensions) + + def _format_unit(self, format_process: list["UnitFormatProcessor"]): + """FIXME: TODO""" + for processor in format_process: + pass + + def __repr__(self): + """FIXME: TODO""" + if self.scale == 1: + # We're in SI + return self.dimensions.si_repr() + + else: + return f"Unit[{self.scale}, {self.dimensions}]" + + @staticmethod + def parse(unit_string: str) -> "Unit": + """FIXME: TODO""" + pass # # Parsing plan: # Require unknown amounts of units to be explicitly positive or negative? diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index 3bc775313..621117a32 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -1,7 +1,7 @@ import math import sasdata.quantities.units as units -from sasdata.quantities.units import Unit +from sasdata.quantities.units import Unit, ArbitraryUnit class EqualUnits: @@ -12,6 +12,7 @@ def __init__(self, test_name: str, *units): def run_test(self): for i, unit_1 in enumerate(self.units): for unit_2 in self.units[i + 1 :]: + print(unit_1, unit_2) assert unit_1.equivalent(unit_2), "Units should be equivalent" assert unit_1 == unit_2, "Units should be equal" @@ -38,6 +39,10 @@ def run_test(self): for unit_2 in self.units[i + 1 :]: assert not unit_1.equivalent(unit_2), "Units should not be equivalent" +pizza_a = ArbitraryUnit("Pizzas") +pizza_b = ArbitraryUnit(["Pizzas"]) +print(f"A: {pizza_a}\tB: {pizza_b}") + tests = [ @@ -63,6 +68,18 @@ def run_test(self): (units.rotations/units.minutes), (units.hertz)), + EqualUnits("Arbitrary Units", + ArbitraryUnit("Pizzas"), + ArbitraryUnit(["Pizzas"])), + + EqualUnits("Arbitrary Units", + ArbitraryUnit("Slices", denominator=["Pizzas"]), + ArbitraryUnit(["Slices"], denominator=["Pizzas"])), + + DissimilarUnits("Different Arbitrary Units", + ArbitraryUnit("Pizzas"), + ArbitraryUnit(["Doughnuts"])), + ] From abc78d6d93ebd8824208d1c8807408a55bb254d5 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 14:26:39 +0000 Subject: [PATCH 02/11] Refactor utest_units.py The tests now properly appear in the pytest list of tests. Additionally, the tests are given readable parameterised names, but display the actual values on failure. --- sasdata/quantities/_units_base.py | 14 ++-- sasdata/quantities/units.py | 10 ++- test/quantities/utest_units.py | 114 ++++++++++++------------------ 3 files changed, 54 insertions(+), 84 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 881973872..1a7418127 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -398,10 +398,6 @@ def equivalent(self: Self, other: "Unit"): case _: return False - def __eq__(self: Self, other: "Unit"): - """FIXME: TODO""" - return self.equivalent(other) and np.abs(np.log(self.scale/other.scale)) < 1e-5 - def si_equivalent(self): """ Get the SI unit corresponding to this unit""" """FIXME: TODO""" @@ -414,12 +410,10 @@ def _format_unit(self, format_process: list["UnitFormatProcessor"]): def __repr__(self): """FIXME: TODO""" - if self.scale == 1: - # We're in SI - return self.dimensions.si_repr() - - else: - return f"Unit[{self.scale}, {self.dimensions}]" + result = self._name() + if self._unit.__repr__(): + result += f" {self._unit.__repr__()}" + return result @staticmethod def parse(unit_string: str) -> "Unit": diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index 94479bddd..7fbf9750d 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -499,12 +499,10 @@ def _format_unit(self, format_process: list["UnitFormatProcessor"]): def __repr__(self): """FIXME: TODO""" - if self.scale == 1: - # We're in SI - return self.dimensions.si_repr() - - else: - return f"Unit[{self.scale}, {self.dimensions}]" + result = self._name() + if self._unit.__repr__(): + result += f" {self._unit.__repr__()}" + return result @staticmethod def parse(unit_string: str) -> "Unit": diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index 621117a32..33d474721 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -1,89 +1,67 @@ import math +import pytest import sasdata.quantities.units as units -from sasdata.quantities.units import Unit, ArbitraryUnit +from sasdata.quantities.units import ArbitraryUnit -class EqualUnits: - def __init__(self, test_name: str, *units): - self.test_name = "Equality: " + test_name - self.units: list[Unit] = list(units) +EQUAL_TERMS = { + "Pressure": [units.pascals, units.newtons / units.meters**2, units.micronewtons * units.millimeters**-2], + "Resistance": [units.ohms, units.volts / units.amperes, 1e-3 / units.millisiemens], + "Angular frequency": [(units.rotations / units.minutes), (units.radians * units.hertz) * 2 * math.pi / 60.0], + "Arbitrary Units": [ArbitraryUnit("Pizzas"), ArbitraryUnit(["Pizzas"])], + "Arbitrary Fractional Units": [ + ArbitraryUnit("Slices", denominator=["Pizzas"]), + ArbitraryUnit(["Slices"], denominator=["Pizzas"]), + ], +} - def run_test(self): - for i, unit_1 in enumerate(self.units): - for unit_2 in self.units[i + 1 :]: - print(unit_1, unit_2) - assert unit_1.equivalent(unit_2), "Units should be equivalent" - assert unit_1 == unit_2, "Units should be equal" +@pytest.fixture(params=EQUAL_TERMS) +def equal_term(request): + return EQUAL_TERMS[request.param] -class EquivalentButUnequalUnits: - def __init__(self, test_name: str, *units): - self.test_name = "Equivalence: " + test_name - self.units: list[Unit] = list(units) - def run_test(self): - for i, unit_1 in enumerate(self.units): - for unit_2 in self.units[i + 1 :]: - assert unit_1.equivalent(unit_2), "Units should be equivalent" - assert unit_1 != unit_2, "Units should not be equal" +def test_unit_equality(equal_term): + for i, unit_1 in enumerate(equal_term): + for unit_2 in equal_term[i + 1 :]: + assert unit_1.equivalent(unit_2), "Units should be equivalent" + assert unit_1 == unit_2, "Units should be equal" -class DissimilarUnits: - def __init__(self, test_name: str, *units): - self.test_name = "Dissimilar: " + test_name - self.units: list[Unit] = list(units) +EQUIVALENT_TERMS = { + "Angular frequency": [units.rotations / units.minutes, units.degrees * units.hertz], +} - def run_test(self): - for i, unit_1 in enumerate(self.units): - for unit_2 in self.units[i + 1 :]: - assert not unit_1.equivalent(unit_2), "Units should not be equivalent" -pizza_a = ArbitraryUnit("Pizzas") -pizza_b = ArbitraryUnit(["Pizzas"]) -print(f"A: {pizza_a}\tB: {pizza_b}") +@pytest.fixture(params=EQUIVALENT_TERMS) +def equivalent_term(request): + return EQUIVALENT_TERMS[request.param] -tests = [ +def test_unit_equivalent(equivalent_term): + units = equivalent_term + for i, unit_1 in enumerate(units): + for unit_2 in units[i + 1 :]: + print(unit_1, unit_2) + assert unit_1.equivalent(unit_2), "Units should be equivalent" + assert unit_1 != unit_2, "Units not should be equal" - EqualUnits("Pressure", - units.pascals, - units.newtons / units.meters ** 2, - units.micronewtons * units.millimeters ** -2), - EqualUnits("Resistance", - units.ohms, - units.volts / units.amperes, - 1e-3/units.millisiemens), +DISSIMILAR_TERMS = { + "Frequency and Angular frequency": [(units.rotations / units.minutes), (units.hertz)], + "Different Arbitrary Units": [ArbitraryUnit("Pizzas"), ArbitraryUnit(["Donuts"])], +} - EquivalentButUnequalUnits("Angular frequency", - units.rotations / units.minutes, - units.degrees * units.hertz), - EqualUnits("Angular frequency", - (units.rotations/units.minutes ), - (units.radians*units.hertz) * 2 * math.pi/60.0), +@pytest.fixture(params=DISSIMILAR_TERMS) +def dissimilar_term(request): + return DISSIMILAR_TERMS[request.param] - DissimilarUnits("Frequency and Angular frequency", - (units.rotations/units.minutes), - (units.hertz)), - EqualUnits("Arbitrary Units", - ArbitraryUnit("Pizzas"), - ArbitraryUnit(["Pizzas"])), - - EqualUnits("Arbitrary Units", - ArbitraryUnit("Slices", denominator=["Pizzas"]), - ArbitraryUnit(["Slices"], denominator=["Pizzas"])), - - DissimilarUnits("Different Arbitrary Units", - ArbitraryUnit("Pizzas"), - ArbitraryUnit(["Doughnuts"])), - - -] - - -for test in tests: - print(test.test_name) - test.run_test() +def test_unit_dissimilar(dissimilar_term): + units = dissimilar_term + for i, unit_1 in enumerate(units): + for unit_2 in units[i + 1 :]: + print(unit_1, unit_2) + assert not unit_1.equivalent(unit_2), "Units should not be equivalent" From 2225c542f39401adaf29ae16972725154abefaba Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 15:31:04 +0000 Subject: [PATCH 03/11] Add multiplication support for arbitrary units --- sasdata/quantities/_units_base.py | 22 +++++++++++++++------- sasdata/quantities/units.py | 20 ++++++++++++++------ test/quantities/utest_units.py | 12 ++++++++++++ 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 1a7418127..a9b56e88f 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -337,10 +337,8 @@ def __init__(self, numerator: str | list[str], denominator: None | list[str] = N case _: self._denominator = denominator self._unit = Unit(1, Dimensions()) # Unitless - print("Made") super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) - print("Make more") def _name(self): match (self._numerator, self._denominator): @@ -362,11 +360,21 @@ def __eq__(self, other): def __mul__(self: Self, other: "Unit"): - # if isinstance(other, Unit): - # return Unit(self.scale * other.scale, self.dimensions * other.dimensions) - # elif isinstance(other, (int, float)): - # return Unit(other * self.scale, self.dimensions) - return NotImplemented + print(f"Calling mul on {self} and {other}") + match other: + case ArbitraryUnit(): + result = ArbitraryUnit(self._numerator + other._numerator, self._denominator + other._denominator) + result._unit *= other._unit + return result + case NamedUnit() | Unit() | int() | float(): + result = ArbitraryUnit(self._numerator, self._denominator) + result._unit *= other + return result + case _: + return NotImplemented + + def __rmul__(self: Self, other): + return self * other def __truediv__(self: Self, other: "Unit"): # if isinstance(other, Unit): diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index 7fbf9750d..465735441 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -422,10 +422,8 @@ def __init__(self, numerator: str | list[str], denominator: None | list[str] = N case _: self._denominator = denominator self._unit = Unit(1, Dimensions()) # Unitless - print("Made") super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) - print("Make more") def _name(self): match (self._numerator, self._denominator): @@ -447,6 +445,20 @@ def __eq__(self, other): def __mul__(self: Self, other: "Unit"): + print(f"Calling mul on {self} and {other}") + match other: + case ArbitraryUnit(): + result = ArbitraryUnit(self._numerator + other._numerator, self._denominator + other._denominator) + result._unit *= other._unit + return result + case NamedUnit() | Unit() | int() | float(): + result = ArbitraryUnit(self._numerator, self._denominator) + result._unit *= other + return result + case _: + return NotImplemented + + # if isinstance(other, Unit): # return Unit(self.scale * other.scale, self.dimensions * other.dimensions) # elif isinstance(other, (int, float)): @@ -483,10 +495,6 @@ def equivalent(self: Self, other: "Unit"): case _: return False - def __eq__(self: Self, other: "Unit"): - """FIXME: TODO""" - return self.equivalent(other) and np.abs(np.log(self.scale/other.scale)) < 1e-5 - def si_equivalent(self): """ Get the SI unit corresponding to this unit""" """FIXME: TODO""" diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index 33d474721..050e5ff2d 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -14,6 +14,14 @@ ArbitraryUnit("Slices", denominator=["Pizzas"]), ArbitraryUnit(["Slices"], denominator=["Pizzas"]), ], + "Arbitrary Multiplication": [ + ArbitraryUnit("Pizzas") * ArbitraryUnit("People"), + ArbitraryUnit(["Pizzas", "People"]), + ], + "Arbitrary Multiplication with Units": [ + ArbitraryUnit("Pizzas") * units.meters, + units.meters * ArbitraryUnit(["Pizzas"]), + ], } @@ -51,6 +59,10 @@ def test_unit_equivalent(equivalent_term): DISSIMILAR_TERMS = { "Frequency and Angular frequency": [(units.rotations / units.minutes), (units.hertz)], "Different Arbitrary Units": [ArbitraryUnit("Pizzas"), ArbitraryUnit(["Donuts"])], + "Arbitrary Multiplication with Units": [ + ArbitraryUnit("Pizzas") * units.meters, + units.seconds * ArbitraryUnit(["Pizzas"]), + ], } From fbb1d28cfd3b4d68a0f7a4ef6a22905698921ef8 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 15:48:51 +0000 Subject: [PATCH 04/11] Add power support for arbitrary units --- sasdata/quantities/_units_base.py | 13 ++++++++----- sasdata/quantities/units.py | 23 +++++++++++------------ test/quantities/utest_units.py | 6 ++++++ 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index a9b56e88f..13c56364e 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -392,11 +392,14 @@ def __rtruediv__(self: Self, other: "Unit"): # else: return NotImplemented - def __pow__(self, power: int | float): - # if not isinstance(power, int | float): - # return NotImplemented - - return Unit(self.scale**power, self.dimensions**power) + def __pow__(self, power: int): + match power: + case int(): + result = ArbitraryUnit(sorted(self._numerator * power), sorted(self._denominator * power)) + result._unit = self._unit ** power + return result + case _: + return NotImplemented def equivalent(self: Self, other: "Unit"): diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index 465735441..936bd70e7 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -457,13 +457,9 @@ def __mul__(self: Self, other: "Unit"): return result case _: return NotImplemented - - - # if isinstance(other, Unit): - # return Unit(self.scale * other.scale, self.dimensions * other.dimensions) - # elif isinstance(other, (int, float)): - # return Unit(other * self.scale, self.dimensions) - return NotImplemented + + def __rmul__(self: Self, other): + return self * other def __truediv__(self: Self, other: "Unit"): # if isinstance(other, Unit): @@ -481,11 +477,14 @@ def __rtruediv__(self: Self, other: "Unit"): # else: return NotImplemented - def __pow__(self, power: int | float): - # if not isinstance(power, int | float): - # return NotImplemented - - return Unit(self.scale**power, self.dimensions**power) + def __pow__(self, power: int): + match power: + case int(): + result = ArbitraryUnit(sorted(self._numerator * power), sorted(self._denominator * power)) + result._unit = self._unit ** power + return result + case _: + return NotImplemented def equivalent(self: Self, other: "Unit"): diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index 050e5ff2d..ec3b4ad0d 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -22,6 +22,10 @@ ArbitraryUnit("Pizzas") * units.meters, units.meters * ArbitraryUnit(["Pizzas"]), ], + "Arbitrary Power": [ + ArbitraryUnit(["Slices"], denominator=["Pizza"]) * ArbitraryUnit(["Slices"], denominator=["Pizza"]), + ArbitraryUnit(["Slices"], denominator=["Pizza"]) ** 2, + ], } @@ -33,6 +37,8 @@ def equal_term(request): def test_unit_equality(equal_term): for i, unit_1 in enumerate(equal_term): for unit_2 in equal_term[i + 1 :]: + print(f"A: {unit_1}") + print(f"B: {unit_2}") assert unit_1.equivalent(unit_2), "Units should be equivalent" assert unit_1 == unit_2, "Units should be equal" From 48722d49d9734e26d628aa2b7cc487a953e7fd8e Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 15:57:51 +0000 Subject: [PATCH 05/11] Refactor arbitrary unit representations --- sasdata/quantities/_units_base.py | 59 ++++++++++++++++++++++++------- sasdata/quantities/units.py | 59 ++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 24 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 13c56364e..651894d33 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -325,28 +325,50 @@ class ArbitraryUnit(NamedUnit): units (for example, calculating donuts per person for a meeting). The arbitrary unit allows for these unforseeable quantities.""" - def __init__(self, numerator: str | list[str], denominator: None | list[str] = None): + def __init__(self, + numerator: str | list[str] | dict[str, int], + denominator: None | list[str] | dict[str, int]= None): match numerator: - case str(name): - self._numerator = [name] - case _: + case str(): + self._numerator = {numerator: 1} + case list(): + self._numerator = {} + for key in numerator: + if key in self._numerator: + self._numerator[key] += 1 + else: + self._numerator[key] = 1 + case dict(): self._numerator = numerator + case _: + raise TypeError match denominator: case None: - self._denominator = [] - case _: + self._denominator = {} + case str(): + self._denominator = {denominator: 1} + case list(): + self._denominator = {} + for key in denominator: + if key in self._denominator: + self._denominator[key] += 1 + else: + self._denominator[key] = 1 + case dict(): self._denominator = denominator + case _: + raise TypeError self._unit = Unit(1, Dimensions()) # Unitless super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) def _name(self): match (self._numerator, self._denominator): - case ([], []): + case ({}, {}): return "" - case (_, []): + case (_, {}): return " ".join(self._numerator) - case ([], _): + case ({}, _): return "1 / " + " ".join(self._denominator) case _: return " ".join(self._numerator) + " / " + " ".join(self._denominator) @@ -360,10 +382,21 @@ def __eq__(self, other): def __mul__(self: Self, other: "Unit"): - print(f"Calling mul on {self} and {other}") match other: case ArbitraryUnit(): - result = ArbitraryUnit(self._numerator + other._numerator, self._denominator + other._denominator) + num = dict(self._numerator) + for key in other._numerator: + if key in num: + num[key] += other._numerator[key] + else: + num[key] = other._numerator[key] + den = dict(self._denominator) + for key in other._denominator: + if key in den: + den[key] += other._denominator[key] + else: + den[key] = other._denominator[key] + result = ArbitraryUnit(num, den) result._unit *= other._unit return result case NamedUnit() | Unit() | int() | float(): @@ -395,7 +428,9 @@ def __rtruediv__(self: Self, other: "Unit"): def __pow__(self, power: int): match power: case int(): - result = ArbitraryUnit(sorted(self._numerator * power), sorted(self._denominator * power)) + num = {key: value * power for key, value in self._numerator.items()} + den = {key: value * power for key, value in self._denominator.items()} + result = ArbitraryUnit(num, den) result._unit = self._unit ** power return result case _: diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index 936bd70e7..cd1517544 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -410,28 +410,50 @@ class ArbitraryUnit(NamedUnit): units (for example, calculating donuts per person for a meeting). The arbitrary unit allows for these unforseeable quantities.""" - def __init__(self, numerator: str | list[str], denominator: None | list[str] = None): + def __init__(self, + numerator: str | list[str] | dict[str, int], + denominator: None | list[str] | dict[str, int]= None): match numerator: - case str(name): - self._numerator = [name] - case _: + case str(): + self._numerator = {numerator: 1} + case list(): + self._numerator = {} + for key in numerator: + if key in self._numerator: + self._numerator[key] += 1 + else: + self._numerator[key] = 1 + case dict(): self._numerator = numerator + case _: + raise TypeError match denominator: case None: - self._denominator = [] - case _: + self._denominator = {} + case str(): + self._denominator = {denominator: 1} + case list(): + self._denominator = {} + for key in denominator: + if key in self._denominator: + self._denominator[key] += 1 + else: + self._denominator[key] = 1 + case dict(): self._denominator = denominator + case _: + raise TypeError self._unit = Unit(1, Dimensions()) # Unitless super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) def _name(self): match (self._numerator, self._denominator): - case ([], []): + case ({}, {}): return "" - case (_, []): + case (_, {}): return " ".join(self._numerator) - case ([], _): + case ({}, _): return "1 / " + " ".join(self._denominator) case _: return " ".join(self._numerator) + " / " + " ".join(self._denominator) @@ -445,10 +467,21 @@ def __eq__(self, other): def __mul__(self: Self, other: "Unit"): - print(f"Calling mul on {self} and {other}") match other: case ArbitraryUnit(): - result = ArbitraryUnit(self._numerator + other._numerator, self._denominator + other._denominator) + num = dict(self._numerator) + for key in other._numerator: + if key in num: + num[key] += other._numerator[key] + else: + num[key] = other._numerator[key] + den = dict(self._denominator) + for key in other._denominator: + if key in den: + den[key] += other._denominator[key] + else: + den[key] = other._denominator[key] + result = ArbitraryUnit(num, den) result._unit *= other._unit return result case NamedUnit() | Unit() | int() | float(): @@ -480,7 +513,9 @@ def __rtruediv__(self: Self, other: "Unit"): def __pow__(self, power: int): match power: case int(): - result = ArbitraryUnit(sorted(self._numerator * power), sorted(self._denominator * power)) + num = {key: value * power for key, value in self._numerator.items()} + den = {key: value * power for key, value in self._denominator.items()} + result = ArbitraryUnit(num, den) result._unit = self._unit ** power return result case _: From a236fd4b2b1f484cf6a45414be21b36f3c3ce453 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 16:09:31 +0000 Subject: [PATCH 06/11] Enable arbitrary division --- sasdata/quantities/_units_base.py | 31 ++++++++++++++++++++++++------- sasdata/quantities/units.py | 31 ++++++++++++++++++++++++------- test/quantities/utest_units.py | 14 ++++++++++++-- 3 files changed, 60 insertions(+), 16 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 651894d33..8163475d9 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -410,12 +410,29 @@ def __rmul__(self: Self, other): return self * other def __truediv__(self: Self, other: "Unit"): - # if isinstance(other, Unit): - # return Unit(self.scale / other.scale, self.dimensions / other.dimensions) - # elif isinstance(other, (int, float)): - # return Unit(self.scale / other, self.dimensions) - # else: - return NotImplemented + match other: + case ArbitraryUnit(): + num = dict(self._numerator) + for key in other._denominator: + if key in num: + num[key] += other._denominator[key] + else: + num[key] = other._denominator[key] + den = dict(self._denominator) + for key in other._numerator: + if key in den: + den[key] += other._numerator[key] + else: + den[key] = other._numerator[key] + result = ArbitraryUnit(num, den) + result._unit /= other._unit + return result + case NamedUnit() | Unit() | int() | float(): + result = ArbitraryUnit(self._numerator, self._denominator) + result._unit /= other + return result + case _: + return NotImplemented def __rtruediv__(self: Self, other: "Unit"): # if isinstance(other, Unit): @@ -427,7 +444,7 @@ def __rtruediv__(self: Self, other: "Unit"): def __pow__(self, power: int): match power: - case int(): + case int() | float(): num = {key: value * power for key, value in self._numerator.items()} den = {key: value * power for key, value in self._denominator.items()} result = ArbitraryUnit(num, den) diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index cd1517544..4c2d8250e 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -495,12 +495,29 @@ def __rmul__(self: Self, other): return self * other def __truediv__(self: Self, other: "Unit"): - # if isinstance(other, Unit): - # return Unit(self.scale / other.scale, self.dimensions / other.dimensions) - # elif isinstance(other, (int, float)): - # return Unit(self.scale / other, self.dimensions) - # else: - return NotImplemented + match other: + case ArbitraryUnit(): + num = dict(self._numerator) + for key in other._denominator: + if key in num: + num[key] += other._denominator[key] + else: + num[key] = other._denominator[key] + den = dict(self._denominator) + for key in other._numerator: + if key in den: + den[key] += other._numerator[key] + else: + den[key] = other._numerator[key] + result = ArbitraryUnit(num, den) + result._unit /= other._unit + return result + case NamedUnit() | Unit() | int() | float(): + result = ArbitraryUnit(self._numerator, self._denominator) + result._unit /= other + return result + case _: + return NotImplemented def __rtruediv__(self: Self, other: "Unit"): # if isinstance(other, Unit): @@ -512,7 +529,7 @@ def __rtruediv__(self: Self, other: "Unit"): def __pow__(self, power: int): match power: - case int(): + case int() | float(): num = {key: value * power for key, value in self._numerator.items()} den = {key: value * power for key, value in self._denominator.items()} result = ArbitraryUnit(num, den) diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index ec3b4ad0d..df4147911 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -26,6 +26,14 @@ ArbitraryUnit(["Slices"], denominator=["Pizza"]) * ArbitraryUnit(["Slices"], denominator=["Pizza"]), ArbitraryUnit(["Slices"], denominator=["Pizza"]) ** 2, ], + "Arbitrary Fractional Power": [ + ArbitraryUnit(["Pizza", "Pizza", "Pizza"]), + ArbitraryUnit(["Pizza", "Pizza"]) ** 1.5, + ], + "Arbitrary Division": [ + ArbitraryUnit("Slices") / ArbitraryUnit("Pizza"), + ArbitraryUnit(["Slices"], denominator=["Pizza"]), + ], } @@ -37,8 +45,10 @@ def equal_term(request): def test_unit_equality(equal_term): for i, unit_1 in enumerate(equal_term): for unit_2 in equal_term[i + 1 :]: - print(f"A: {unit_1}") - print(f"B: {unit_2}") + if type(unit_1) is ArbitraryUnit: + print(f"A: {unit_1._numerator} / {unit_1._denominator}") + if type(unit_2) is ArbitraryUnit: + print(f"B: {unit_2._numerator} / {unit_2._denominator}") assert unit_1.equivalent(unit_2), "Units should be equivalent" assert unit_1 == unit_2, "Units should be equal" From 1ed0a977cd05a5bc36663013498880e990efbec3 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 16:23:57 +0000 Subject: [PATCH 07/11] Rework display of arbitrary units --- sasdata/quantities/_units_base.py | 39 +++++++++++++++++++++---------- sasdata/quantities/units.py | 39 +++++++++++++++++++++---------- test/quantities/utest_units.py | 19 +++++++++++++++ 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 8163475d9..0c506bdc3 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -358,20 +358,34 @@ def __init__(self, self._denominator = denominator case _: raise TypeError - self._unit = Unit(1, Dimensions()) # Unitless + self._unit = NamedUnit(1, Dimensions(), "") # Unitless super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) def _name(self): - match (self._numerator, self._denominator): - case ({}, {}): + num = [] + for key, value in self._numerator.items(): + if value == 1: + num.append(key) + else: + num.append(f"{key}^{value}") + den = [] + for key, value in self._denominator.items(): + if value == 1: + den.append(key) + else: + den.append(f"{key}^{value}") + num.sort() + den.sort() + match (num, den): + case ([], []): return "" - case (_, {}): - return " ".join(self._numerator) - case ({}, _): - return "1 / " + " ".join(self._denominator) + case (_, []): + return " ".join(num) + case ([], _): + return "1 / " + " ".join(den) case _: - return " ".join(self._numerator) + " / " + " ".join(self._denominator) + return " ".join(num) + " / " + " ".join(den) def __eq__(self, other): match other: @@ -471,11 +485,12 @@ def _format_unit(self, format_process: list["UnitFormatProcessor"]): for processor in format_process: pass - def __repr__(self): - """FIXME: TODO""" + def __str__(self): result = self._name() - if self._unit.__repr__(): - result += f" {self._unit.__repr__()}" + if type(self._unit) is NamedUnit and self._unit.name.strip(): + result += f" {self._unit.name.strip()}" + if type(self._unit) is Unit and str(self._unit).strip(): + result += f" {str(self._unit).strip()}" return result @staticmethod diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index 4c2d8250e..d5ae6e6ff 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -443,20 +443,34 @@ def __init__(self, self._denominator = denominator case _: raise TypeError - self._unit = Unit(1, Dimensions()) # Unitless + self._unit = NamedUnit(1, Dimensions(), "") # Unitless super().__init__(si_scaling_factor=1, dimensions=self._unit.dimensions, symbol=self._name()) def _name(self): - match (self._numerator, self._denominator): - case ({}, {}): + num = [] + for key, value in self._numerator.items(): + if value == 1: + num.append(key) + else: + num.append(f"{key}^{value}") + den = [] + for key, value in self._denominator.items(): + if value == 1: + den.append(key) + else: + den.append(f"{key}^{value}") + num.sort() + den.sort() + match (num, den): + case ([], []): return "" - case (_, {}): - return " ".join(self._numerator) - case ({}, _): - return "1 / " + " ".join(self._denominator) + case (_, []): + return " ".join(num) + case ([], _): + return "1 / " + " ".join(den) case _: - return " ".join(self._numerator) + " / " + " ".join(self._denominator) + return " ".join(num) + " / " + " ".join(den) def __eq__(self, other): match other: @@ -556,11 +570,12 @@ def _format_unit(self, format_process: list["UnitFormatProcessor"]): for processor in format_process: pass - def __repr__(self): - """FIXME: TODO""" + def __str__(self): result = self._name() - if self._unit.__repr__(): - result += f" {self._unit.__repr__()}" + if type(self._unit) is NamedUnit and self._unit.name.strip(): + result += f" {self._unit.name.strip()}" + if type(self._unit) is Unit and str(self._unit).strip(): + result += f" {str(self._unit).strip()}" return result @staticmethod diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index df4147911..5ad29a95d 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -93,3 +93,22 @@ def test_unit_dissimilar(dissimilar_term): for unit_2 in units[i + 1 :]: print(unit_1, unit_2) assert not unit_1.equivalent(unit_2), "Units should not be equivalent" + + +def test_unit_names(): + pizza = ArbitraryUnit(["Pizza"]) + slice = ArbitraryUnit(["Slice"]) + pineapple = ArbitraryUnit(["Pineapple"]) + pie = ArbitraryUnit(["Pie"]) + empty = ArbitraryUnit([]) + + assert str(empty) == "" + + assert str(pizza) == "Pizza" + assert str((pizza * pineapple)) == "Pineapple Pizza" + assert str((pizza * pizza)) == "Pizza^2" + + assert str((slice / pizza)) == "Slice / Pizza" + assert str((slice / pizza) ** 2) == "Slice^2 / Pizza^2" + + assert str((pie**0.5)) == "Pie^0.5" # A valid unit, because pie are square From 4d825906fa2a9fbfd07598e0728cdf78f7de7bbc Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Thu, 19 Feb 2026 16:52:37 +0000 Subject: [PATCH 08/11] Properly reduce terms and add rdiv for arbitrary units --- sasdata/quantities/_units_base.py | 51 ++++++++++++++++++++++++++----- sasdata/quantities/units.py | 51 ++++++++++++++++++++++++++----- test/quantities/utest_units.py | 9 ++++++ 3 files changed, 95 insertions(+), 16 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 0c506bdc3..cbb223d90 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -412,7 +412,7 @@ def __mul__(self: Self, other: "Unit"): den[key] = other._denominator[key] result = ArbitraryUnit(num, den) result._unit *= other._unit - return result + return result._reduce() case NamedUnit() | Unit() | int() | float(): result = ArbitraryUnit(self._numerator, self._denominator) result._unit *= other @@ -440,7 +440,7 @@ def __truediv__(self: Self, other: "Unit"): den[key] = other._numerator[key] result = ArbitraryUnit(num, den) result._unit /= other._unit - return result + return result._reduce() case NamedUnit() | Unit() | int() | float(): result = ArbitraryUnit(self._numerator, self._denominator) result._unit /= other @@ -449,12 +449,29 @@ def __truediv__(self: Self, other: "Unit"): return NotImplemented def __rtruediv__(self: Self, other: "Unit"): - # if isinstance(other, Unit): - # return Unit(other.scale / self.scale, other.dimensions / self.dimensions) - # elif isinstance(other, (int, float)): - # return Unit(other / self.scale, self.dimensions ** -1) - # else: - return NotImplemented + match other: + case ArbitraryUnit(): + num = dict(other._numerator) + for key in self._denominator: + if key in num: + num[key] += self._denominator[key] + else: + num[key] = self._denominator[key] + den = dict(other._denominator) + for key in self._numerator: + if key in den: + den[key] += self._numerator[key] + else: + den[key] = self._numerator[key] + result = ArbitraryUnit(num, den) + result._unit = other._unit / self._unit + return result._reduce() + case NamedUnit() | Unit() | int() | float(): + result = ArbitraryUnit(self._denominator, self._numerator) + result._unit = other / result._unit + return result + case _: + return NotImplemented def __pow__(self, power: int): match power: @@ -485,6 +502,21 @@ def _format_unit(self, format_process: list["UnitFormatProcessor"]): for processor in format_process: pass + def _reduce(self): + """Remove redundant units""" + for k in self._denominator: + if k in self._numerator: + common = min(self._numerator[k], self._denominator[k]) + self._numerator[k] -= common + self._denominator[k] -= common + dead_nums = [k for k in self._numerator if self._numerator[k] == 0] + for k in dead_nums: + del self._numerator[k] + dead_dens = [k for k in self._denominator if self._denominator[k] == 0] + for k in dead_dens: + del self._denominator[k] + return self + def __str__(self): result = self._name() if type(self._unit) is NamedUnit and self._unit.name.strip(): @@ -493,6 +525,9 @@ def __str__(self): result += f" {str(self._unit).strip()}" return result + def __repr__(self): + return str(self) + @staticmethod def parse(unit_string: str) -> "Unit": """FIXME: TODO""" diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index d5ae6e6ff..0741b864a 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -497,7 +497,7 @@ def __mul__(self: Self, other: "Unit"): den[key] = other._denominator[key] result = ArbitraryUnit(num, den) result._unit *= other._unit - return result + return result._reduce() case NamedUnit() | Unit() | int() | float(): result = ArbitraryUnit(self._numerator, self._denominator) result._unit *= other @@ -525,7 +525,7 @@ def __truediv__(self: Self, other: "Unit"): den[key] = other._numerator[key] result = ArbitraryUnit(num, den) result._unit /= other._unit - return result + return result._reduce() case NamedUnit() | Unit() | int() | float(): result = ArbitraryUnit(self._numerator, self._denominator) result._unit /= other @@ -534,12 +534,29 @@ def __truediv__(self: Self, other: "Unit"): return NotImplemented def __rtruediv__(self: Self, other: "Unit"): - # if isinstance(other, Unit): - # return Unit(other.scale / self.scale, other.dimensions / self.dimensions) - # elif isinstance(other, (int, float)): - # return Unit(other / self.scale, self.dimensions ** -1) - # else: - return NotImplemented + match other: + case ArbitraryUnit(): + num = dict(other._numerator) + for key in self._denominator: + if key in num: + num[key] += self._denominator[key] + else: + num[key] = self._denominator[key] + den = dict(other._denominator) + for key in self._numerator: + if key in den: + den[key] += self._numerator[key] + else: + den[key] = self._numerator[key] + result = ArbitraryUnit(num, den) + result._unit = other._unit / self._unit + return result._reduce() + case NamedUnit() | Unit() | int() | float(): + result = ArbitraryUnit(self._denominator, self._numerator) + result._unit = other / result._unit + return result + case _: + return NotImplemented def __pow__(self, power: int): match power: @@ -570,6 +587,21 @@ def _format_unit(self, format_process: list["UnitFormatProcessor"]): for processor in format_process: pass + def _reduce(self): + """Remove redundant units""" + for k in self._denominator: + if k in self._numerator: + common = min(self._numerator[k], self._denominator[k]) + self._numerator[k] -= common + self._denominator[k] -= common + dead_nums = [k for k in self._numerator if self._numerator[k] == 0] + for k in dead_nums: + del self._numerator[k] + dead_dens = [k for k in self._denominator if self._denominator[k] == 0] + for k in dead_dens: + del self._denominator[k] + return self + def __str__(self): result = self._name() if type(self._unit) is NamedUnit and self._unit.name.strip(): @@ -578,6 +610,9 @@ def __str__(self): result += f" {str(self._unit).strip()}" return result + def __repr__(self): + return str(self) + @staticmethod def parse(unit_string: str) -> "Unit": """FIXME: TODO""" diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index 5ad29a95d..bbcce09da 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -33,6 +33,14 @@ "Arbitrary Division": [ ArbitraryUnit("Slices") / ArbitraryUnit("Pizza"), ArbitraryUnit(["Slices"], denominator=["Pizza"]), + (1 / ArbitraryUnit("Pizza")) * ArbitraryUnit("Slices"), + 1 / (ArbitraryUnit("Pizza") / ArbitraryUnit("Slices")), + ], + "Arbitrary Complicated Math": [ + (ArbitraryUnit("Slices") / ArbitraryUnit("Person")) + / (ArbitraryUnit("Slices") / ArbitraryUnit("Pizzas")) + * ArbitraryUnit("Person"), + ArbitraryUnit("Pizzas"), ], } @@ -108,6 +116,7 @@ def test_unit_names(): assert str((pizza * pineapple)) == "Pineapple Pizza" assert str((pizza * pizza)) == "Pizza^2" + assert str((1 / pizza)) == "1 / Pizza" assert str((slice / pizza)) == "Slice / Pizza" assert str((slice / pizza) ** 2) == "Slice^2 / Pizza^2" From f69e363bf840b022de43aeb6d497caa4ec15ab20 Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Fri, 20 Feb 2026 11:49:04 +0000 Subject: [PATCH 09/11] Remove unneeded function stubs --- sasdata/quantities/_units_base.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index cbb223d90..095cad754 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -492,16 +492,6 @@ def equivalent(self: Self, other: "Unit"): case _: return False - def si_equivalent(self): - """ Get the SI unit corresponding to this unit""" - """FIXME: TODO""" - return Unit(1, self.dimensions) - - def _format_unit(self, format_process: list["UnitFormatProcessor"]): - """FIXME: TODO""" - for processor in format_process: - pass - def _reduce(self): """Remove redundant units""" for k in self._denominator: @@ -528,17 +518,6 @@ def __str__(self): def __repr__(self): return str(self) - @staticmethod - def parse(unit_string: str) -> "Unit": - """FIXME: TODO""" - pass -# -# Parsing plan: -# Require unknown amounts of units to be explicitly positive or negative? -# -# - - @dataclass class ProcessedUnitToken: From abc2f11b11f582b95d707a954acbeb4f9e0b5835 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Fri, 20 Feb 2026 11:57:10 +0000 Subject: [PATCH 10/11] [pre-commit.ci lite] apply automatic fixes for ruff linting errors --- sasdata/quantities/_units_base.py | 4 ++-- sasdata/quantities/units.py | 4 ++-- test/quantities/utest_units.py | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sasdata/quantities/_units_base.py b/sasdata/quantities/_units_base.py index 095cad754..918126a63 100644 --- a/sasdata/quantities/_units_base.py +++ b/sasdata/quantities/_units_base.py @@ -501,10 +501,10 @@ def _reduce(self): self._denominator[k] -= common dead_nums = [k for k in self._numerator if self._numerator[k] == 0] for k in dead_nums: - del self._numerator[k] + del self._numerator[k] dead_dens = [k for k in self._denominator if self._denominator[k] == 0] for k in dead_dens: - del self._denominator[k] + del self._denominator[k] return self def __str__(self): diff --git a/sasdata/quantities/units.py b/sasdata/quantities/units.py index 0741b864a..6b6ef194e 100644 --- a/sasdata/quantities/units.py +++ b/sasdata/quantities/units.py @@ -596,10 +596,10 @@ def _reduce(self): self._denominator[k] -= common dead_nums = [k for k in self._numerator if self._numerator[k] == 0] for k in dead_nums: - del self._numerator[k] + del self._numerator[k] dead_dens = [k for k in self._denominator if self._denominator[k] == 0] for k in dead_dens: - del self._denominator[k] + del self._denominator[k] return self def __str__(self): diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index bbcce09da..f9ba2c435 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -1,10 +1,10 @@ import math + import pytest import sasdata.quantities.units as units from sasdata.quantities.units import ArbitraryUnit - EQUAL_TERMS = { "Pressure": [units.pascals, units.newtons / units.meters**2, units.micronewtons * units.millimeters**-2], "Resistance": [units.ohms, units.volts / units.amperes, 1e-3 / units.millisiemens], @@ -113,11 +113,11 @@ def test_unit_names(): assert str(empty) == "" assert str(pizza) == "Pizza" - assert str((pizza * pineapple)) == "Pineapple Pizza" - assert str((pizza * pizza)) == "Pizza^2" + assert str(pizza * pineapple) == "Pineapple Pizza" + assert str(pizza * pizza) == "Pizza^2" - assert str((1 / pizza)) == "1 / Pizza" - assert str((slice / pizza)) == "Slice / Pizza" + assert str(1 / pizza) == "1 / Pizza" + assert str(slice / pizza) == "Slice / Pizza" assert str((slice / pizza) ** 2) == "Slice^2 / Pizza^2" - assert str((pie**0.5)) == "Pie^0.5" # A valid unit, because pie are square + assert str(pie**0.5) == "Pie^0.5" # A valid unit, because pie are square From 6e853653212e9b8546b0231d816c1ae797f4956d Mon Sep 17 00:00:00 2001 From: Adam Washington Date: Fri, 20 Feb 2026 11:49:04 +0000 Subject: [PATCH 11/11] Fix windows unicode printing issue in test --- test/quantities/utest_units.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/quantities/utest_units.py b/test/quantities/utest_units.py index f9ba2c435..95bf4c3ca 100644 --- a/test/quantities/utest_units.py +++ b/test/quantities/utest_units.py @@ -53,10 +53,6 @@ def equal_term(request): def test_unit_equality(equal_term): for i, unit_1 in enumerate(equal_term): for unit_2 in equal_term[i + 1 :]: - if type(unit_1) is ArbitraryUnit: - print(f"A: {unit_1._numerator} / {unit_1._denominator}") - if type(unit_2) is ArbitraryUnit: - print(f"B: {unit_2._numerator} / {unit_2._denominator}") assert unit_1.equivalent(unit_2), "Units should be equivalent" assert unit_1 == unit_2, "Units should be equal" @@ -75,7 +71,6 @@ def test_unit_equivalent(equivalent_term): units = equivalent_term for i, unit_1 in enumerate(units): for unit_2 in units[i + 1 :]: - print(unit_1, unit_2) assert unit_1.equivalent(unit_2), "Units should be equivalent" assert unit_1 != unit_2, "Units not should be equal" @@ -99,7 +94,6 @@ def test_unit_dissimilar(dissimilar_term): units = dissimilar_term for i, unit_1 in enumerate(units): for unit_2 in units[i + 1 :]: - print(unit_1, unit_2) assert not unit_1.equivalent(unit_2), "Units should not be equivalent"