Skip to content

Commit 1435be1

Browse files
jack-herrmannllucax
authored andcommitted
Add contains function to Bounds and SystemBounds
Signed-off-by: Jack <[email protected]>
1 parent 72c25a8 commit 1435be1

File tree

2 files changed

+154
-1
lines changed

2 files changed

+154
-1
lines changed

src/frequenz/sdk/timeseries/_base_types.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from collections.abc import Callable, Iterator
99
from dataclasses import dataclass
1010
from datetime import datetime, timezone
11-
from typing import Generic, Self, TypeVar, overload
11+
from typing import Any, Generic, Protocol, Self, TypeVar, overload, runtime_checkable
1212

1313
from ._quantities import Power, QuantityT
1414

@@ -134,6 +134,39 @@ def map(
134134
)
135135

136136

137+
@runtime_checkable
138+
class Comparable(Protocol):
139+
"""
140+
A protocol that requires the implementation of comparison methods.
141+
142+
This protocol is used to ensure that types can be compared using
143+
the less than or equal to (`<=`) and greater than or equal to (`>=`)
144+
operators.
145+
"""
146+
147+
def __le__(self, other: Any, /) -> bool:
148+
"""
149+
Return True if self is less than or equal to other, otherwise False.
150+
151+
Args:
152+
other: The value to compare with.
153+
154+
Returns:
155+
bool: True if self is less than or equal to other, otherwise False.
156+
"""
157+
158+
def __ge__(self, other: Any, /) -> bool:
159+
"""
160+
Return True if self is greater than or equal to other, otherwise False.
161+
162+
Args:
163+
other: The value to compare with.
164+
165+
Returns:
166+
bool: True if self is greater than or equal to other, otherwise False.
167+
"""
168+
169+
137170
_T = TypeVar("_T")
138171

139172

@@ -147,6 +180,29 @@ class Bounds(Generic[_T]):
147180
upper: _T
148181
"""Upper bound."""
149182

183+
def __contains__(self, item: _T) -> bool:
184+
"""
185+
Check if the value is within the range of the container.
186+
187+
Args:
188+
item: The value to check.
189+
190+
Returns:
191+
bool: True if value is within the range, otherwise False.
192+
"""
193+
if item is None:
194+
return False
195+
196+
assert isinstance(item, Comparable)
197+
198+
if self.lower is not None and self.upper is not None:
199+
return self.lower <= item <= self.upper
200+
if self.lower is not None:
201+
return self.lower <= item
202+
if self.upper is not None:
203+
return item <= self.upper
204+
return False
205+
150206

151207
@dataclass(frozen=True, kw_only=True)
152208
class SystemBounds:
@@ -171,3 +227,21 @@ class SystemBounds:
171227
This is the range within which power requests are NOT allowed by the pool.
172228
If present, they will be a subset of the inclusion bounds.
173229
"""
230+
231+
def __contains__(self, item: Power) -> bool:
232+
"""
233+
Check if the value is within the range of the container.
234+
235+
Args:
236+
item: The value to check.
237+
238+
Returns:
239+
bool: True if value is within the range, otherwise False.
240+
"""
241+
if not self.inclusion_bounds:
242+
return False
243+
if item not in self.inclusion_bounds:
244+
return False
245+
if self.exclusion_bounds and item in self.exclusion_bounds:
246+
return False
247+
return True
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# License: MIT
2+
# Copyright © 2023 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for battery pool."""
5+
6+
7+
from datetime import datetime
8+
9+
from frequenz.sdk.timeseries._base_types import Bounds, SystemBounds
10+
from frequenz.sdk.timeseries._quantities import Power
11+
12+
13+
def test_bounds_contains() -> None:
14+
"""
15+
Test the `__contains__` method of the `Bounds` class.
16+
17+
This method checks if a value is within the defined bounds.
18+
"""
19+
bounds = Bounds(lower=Power.from_watts(10), upper=Power.from_watts(100))
20+
21+
assert Power.from_watts(50) in bounds # within bounds
22+
assert Power.from_watts(10) in bounds # at lower bound
23+
assert Power.from_watts(100) in bounds # at upper bound
24+
assert Power.from_watts(9) not in bounds # below lower bound
25+
assert Power.from_watts(101) not in bounds # above upper bound
26+
27+
bounds_no_lower = Bounds(lower=None, upper=Power.from_watts(100))
28+
assert Power.from_watts(50) in bounds_no_lower # within upper bound
29+
assert Power.from_watts(100) in bounds_no_lower # at upper bound
30+
assert Power.from_watts(101) not in bounds_no_lower # above upper bound
31+
32+
bounds_no_upper = Bounds(lower=Power.from_watts(10), upper=None)
33+
assert Power.from_watts(50) in bounds_no_upper # within lower bound
34+
assert Power.from_watts(10) in bounds_no_upper # at lower bound
35+
assert Power.from_watts(9) not in bounds_no_upper # below lower bound
36+
37+
# test succeeds, mypy 'Unsupported operand types for in ("Power" and "Bounds[None]")'
38+
# bounds_none = Bounds(lower=None, upper=None)
39+
# assert Power.from_watts(15) not in bounds_none
40+
41+
42+
def test_system_bounds_contains() -> None:
43+
"""
44+
Test the `__contains__` method of the `SystemBounds` class.
45+
46+
Checks if value is within inclusion bounds and not exclusion bounds.
47+
"""
48+
inclusion_bounds = Bounds(lower=Power.from_watts(10), upper=Power.from_watts(100))
49+
exclusion_bounds = Bounds(lower=Power.from_watts(40), upper=Power.from_watts(50))
50+
system_bounds = SystemBounds(
51+
timestamp=datetime.now(),
52+
inclusion_bounds=inclusion_bounds,
53+
exclusion_bounds=exclusion_bounds,
54+
)
55+
56+
assert Power.from_watts(30) in system_bounds # within inclusion, not in exclusion
57+
assert (
58+
Power.from_watts(45) not in system_bounds
59+
) # within inclusion, also in exclusion
60+
assert Power.from_watts(110) not in system_bounds # outside inclusion bounds
61+
62+
system_bounds_no_exclusion = SystemBounds(
63+
timestamp=datetime.now(),
64+
inclusion_bounds=inclusion_bounds,
65+
exclusion_bounds=None,
66+
)
67+
assert Power.from_watts(30) in system_bounds_no_exclusion # within inclusion bounds
68+
assert (
69+
Power.from_watts(110) not in system_bounds_no_exclusion
70+
) # outside inclusion bounds
71+
72+
system_bounds_no_inclusion = SystemBounds(
73+
timestamp=datetime.now(),
74+
inclusion_bounds=None,
75+
exclusion_bounds=exclusion_bounds,
76+
)
77+
assert (
78+
Power.from_watts(30) not in system_bounds_no_inclusion
79+
) # not in inclusion bounds

0 commit comments

Comments
 (0)