Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/keylabeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ caseSensitive: true
# Explicit keyword mappings to labels. Form of match:label. Required.
labelMappings:
"part:asyncio": "part:asyncio"
"part:collections": "part:collections"
"part:datetime": "part:datetime"
"part:docs": "part:docs"
"part:math": "part:math"
Expand Down
8 changes: 0 additions & 8 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,6 @@
- "tests/test_asyncio.py"
- "tests/asyncio/**"

"part:collections":
- changed-files:
- any-glob-to-any-file:
- "src/frequenz/core/collections.py"
- "src/frequenz/core/collections/**"
- "tests/test_collections.py"
- "tests/collections/**"

"part:datetime":
- changed-files:
- any-glob-to-any-file:
Expand Down
86 changes: 0 additions & 86 deletions src/frequenz/core/collections.py

This file was deleted.

91 changes: 91 additions & 0 deletions src/frequenz/core/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"""Internal math tools."""

import math
from dataclasses import dataclass
from typing import Generic, Protocol, Self, TypeVar, cast


def is_close_to_zero(value: float, abs_tol: float = 1e-9) -> bool:
Expand All @@ -23,3 +25,92 @@ def is_close_to_zero(value: float, abs_tol: float = 1e-9) -> bool:
"""
zero: float = 0.0
return math.isclose(a=value, b=zero, abs_tol=abs_tol)


class LessThanComparable(Protocol):
"""A protocol that requires the `__lt__` method to compare values."""

def __lt__(self, other: Self, /) -> bool:
"""Return whether self is less than other."""


LessThanComparableOrNoneT = TypeVar(
"LessThanComparableOrNoneT", bound=LessThanComparable | None
)
"""Type variable for a value that a `LessThanComparable` or `None`."""


@dataclass(frozen=True, repr=False)
class Interval(Generic[LessThanComparableOrNoneT]):
"""An interval to test if a value is within its limits.

The `start` and `end` are inclusive, meaning that the `start` and `end` limites are
included in the range when checking if a value is contained by the interval.

If the `start` or `end` is `None`, it means that the interval is unbounded in that
direction.

If `start` is bigger than `end`, a `ValueError` is raised.

The type stored in the interval must be comparable, meaning that it must implement
the `__lt__` method to be able to compare values.
"""

start: LessThanComparableOrNoneT
"""The start of the interval."""

end: LessThanComparableOrNoneT
"""The end of the interval."""

def __post_init__(self) -> None:
"""Check if the start is less than or equal to the end."""
if self.start is None or self.end is None:
return
start = cast(LessThanComparable, self.start)
end = cast(LessThanComparable, self.end)
if start > end:
raise ValueError(
f"The start ({self.start}) can't be bigger than end ({self.end})"
)

def __contains__(self, item: LessThanComparableOrNoneT) -> bool:
"""
Check if the value is within the range of the container.

Args:
item: The value to check.

Returns:
bool: True if value is within the range, otherwise False.
"""
if item is None:
return False
casted_item = cast(LessThanComparable, item)

if self.start is None and self.end is None:
return True
if self.start is None:
start = cast(LessThanComparable, self.end)
return not casted_item > start
if self.end is None:
return not self.start > item
# mypy seems to get confused here, not being able to narrow start and end to
# just LessThanComparable, complaining with:
# error: Unsupported left operand type for <= (some union)
# But we know if they are not None, they should be LessThanComparable, and
# actually mypy is being able to figure it out in the lines above, just not in
# this one, so it should be safe to cast.
return not (
casted_item < cast(LessThanComparable, self.start)
or casted_item > cast(LessThanComparable, self.end)
)

def __repr__(self) -> str:
"""Return a string representation of this instance."""
return f"Interval({self.start!r}, {self.end!r})"

def __str__(self) -> str:
"""Return a string representation of this instance."""
start = "∞" if self.start is None else str(self.start)
end = "∞" if self.end is None else str(self.end)
return f"[{start}, {end}]"
10 changes: 5 additions & 5 deletions tests/test_collections.py → tests/math/test_interval.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import pytest

from frequenz.core.collections import Interval, LessThanComparable
from frequenz.core.math import Interval, LessThanComparable


class CustomComparable:
Expand Down Expand Up @@ -64,7 +64,7 @@ def test_invalid_range(start: LessThanComparable, end: LessThanComparable) -> No
),
],
)
def test_interval_contains( # pylint: disable=too-many-arguments
def test_contains( # pylint: disable=too-many-arguments
start: LessThanComparable,
end: LessThanComparable,
within: LessThanComparable,
Expand Down Expand Up @@ -94,7 +94,7 @@ def test_interval_contains( # pylint: disable=too-many-arguments
),
],
)
def test_interval_contains_no_start(
def test_contains_no_start(
end: LessThanComparable,
within: LessThanComparable,
at_end: LessThanComparable,
Expand All @@ -119,7 +119,7 @@ def test_interval_contains_no_start(
),
],
)
def test_interval_contains_no_end(
def test_contains_no_end(
start: LessThanComparable,
within: LessThanComparable,
at_start: LessThanComparable,
Expand All @@ -143,7 +143,7 @@ def test_interval_contains_no_end(
CustomComparable(-10),
],
)
def test_interval_contains_unbound(value: LessThanComparable) -> None:
def test_contains_unbound(value: LessThanComparable) -> None:
"""Test if a value is within the interval with no bounds."""
interval_no_bounds: Interval[LessThanComparable | None] = Interval(
start=None, end=None
Expand Down
12 changes: 5 additions & 7 deletions tests/test_math.py → tests/math/test_is_close_to_zero.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,28 @@
# basic cases not working.


def test_is_close_to_zero_default_tolerance() -> None:
def test_default_tolerance() -> None:
"""Test the is_close_to_zero function with the default tolerance."""
assert is_close_to_zero(0.0)
assert is_close_to_zero(1e-10)
assert not is_close_to_zero(1e-8)


def test_is_close_to_zero_custom_tolerance() -> None:
def test_custom_tolerance() -> None:
"""Test the is_close_to_zero function with a custom tolerance."""
assert is_close_to_zero(0.0, abs_tol=1e-8)
assert is_close_to_zero(1e-8, abs_tol=1e-8)
assert not is_close_to_zero(1e-7, abs_tol=1e-8)


def test_is_close_to_zero_negative_values() -> None:
def test_negative_values() -> None:
"""Test the is_close_to_zero function with negative values."""
assert is_close_to_zero(-1e-10)
assert not is_close_to_zero(-1e-8)


@given(st.floats(allow_nan=False, allow_infinity=False))
def test_is_close_to_zero_default_tolerance_hypothesis(value: float) -> None:
def test_default_tolerance_hypothesis(value: float) -> None:
"""Test the is_close_to_zero function with the default tolerance for many values."""
if -1e-9 <= value <= 1e-9:
assert is_close_to_zero(value)
Expand All @@ -45,9 +45,7 @@ def test_is_close_to_zero_default_tolerance_hypothesis(value: float) -> None:
st.floats(allow_nan=False, allow_infinity=False),
st.floats(allow_nan=False, allow_infinity=False, min_value=0.0, max_value=2.0),
)
def test_is_close_to_zero_custom_tolerance_hypothesis(
value: float, abs_tol: float
) -> None:
def test_custom_tolerance_hypothesis(value: float, abs_tol: float) -> None:
"""Test the is_close_to_zero function with a custom tolerance with many values/tolerance."""
if -abs_tol <= value <= abs_tol:
assert is_close_to_zero(value, abs_tol=abs_tol)
Expand Down