Skip to content

Commit d34512e

Browse files
authored
Allow all quantities division by float | Self (#876)
Dividing by `float` is only applying a factor so it should be safe for any `Quantity`. Dividing by `Self` produces a unit-less ration (`float`), so it should be safe too. To be able to use nicer names in overloads, we use positional-only arguments for `__truediv__()`, which in practice shouldn't be much of a breaking change as these methods should be used mostly via operators. We also use `match` syntax in the changed methods.
2 parents f8caead + 15ac8c8 commit d34512e

File tree

3 files changed

+374
-33
lines changed

3 files changed

+374
-33
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
## New Features
1212

13-
- Allow multiplying any `Quantity` by a `float` too. This just scales the `Quantity` value.
13+
- Allow multiplying and dividing any `Quantity` by a `float`. This just scales the `Quantity` value.
14+
- Allow dividing any `Quantity` by another quaintity of the same type. This just returns a ration between both quantities.
1415

1516
## Bug Fixes
1617

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 125 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,45 @@ def __mul__(self, value: float | Percentage, /) -> Self:
373373
case _:
374374
return NotImplemented
375375

376+
@overload
377+
def __truediv__(self, other: float, /) -> Self:
378+
"""Divide this quantity by a scalar.
379+
380+
Args:
381+
other: The scalar or percentage to divide this quantity by.
382+
383+
Returns:
384+
The divided quantity.
385+
"""
386+
387+
@overload
388+
def __truediv__(self, other: Self, /) -> float:
389+
"""Return the ratio of this quantity to another.
390+
391+
Args:
392+
other: The other quantity.
393+
394+
Returns:
395+
The ratio of this quantity to another.
396+
"""
397+
398+
def __truediv__(self, value: float | Self, /) -> Self | float:
399+
"""Divide this quantity by a scalar or another quantity.
400+
401+
Args:
402+
value: The scalar or quantity to divide this quantity by.
403+
404+
Returns:
405+
The divided quantity or the ratio of this quantity to another.
406+
"""
407+
match value:
408+
case float():
409+
return type(self)._new(self._base_value / value)
410+
case Quantity() if type(value) is type(self):
411+
return self._base_value / value._base_value
412+
case _:
413+
return NotImplemented
414+
376415
def __gt__(self, other: Self) -> bool:
377416
"""Return whether this quantity is greater than another.
378417
@@ -664,47 +703,73 @@ def __mul__(self, other: float | Percentage | timedelta, /) -> Self | Energy:
664703
case _:
665704
return NotImplemented
666705

706+
# See the comment for Power.__mul__ for why we need the ignore here.
707+
@overload # type: ignore[override]
708+
def __truediv__(self, other: float, /) -> Self:
709+
"""Divide this power by a scalar.
710+
711+
Args:
712+
other: The scalar to divide this power by.
713+
714+
Returns:
715+
The divided power.
716+
"""
717+
667718
@overload
668-
def __truediv__(self, other: Current) -> Voltage:
719+
def __truediv__(self, other: Self, /) -> float:
720+
"""Return the ratio of this power to another.
721+
722+
Args:
723+
other: The other power.
724+
725+
Returns:
726+
The ratio of this power to another.
727+
"""
728+
729+
@overload
730+
def __truediv__(self, current: Current, /) -> Voltage:
669731
"""Return a voltage from dividing this power by the given current.
670732
671733
Args:
672-
other: The current to divide by.
734+
current: The current to divide by.
673735
674736
Returns:
675737
A voltage from dividing this power by the a current.
676738
"""
677739

678740
@overload
679-
def __truediv__(self, other: Voltage) -> Current:
741+
def __truediv__(self, voltage: Voltage, /) -> Current:
680742
"""Return a current from dividing this power by the given voltage.
681743
682744
Args:
683-
other: The voltage to divide by.
745+
voltage: The voltage to divide by.
684746
685747
Returns:
686748
A current from dividing this power by a voltage.
687749
"""
688750

689-
def __truediv__(self, other: Current | Voltage) -> Voltage | Current:
751+
def __truediv__(
752+
self, other: float | Self | Current | Voltage, /
753+
) -> Self | float | Voltage | Current:
690754
"""Return a current or voltage from dividing this power by the given value.
691755
692756
Args:
693-
other: The current or voltage to divide by.
757+
other: The scalar, power, current or voltage to divide by.
694758
695759
Returns:
696760
A current or voltage from dividing this power by the given value.
697-
698-
Raises:
699-
TypeError: If the given value is not a current or voltage.
700761
"""
701-
if isinstance(other, Current):
702-
return Voltage.from_volts(self._base_value / other._base_value)
703-
if isinstance(other, Voltage):
704-
return Current.from_amperes(self._base_value / other._base_value)
705-
raise TypeError(
706-
f"unsupported operand type(s) for /: '{type(self)}' and '{type(other)}'"
707-
)
762+
match other:
763+
case float():
764+
return super().__truediv__(other)
765+
case Power():
766+
return self._base_value / other._base_value
767+
case Current():
768+
return Voltage._new(self._base_value / other._base_value)
769+
case Voltage():
770+
return Current._new(self._base_value / other._base_value)
771+
case _:
772+
return NotImplemented
708773

709774

710775
class Current(
@@ -1043,47 +1108,75 @@ def __mul__(self, other: float | Percentage) -> Self:
10431108
case _:
10441109
return NotImplemented
10451110

1111+
# See the comment for Power.__mul__ for why we need the ignore here.
1112+
@overload # type: ignore[override]
1113+
def __truediv__(self, other: float, /) -> Self:
1114+
"""Divide this energy by a scalar.
1115+
1116+
Args:
1117+
other: The scalar to divide this energy by.
1118+
1119+
Returns:
1120+
The divided energy.
1121+
"""
1122+
10461123
@overload
1047-
def __truediv__(self, other: timedelta) -> Power:
1124+
def __truediv__(self, other: Self, /) -> float:
1125+
"""Return the ratio of this energy to another.
1126+
1127+
Args:
1128+
other: The other energy.
1129+
1130+
Returns:
1131+
The ratio of this energy to another.
1132+
"""
1133+
1134+
@overload
1135+
def __truediv__(self, duration: timedelta, /) -> Power:
10481136
"""Return a power from dividing this energy by the given duration.
10491137
10501138
Args:
1051-
other: The duration to divide by.
1139+
duration: The duration to divide by.
10521140
10531141
Returns:
10541142
A power from dividing this energy by the given duration.
10551143
"""
10561144

10571145
@overload
1058-
def __truediv__(self, other: Power) -> timedelta:
1146+
def __truediv__(self, power: Power, /) -> timedelta:
10591147
"""Return a duration from dividing this energy by the given power.
10601148
10611149
Args:
1062-
other: The power to divide by.
1150+
power: The power to divide by.
10631151
10641152
Returns:
10651153
A duration from dividing this energy by the given power.
10661154
"""
10671155

1068-
def __truediv__(self, other: timedelta | Power) -> Power | timedelta:
1156+
def __truediv__(
1157+
self, other: float | Self | timedelta | Power, /
1158+
) -> Self | float | Power | timedelta:
10691159
"""Return a power or duration from dividing this energy by the given value.
10701160
10711161
Args:
1072-
other: The power or duration to divide by.
1162+
other: The scalar, energy, power or duration to divide by.
10731163
10741164
Returns:
10751165
A power or duration from dividing this energy by the given value.
1076-
1077-
Raises:
1078-
TypeError: If the given value is not a power or duration.
10791166
"""
1080-
if isinstance(other, timedelta):
1081-
return Power.from_watts(self._base_value / (other.total_seconds() / 3600.0))
1082-
if isinstance(other, Power):
1083-
return timedelta(seconds=(self._base_value / other._base_value) * 3600.0)
1084-
raise TypeError(
1085-
f"unsupported operand type(s) for /: '{type(self)}' and '{type(other)}'"
1086-
)
1167+
match other:
1168+
case float():
1169+
return super().__truediv__(other)
1170+
case Energy():
1171+
return self._base_value / other._base_value
1172+
case timedelta():
1173+
return Power._new(self._base_value / (other.total_seconds() / 3600.0))
1174+
case Power():
1175+
return timedelta(
1176+
seconds=(self._base_value / other._base_value) * 3600.0
1177+
)
1178+
case _:
1179+
return NotImplemented
10871180

10881181

10891182
class Frequency(

0 commit comments

Comments
 (0)