Skip to content

Commit 361bcb0

Browse files
committed
Add zero() constructor for Quantities
It is a very common case which becomes very verbose, and it includes unit information that is completely irrelevant (zero is zero in any unit). Signed-off-by: Leandro Lucarella <[email protected]>
1 parent 1cd226b commit 361bcb0

File tree

3 files changed

+89
-5
lines changed

3 files changed

+89
-5
lines changed

RELEASE_NOTES.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,15 @@
1313

1414
## New Features
1515

16-
- Add `abs()` support for quantities.
17-
- Add quantity class `Frequency` for frequency values.
18-
- Quantities can now be multiplied with `Percentage` types.
19-
- `FormulaEngine` arithmetics now supports scalar multiplication with floats and addition with Quantities.
20-
- Add a `isclose()` method on quantities to compare them to other values of the same type. Because `Quantity` types are just wrappers around `float`s, direct comparison might not always be desirable.
16+
- Quantities
17+
18+
* Add `abs()`.
19+
* Add a `isclose()` method on quantities to compare them to other values of the same type. Because `Quantity` types are just wrappers around `float`s, direct comparison might not always be desirable.
20+
* Add `zero()` constructor (which returns a singleton) to easily get a zero value.
21+
* Add multiplication by `Percentage` types.
22+
* Add a new quantity class `Frequency` for frequency values.
23+
24+
- `FormulaEngine` arithmetics now supports scalar multiplication with floats and addition with Quantities
2125

2226
## Bug Fixes
2327

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,30 @@ def __init_subclass__(cls, exponent_unit_map: dict[int, str]) -> None:
6565
cls._exponent_unit_map = exponent_unit_map
6666
super().__init_subclass__()
6767

68+
_zero_cache: dict[type, Quantity] = {}
69+
"""Cache for zero singletons.
70+
71+
This is a workaround for mypy getting confused when using @functools.cache and
72+
@classmethod combined with returning Self. It believes the resulting type of this
73+
method is Self and complains that members of the actual class don't exist in Self,
74+
so we need to implement the cache ourselves.
75+
"""
76+
77+
@classmethod
78+
def zero(cls) -> Self:
79+
"""Return a quantity with value 0.
80+
81+
Returns:
82+
A quantity with value 0.
83+
"""
84+
_zero = cls._zero_cache.get(cls, None)
85+
if _zero is None:
86+
_zero = cls.__new__(cls)
87+
_zero._base_value = 0
88+
cls._zero_cache[cls] = _zero
89+
assert isinstance(_zero, cls)
90+
return _zero
91+
6892
@property
6993
def base_value(self) -> float:
7094
"""Return the value of this quantity in the base unit.

tests/timeseries/test_quantities.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,62 @@ class Fz2(
4242
"""Frequency quantity with broad exponent unit map."""
4343

4444

45+
def test_zero() -> None:
46+
"""Test the zero value for quantity."""
47+
assert Quantity(0.0) == Quantity.zero()
48+
assert Quantity(0.0, exponent=100) == Quantity.zero()
49+
assert Quantity.zero() is Quantity.zero() # It is a "singleton"
50+
assert Quantity.zero().base_value == 0.0
51+
52+
# Test the singleton is immutable
53+
one = Quantity.zero()
54+
one += Quantity(1.0)
55+
assert one != Quantity.zero()
56+
assert Quantity.zero() == Quantity(0.0)
57+
58+
assert Power.from_watts(0.0) == Power.zero()
59+
assert Power.from_kilowatts(0.0) == Power.zero()
60+
assert isinstance(Power.zero(), Power)
61+
assert Power.zero().as_watts() == 0.0
62+
assert Power.zero().as_kilowatts() == 0.0
63+
assert Power.zero() is Power.zero() # It is a "singleton"
64+
65+
assert Current.from_amperes(0.0) == Current.zero()
66+
assert Current.from_milliamperes(0.0) == Current.zero()
67+
assert isinstance(Current.zero(), Current)
68+
assert Current.zero().as_amperes() == 0.0
69+
assert Current.zero().as_milliamperes() == 0.0
70+
assert Current.zero() is Current.zero() # It is a "singleton"
71+
72+
assert Voltage.from_volts(0.0) == Voltage.zero()
73+
assert Voltage.from_kilovolts(0.0) == Voltage.zero()
74+
assert isinstance(Voltage.zero(), Voltage)
75+
assert Voltage.zero().as_volts() == 0.0
76+
assert Voltage.zero().as_kilovolts() == 0.0
77+
assert Voltage.zero() is Voltage.zero() # It is a "singleton"
78+
79+
assert Energy.from_kilowatt_hours(0.0) == Energy.zero()
80+
assert Energy.from_megawatt_hours(0.0) == Energy.zero()
81+
assert isinstance(Energy.zero(), Energy)
82+
assert Energy.zero().as_kilowatt_hours() == 0.0
83+
assert Energy.zero().as_megawatt_hours() == 0.0
84+
assert Energy.zero() is Energy.zero() # It is a "singleton"
85+
86+
assert Frequency.from_hertz(0.0) == Frequency.zero()
87+
assert Frequency.from_megahertz(0.0) == Frequency.zero()
88+
assert isinstance(Frequency.zero(), Frequency)
89+
assert Frequency.zero().as_hertz() == 0.0
90+
assert Frequency.zero().as_megahertz() == 0.0
91+
assert Frequency.zero() is Frequency.zero() # It is a "singleton"
92+
93+
assert Percentage.from_percent(0.0) == Percentage.zero()
94+
assert Percentage.from_fraction(0.0) == Percentage.zero()
95+
assert isinstance(Percentage.zero(), Percentage)
96+
assert Percentage.zero().as_percent() == 0.0
97+
assert Percentage.zero().as_fraction() == 0.0
98+
assert Percentage.zero() is Percentage.zero() # It is a "singleton"
99+
100+
45101
def test_string_representation() -> None:
46102
"""Test the string representation of the quantities."""
47103
assert str(Quantity(1.024445, exponent=0)) == "1.024"

0 commit comments

Comments
 (0)