Skip to content

Commit da6a5ce

Browse files
committed
Add capability to multiply quantities with percentage
Signed-off-by: Mathias L. Baumann <[email protected]>
1 parent 46d8630 commit da6a5ce

File tree

3 files changed

+148
-15
lines changed

3 files changed

+148
-15
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212

1313
## New Features
1414

15-
- Add quantity class `Frequency` for frequency values.
1615
- Add `abs()` support for quantities.
16+
* Add quantity class `Frequency` for frequency values.
17+
* Quantities can now be multiplied with `Percentage` types.
1718

1819
## Bug Fixes
1920

src/frequenz/sdk/timeseries/_quantities.py

Lines changed: 102 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,22 @@ def __sub__(self, other: Self) -> Self:
221221
difference._base_value = self._base_value - other._base_value
222222
return difference
223223

224+
def __mul__(self, percent: Percentage) -> Self:
225+
"""Return the product of this quantity and a percentage.
226+
227+
Args:
228+
percent: The percentage.
229+
230+
Returns:
231+
The product of this quantity and a percentage.
232+
"""
233+
if not isinstance(percent, Percentage):
234+
return NotImplemented
235+
236+
product = type(self).__new__(type(self))
237+
product._base_value = self._base_value * percent.as_fraction()
238+
return product
239+
224240
def __gt__(self, other: Self) -> bool:
225241
"""Return whether this quantity is greater than another.
226242
@@ -423,18 +439,42 @@ def as_megawatts(self) -> float:
423439
"""
424440
return self._base_value / 1e6
425441

426-
def __mul__(self, duration: timedelta) -> Energy:
442+
@overload # type: ignore
443+
def __mul__(self, other: Percentage) -> Self:
444+
"""Return a power from multiplying this power by the given percentage.
445+
446+
Args:
447+
other: The percentage to multiply by.
448+
"""
449+
450+
@overload
451+
def __mul__(self, other: timedelta) -> Energy:
427452
"""Return an energy from multiplying this power by the given duration.
428453
429454
Args:
430-
duration: The duration to multiply by.
455+
other: The duration to multiply by.
456+
"""
457+
458+
def __mul__(self, other: Percentage | timedelta) -> Self | Energy:
459+
"""Return a power or energy from multiplying this power by the given value.
460+
461+
Args:
462+
other: The percentage or duration to multiply by.
431463
432464
Returns:
433-
An energy from multiplying this power by the given duration.
465+
A power or energy.
466+
467+
Raises:
468+
TypeError: If the given value is not a percentage or duration.
434469
"""
435-
return Energy.from_watt_hours(
436-
self._base_value * duration.total_seconds() / 3600.0
437-
)
470+
if isinstance(other, Percentage):
471+
return super().__mul__(other)
472+
if isinstance(other, timedelta):
473+
return Energy.from_watt_hours(
474+
self._base_value * other.total_seconds() / 3600.0
475+
)
476+
477+
return NotImplemented
438478

439479
@overload
440480
def __truediv__(self, other: Current) -> Voltage:
@@ -527,16 +567,40 @@ def as_milliamperes(self) -> float:
527567
"""
528568
return self._base_value * 1e3
529569

530-
def __mul__(self, voltage: Voltage) -> Power:
570+
@overload # type: ignore
571+
def __mul__(self, other: Percentage) -> Self:
572+
"""Return a power from multiplying this power by the given percentage.
573+
574+
Args:
575+
other: The percentage to multiply by.
576+
"""
577+
578+
@overload
579+
def __mul__(self, other: Voltage) -> Power:
531580
"""Multiply the current by a voltage to get a power.
532581
533582
Args:
534-
voltage: The voltage.
583+
other: The voltage.
584+
"""
585+
586+
def __mul__(self, other: Percentage | Voltage) -> Self | Power:
587+
"""Return a current or power from multiplying this current by the given value.
588+
589+
Args:
590+
other: The percentage or voltage to multiply by.
535591
536592
Returns:
537-
The power.
593+
A current or power.
594+
595+
Raises:
596+
TypeError: If the given value is not a percentage or voltage.
538597
"""
539-
return Power.from_watts(self._base_value * voltage._base_value)
598+
if isinstance(other, Percentage):
599+
return super().__mul__(other)
600+
if isinstance(other, Voltage):
601+
return Power.from_watts(self._base_value * other._base_value)
602+
603+
return NotImplemented
540604

541605

542606
class Voltage(
@@ -612,16 +676,40 @@ def as_kilovolts(self) -> float:
612676
"""
613677
return self._base_value / 1e3
614678

615-
def __mul__(self, current: Current) -> Power:
679+
@overload # type: ignore
680+
def __mul__(self, other: Percentage) -> Self:
681+
"""Return a power from multiplying this power by the given percentage.
682+
683+
Args:
684+
other: The percentage to multiply by.
685+
"""
686+
687+
@overload
688+
def __mul__(self, other: Current) -> Power:
616689
"""Multiply the voltage by the current to get the power.
617690
618691
Args:
619-
current: The current to multiply the voltage with.
692+
other: The current to multiply the voltage with.
693+
"""
694+
695+
def __mul__(self, other: Percentage | Current) -> Self | Power:
696+
"""Return a voltage or power from multiplying this voltage by the given value.
697+
698+
Args:
699+
other: The percentage or current to multiply by.
620700
621701
Returns:
622-
The calculated power.
702+
The calculated voltage or power.
703+
704+
Raises:
705+
TypeError: If the given value is not a percentage or current.
623706
"""
624-
return Power.from_watts(self._base_value * current._base_value)
707+
if isinstance(other, Percentage):
708+
return super().__mul__(other)
709+
if isinstance(other, Current):
710+
return Power.from_watts(self._base_value * other._base_value)
711+
712+
return NotImplemented
625713

626714

627715
class Energy(

tests/timeseries/test_quantities.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,3 +377,47 @@ def test_abs() -> None:
377377
pct = Percentage.from_fraction(30)
378378
assert abs(pct) == Percentage.from_fraction(30)
379379
assert abs(-pct) == Percentage.from_fraction(30)
380+
381+
382+
def test_quantity_multiplied_with_precentage() -> None:
383+
"""Test the multiplication of all quantities with percentage."""
384+
percentage = Percentage.from_percent(50)
385+
power = Power.from_watts(1000.0)
386+
voltage = Voltage.from_volts(230.0)
387+
current = Current.from_amperes(2)
388+
energy = Energy.from_kilowatt_hours(12)
389+
percentage_ = Percentage.from_percent(50)
390+
391+
assert power * percentage == Power.from_watts(500.0)
392+
assert voltage * percentage == Voltage.from_volts(115.0)
393+
assert current * percentage == Current.from_amperes(1)
394+
assert energy * percentage == Energy.from_kilowatt_hours(6)
395+
assert percentage_ * percentage == Percentage.from_percent(25)
396+
397+
398+
def test_invalid_multiplications() -> None:
399+
"""Test the multiplication of quantities with invalid quantities."""
400+
power = Power.from_watts(1000.0)
401+
voltage = Voltage.from_volts(230.0)
402+
current = Current.from_amperes(2)
403+
energy = Energy.from_kilowatt_hours(12)
404+
405+
for quantity in [power, voltage, current, energy]:
406+
with pytest.raises(TypeError):
407+
_ = power * quantity # type: ignore
408+
409+
for quantity in [voltage, power, energy]:
410+
with pytest.raises(TypeError):
411+
_ = voltage * quantity # type: ignore
412+
413+
for quantity in [current, power, energy]:
414+
with pytest.raises(TypeError):
415+
_ = current * quantity # type: ignore
416+
417+
for quantity in [energy, power, voltage, current]:
418+
with pytest.raises(TypeError):
419+
_ = energy * quantity # type: ignore
420+
421+
for quantity in [power, voltage, current, energy, Percentage.from_percent(50)]:
422+
with pytest.raises(TypeError):
423+
_ = quantity * 200.0 # type: ignore

0 commit comments

Comments
 (0)