From eec88dcd31db05170a44c69048cb6b59effdd899 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Mon, 9 Jun 2025 14:12:04 +0200 Subject: [PATCH] Add operations to IntValue and FloatValue --- src/spellbind/float_values.py | 83 ++++++++++++--- src/spellbind/int_values.py | 59 +++++++++-- tests/test_float_values_arithmatic.py | 142 ++++++++++++++++++++++++++ tests/test_float_values_rounding.py | 59 +++++++++++ tests/test_int_values_arithmatic.py | 86 ++++++++++++++++ 5 files changed, 408 insertions(+), 21 deletions(-) create mode 100644 tests/test_float_values_rounding.py diff --git a/src/spellbind/float_values.py b/src/spellbind/float_values.py index c974efe..caa8a4a 100644 --- a/src/spellbind/float_values.py +++ b/src/spellbind/float_values.py @@ -1,13 +1,19 @@ from __future__ import annotations +from typing_extensions import Self import operator from abc import ABC, abstractmethod -from typing import Generic, Callable, Sequence, TypeVar +from typing import Generic, Callable, Sequence, TypeVar, overload + +from typing_extensions import TYPE_CHECKING from spellbind.bool_values import BoolValue -from spellbind.values import Value, SimpleVariable, DerivedValue, DerivedValueBase, Constant +from spellbind.values import Value, SimpleVariable, DerivedValue, DerivedValueBase, Constant, CombinedTwoValues + +if TYPE_CHECKING: + from spellbind.int_values import IntValue, IntLike -FloatLike = int | Value[int] | float | Value[float] +FloatLike = Value[int] | float | Value[float] _S = TypeVar("_S") _T = TypeVar("_T") @@ -39,6 +45,41 @@ def __truediv__(self, other: FloatLike) -> FloatValue: def __rtruediv__(self, other: int | float) -> FloatValue: return DivideValues(other, self) + def __pow__(self, other: FloatLike) -> FloatValue: + return PowerFloatValues(self, other) + + def __rpow__(self, other: FloatLike) -> FloatValue: + return PowerFloatValues(other, self) + + def __mod__(self, other: FloatLike) -> FloatValue: + return ModuloFloatValues(self, other) + + def __rmod__(self, other: int | float) -> FloatValue: + return ModuloFloatValues(other, self) + + def __abs__(self) -> FloatValue: + return AbsFloatValue(self) + + def floor(self) -> IntValue: + from spellbind.int_values import FloorFloatValue + return FloorFloatValue(self) + + def ceil(self) -> IntValue: + from spellbind.int_values import CeilFloatValue + return CeilFloatValue(self) + + @overload + def round(self) -> IntValue: ... + + @overload + def round(self, ndigits: IntLike) -> FloatValue: ... + + def round(self, ndigits: IntLike | None = None) -> FloatValue | IntValue: + if ndigits is None: + from spellbind.int_values import RoundFloatToIntValue + return RoundFloatToIntValue(self) + return RoundFloatValue(self, ndigits) + def __lt__(self, other: FloatLike) -> BoolValue: return CompareNumbersValues(self, other, operator.lt) @@ -54,6 +95,9 @@ def __ge__(self, other: FloatLike) -> BoolValue: def __neg__(self) -> FloatValue: return NegateFloatValue(self) + def __pos__(self) -> Self: + return self + class FloatConstant(FloatValue, Constant[float]): pass @@ -106,9 +150,7 @@ def value(self) -> _U: class CombinedTwoFloatValues(CombinedFloatValues[_U], Generic[_U], ABC): - def __init__(self, - left: float | Value[int] | Value[float], - right: float | Value[int] | Value[float]): + def __init__(self, left: FloatLike, right: FloatLike): super().__init__(left, right) def transform(self, values: Sequence[float]) -> _U: @@ -128,9 +170,6 @@ def transform(self, values: Sequence[float]) -> float: class SubtractFloatValues(CombinedTwoFloatValues[float], FloatValue): - def __init__(self, left: FloatLike, right: FloatLike): - super().__init__(left, right) - def transform_two(self, left: float, right: float) -> float: return left - right @@ -147,9 +186,6 @@ def transform(self, values: Sequence[float]) -> float: class DivideValues(CombinedTwoFloatValues[float], FloatValue): - def __init__(self, left: FloatLike, right: FloatLike): - super().__init__(left, right) - def transform_two(self, left: float, right: float) -> float: return left / right @@ -158,6 +194,29 @@ class FloatVariable(SimpleVariable[float], FloatValue): pass +class RoundFloatValue(CombinedTwoValues[float, int, float], FloatValue): + def __init__(self, value: FloatValue, ndigits: IntLike): + super().__init__(value, ndigits) + + def transform(self, value: float, ndigits: int) -> float: + return round(value, ndigits) + + +class ModuloFloatValues(CombinedTwoFloatValues[float], FloatValue): + def transform_two(self, left: float, right: float) -> float: + return left % right + + +class AbsFloatValue(DerivedValue[float, float], FloatValue): + def transform(self, value: float) -> float: + return abs(value) + + +class PowerFloatValues(CombinedTwoFloatValues[float], FloatValue): + def transform_two(self, left: float, right: float) -> float: + return left ** right + + class NegateFloatValue(DerivedValue[float, float], FloatValue): def transform(self, value: float) -> float: return -value diff --git a/src/spellbind/int_values.py b/src/spellbind/int_values.py index a4be7c4..5f74717 100644 --- a/src/spellbind/int_values.py +++ b/src/spellbind/int_values.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing_extensions import Self +import math import operator from abc import ABC from typing import overload @@ -92,6 +94,21 @@ def __floordiv__(self, other: IntLike) -> IntValue: def __rfloordiv__(self, other: int) -> IntValue: return FloorDivideIntValues(other, self) + def __pow__(self, other: IntLike) -> IntValue: + return PowerIntValues(self, other) + + def __rpow__(self, other: int) -> IntValue: + return PowerIntValues(other, self) + + def __mod__(self, other: IntLike) -> IntValue: + return ModuloIntValues(self, other) + + def __rmod__(self, other: int) -> IntValue: + return ModuloIntValues(other, self) + + def __abs__(self) -> IntValue: + return AbsIntValue(self) + def __lt__(self, other: FloatLike) -> BoolValue: return CompareNumbersValues(self, other, operator.lt) @@ -107,6 +124,9 @@ def __ge__(self, other: FloatLike) -> BoolValue: def __neg__(self) -> IntValue: return NegateIntValue(self) + def __pos__(self) -> Self: + return self + class IntConstant(IntValue, Constant[int]): pass @@ -122,9 +142,6 @@ def transform(self, *values: int) -> int: class SubtractIntValues(CombinedTwoValues[int, int, int], IntValue): - def __init__(self, left: IntLike, right: IntLike): - super().__init__(left, right) - def transform(self, left: int, right: int) -> int: return left - right @@ -138,21 +155,45 @@ def transform(self, *values: int) -> int: class DivideIntValues(CombinedTwoValues[int, int, float], FloatValue): - def __init__(self, left: IntLike, right: IntLike): - super().__init__(left, right) - def transform(self, left: int, right: int) -> float: return left / right class FloorDivideIntValues(CombinedTwoValues[int, int, int], IntValue): - def __init__(self, left: IntLike, right: IntLike): - super().__init__(left, right) - def transform(self, left: int, right: int) -> int: return left // right +class PowerIntValues(CombinedTwoValues[int, int, int], IntValue): + def transform(self, left: int, right: int) -> int: + return left ** right + + +class ModuloIntValues(CombinedTwoValues[int, int, int], IntValue): + def transform(self, left: int, right: int) -> int: + return left % right + + +class AbsIntValue(DerivedValue[int, int], IntValue): + def transform(self, value: int) -> int: + return abs(value) + + class NegateIntValue(DerivedValue[int, int], IntValue): def transform(self, value: int) -> int: return -value + + +class FloorFloatValue(DerivedValue[float, int], IntValue): + def transform(self, value: float) -> int: + return math.floor(value) + + +class CeilFloatValue(DerivedValue[float, int], IntValue): + def transform(self, value: float) -> int: + return math.ceil(value) + + +class RoundFloatToIntValue(DerivedValue[float, int], IntValue): + def transform(self, value: float) -> int: + return round(value) diff --git a/tests/test_float_values_arithmatic.py b/tests/test_float_values_arithmatic.py index a9f0867..17da5ab 100644 --- a/tests/test_float_values_arithmatic.py +++ b/tests/test_float_values_arithmatic.py @@ -247,3 +247,145 @@ def test_negate_float_value_zero(): v0.value = 7.8 assert v1.value == -7.8 + + +# Power Tests +def test_power_float_values(): + v0 = FloatVariable(2.0) + v1 = FloatVariable(3.0) + v2 = v0 ** v1 + assert v2.value == 8.0 + + v0.value = 3.0 + assert v2.value == 27.0 + + +def test_power_float_value_to_float(): + v0 = FloatVariable(2.0) + v2 = v0 ** 3.0 + assert v2.value == 8.0 + + v0.value = 3.0 + assert v2.value == 27.0 + + +def test_power_float_value_to_int(): + v0 = FloatVariable(2.5) + v2 = v0 ** 2 + assert v2.value == 6.25 + + v0.value = 3.0 + assert v2.value == 9.0 + + +def test_power_float_value_to_int_value(): + v0 = FloatVariable(2.0) + v1 = IntVariable(3) + v2 = v0 ** v1 + assert v2.value == 8.0 + + v0.value = 3.0 + assert v2.value == 27.0 + + +def test_power_float_to_float_value(): + v1 = FloatVariable(3.0) + v2 = 2.0 ** v1 + assert v2.value == 8.0 + + v1.value = 4.0 + assert v2.value == 16.0 + + +def test_power_int_to_float_value(): + v1 = FloatVariable(3.0) + v2 = 2 ** v1 + assert v2.value == 8.0 + + v1.value = 4.0 + assert v2.value == 16.0 + + +# Modulo Tests +def test_modulo_float_values(): + v0 = FloatVariable(10.5) + v1 = FloatVariable(3.0) + v2 = v0 % v1 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_value_by_float(): + v0 = FloatVariable(10.5) + v2 = v0 % 3.0 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_value_by_int(): + v0 = FloatVariable(10.5) + v2 = v0 % 3 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_value_by_int_value(): + v0 = FloatVariable(10.5) + v1 = IntVariable(3) + v2 = v0 % v1 + assert v2.value == 1.5 + + v0.value = 15.5 + assert v2.value == 0.5 + + +def test_modulo_float_by_float_value(): + v1 = FloatVariable(3.0) + v2 = 10.5 % v1 + assert v2.value == 1.5 + + v1.value = 4.0 + assert v2.value == 2.5 + + +def test_modulo_int_by_float_value(): + v1 = FloatVariable(3.0) + v2 = 10 % v1 + assert v2.value == 1.0 + + v1.value = 4.0 + assert v2.value == 2.0 + + +# Absolute Value Tests +def test_abs_float_value_positive(): + v0 = FloatVariable(5.5) + v1 = abs(v0) + assert v1.value == 5.5 + + v0.value = 10.8 + assert v1.value == 10.8 + + +def test_abs_float_value_negative(): + v0 = FloatVariable(-5.5) + v1 = abs(v0) + assert v1.value == 5.5 + + v0.value = -10.8 + assert v1.value == 10.8 + + +def test_abs_float_value_zero(): + v0 = FloatVariable(0.0) + v1 = abs(v0) + assert v1.value == 0.0 + + v0.value = -7.2 + assert v1.value == 7.2 diff --git a/tests/test_float_values_rounding.py b/tests/test_float_values_rounding.py new file mode 100644 index 0000000..2fe7297 --- /dev/null +++ b/tests/test_float_values_rounding.py @@ -0,0 +1,59 @@ +from conftest import OneParameterObserver +from spellbind.float_values import FloatVariable +from spellbind.int_values import IntVariable + + +def test_floor_float_value(): + v0 = FloatVariable(3.7) + v1 = v0.floor() + assert v1.value == 3 + + v0.value = -2.3 + assert v1.value == -3 + + +def test_ceil_float_value(): + v0 = FloatVariable(3.2) + v1 = v0.ceil() + assert v1.value == 4 + + v0.value = -2.8 + assert v1.value == -2 + + +def test_round_float_value_no_digits(): + v0 = FloatVariable(3.7) + v1 = v0.round() + assert v1.value == 4 + + v0.value = 2.3 + assert v1.value == 2 + + +def test_round_float_value_with_digits(): + v0 = FloatVariable(3.14159) + v1 = v0.round(2) + assert v1.value == 3.14 + + v0.value = 2.71828 + assert v1.value == 2.72 + + +def test_round_float_value_with_int_value_digits(): + v0 = FloatVariable(3.14159) + v1 = IntVariable(2) + v2 = v0.round(v1) + assert v2.value == 3.14 + + v1.value = 3 + assert v2.value == 3.142 + + +def test_round_float_change_comma_doesnt_change_int_value(): + v0 = FloatVariable(3.14159) + rounded = v0.round() + observer = OneParameterObserver() + rounded.observe(observer) + assert rounded.value == 3 + + observer.assert_not_called() diff --git a/tests/test_int_values_arithmatic.py b/tests/test_int_values_arithmatic.py index 08b5450..1984018 100644 --- a/tests/test_int_values_arithmatic.py +++ b/tests/test_int_values_arithmatic.py @@ -275,3 +275,89 @@ def test_negate_int_value_zero(): v0.value = 7 assert v1.value == -7 + + +# Power Tests +def test_power_int_values(): + v0 = IntVariable(2) + v1 = IntVariable(3) + v2 = v0 ** v1 + assert v2.value == 8 + + v0.value = 3 + assert v2.value == 27 + + +def test_power_int_value_to_int(): + v0 = IntVariable(2) + v2 = v0 ** 3 + assert v2.value == 8 + + v0.value = 3 + assert v2.value == 27 + + +def test_power_int_to_int_value(): + v1 = IntVariable(3) + v2 = 2 ** v1 + assert v2.value == 8 + + v1.value = 4 + assert v2.value == 16 + + +# Modulo Tests +def test_modulo_int_values(): + v0 = IntVariable(10) + v1 = IntVariable(3) + v2 = v0 % v1 + assert v2.value == 1 + + v0.value = 15 + assert v2.value == 0 + + +def test_modulo_int_value_by_int(): + v0 = IntVariable(10) + v2 = v0 % 3 + assert v2.value == 1 + + v0.value = 15 + assert v2.value == 0 + + +def test_modulo_int_by_int_value(): + v1 = IntVariable(3) + v2 = 10 % v1 + assert v2.value == 1 + + v1.value = 4 + assert v2.value == 2 + + +# Absolute Value Tests +def test_abs_int_value_positive(): + v0 = IntVariable(5) + v1 = abs(v0) + assert v1.value == 5 + + v0.value = 10 + assert v1.value == 10 + + +def test_abs_int_value_negative(): + v0 = IntVariable(-5) + v1 = abs(v0) + assert v1.value == 5 + + v0.value = -10 + assert v1.value == 10 + + +def test_abs_int_value_zero(): + v0 = IntVariable(0) + v1 = abs(v0) + assert v1.value == 0 + + v0.value = -7 + assert v1.value == 7