Skip to content

Commit 1976fc1

Browse files
authored
Add contains function to Bounds and SystemBounds (#962)
2 parents 04ad6e6 + 4863fed commit 1976fc1

File tree

3 files changed

+146
-2
lines changed

3 files changed

+146
-2
lines changed

RELEASE_NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
<!-- Here goes the main new features and examples or instructions on how to use them -->
1717

18+
- Classes Bounds and SystemBounds now work with the `in` operator
19+
1820
## Bug Fixes
1921

2022
- Fixed a typing issue that occurs in some cases when composing formulas with constants.

src/frequenz/sdk/timeseries/_base_types.py

Lines changed: 52 additions & 2 deletions
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, cast, overload
1212

1313
from ._quantities import Power, QuantityT
1414

@@ -134,7 +134,22 @@ def map(
134134
)
135135

136136

137-
_T = TypeVar("_T")
137+
class Comparable(Protocol):
138+
"""A protocol that requires the implementation of comparison methods.
139+
140+
This protocol is used to ensure that types can be compared using
141+
the less than or equal to (`<=`) and greater than or equal to (`>=`)
142+
operators.
143+
"""
144+
145+
def __le__(self, other: Any, /) -> bool:
146+
"""Return whether this instance is less than or equal to `other`."""
147+
148+
def __ge__(self, other: Any, /) -> bool:
149+
"""Return whether this instance is greater than or equal to `other`."""
150+
151+
152+
_T = TypeVar("_T", bound=Comparable | None)
138153

139154

140155
@dataclass(frozen=True)
@@ -147,6 +162,25 @@ class Bounds(Generic[_T]):
147162
upper: _T
148163
"""Upper bound."""
149164

165+
def __contains__(self, item: _T) -> bool:
166+
"""
167+
Check if the value is within the range of the container.
168+
169+
Args:
170+
item: The value to check.
171+
172+
Returns:
173+
bool: True if value is within the range, otherwise False.
174+
"""
175+
if self.lower is None and self.upper is None:
176+
return True
177+
if self.lower is None:
178+
return item <= self.upper
179+
if self.upper is None:
180+
return self.lower <= item
181+
182+
return cast(Comparable, self.lower) <= item <= cast(Comparable, self.upper)
183+
150184

151185
@dataclass(frozen=True, kw_only=True)
152186
class SystemBounds:
@@ -171,3 +205,19 @@ class SystemBounds:
171205
This is the range within which power requests are NOT allowed by the pool.
172206
If present, they will be a subset of the inclusion bounds.
173207
"""
208+
209+
def __contains__(self, item: Power) -> bool:
210+
"""
211+
Check if the value is within the range of the container.
212+
213+
Args:
214+
item: The value to check.
215+
216+
Returns:
217+
bool: True if value is within the range, otherwise False.
218+
"""
219+
if not self.inclusion_bounds or item not in self.inclusion_bounds:
220+
return False
221+
if self.exclusion_bounds and item in self.exclusion_bounds:
222+
return False
223+
return True
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# License: MIT
2+
# Copyright © 2024 Frequenz Energy-as-a-Service GmbH
3+
4+
"""Tests for timeseries base types."""
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+
"""Tests with complete bounds."""
15+
bounds = Bounds(lower=Power.from_watts(10), upper=Power.from_watts(100))
16+
assert Power.from_watts(50) in bounds # within
17+
assert Power.from_watts(10) in bounds # at lower
18+
assert Power.from_watts(100) in bounds # at upper
19+
assert Power.from_watts(9) not in bounds # below lower
20+
assert Power.from_watts(101) not in bounds # above upper
21+
22+
23+
def test_bounds_contains_no_lower() -> None:
24+
"""Tests without lower bound."""
25+
bounds_no_lower = Bounds(lower=None, upper=Power.from_watts(100))
26+
assert Power.from_watts(50) in bounds_no_lower # within upper
27+
assert Power.from_watts(100) in bounds_no_lower # at upper
28+
assert Power.from_watts(101) not in bounds_no_lower # above upper
29+
30+
31+
def test_bounds_contains_no_upper() -> None:
32+
"""Tests without upper bound."""
33+
bounds_no_upper = Bounds(lower=Power.from_watts(10), upper=None)
34+
assert Power.from_watts(50) in bounds_no_upper # within lower
35+
assert Power.from_watts(10) in bounds_no_upper # at lower
36+
assert Power.from_watts(9) not in bounds_no_upper # below lower
37+
38+
39+
def test_bounds_contains_no_bounds() -> None:
40+
"""Tests with no bounds."""
41+
bounds_no_bounds: Bounds[Power | None] = Bounds(lower=None, upper=None)
42+
assert Power.from_watts(50) in bounds_no_bounds # any value within bounds
43+
44+
45+
INCLUSION_BOUND = Bounds(lower=Power.from_watts(10), upper=Power.from_watts(100))
46+
EXCLUSION_BOUND = Bounds(lower=Power.from_watts(40), upper=Power.from_watts(50))
47+
48+
49+
def test_system_bounds_contains() -> None:
50+
"""Tests with complete system bounds."""
51+
system_bounds = SystemBounds(
52+
timestamp=datetime.now(),
53+
inclusion_bounds=INCLUSION_BOUND,
54+
exclusion_bounds=EXCLUSION_BOUND,
55+
)
56+
57+
assert Power.from_watts(30) in system_bounds # within inclusion, not in exclusion
58+
assert Power.from_watts(45) not in system_bounds # within inclusion and exclusion
59+
assert Power.from_watts(110) not in system_bounds # outside inclusion
60+
61+
62+
def test_system_bounds_contains_no_exclusion() -> None:
63+
"""Tests with no exclusion bounds."""
64+
system_bounds_no_exclusion = SystemBounds(
65+
timestamp=datetime.now(),
66+
inclusion_bounds=INCLUSION_BOUND,
67+
exclusion_bounds=None,
68+
)
69+
assert Power.from_watts(30) in system_bounds_no_exclusion # within inclusion
70+
assert Power.from_watts(110) not in system_bounds_no_exclusion # outside inclusion
71+
72+
73+
def test_system_bounds_contains_no_inclusion() -> None:
74+
"""Tests with no inclusion bounds."""
75+
system_bounds_no_inclusion = SystemBounds(
76+
timestamp=datetime.now(),
77+
inclusion_bounds=None,
78+
exclusion_bounds=EXCLUSION_BOUND,
79+
)
80+
assert Power.from_watts(30) not in system_bounds_no_inclusion # outside exclusion
81+
assert Power.from_watts(45) not in system_bounds_no_inclusion # within exclusion
82+
83+
84+
def test_system_bounds_contains_no_bounds() -> None:
85+
"""Tests with no bounds."""
86+
system_bounds_no_bounds = SystemBounds(
87+
timestamp=datetime.now(),
88+
inclusion_bounds=None,
89+
exclusion_bounds=None,
90+
)
91+
assert Power.from_watts(30) not in system_bounds_no_bounds # any value outside
92+
assert Power.from_watts(110) not in system_bounds_no_bounds # any value outside

0 commit comments

Comments
 (0)