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
23 changes: 23 additions & 0 deletions src/spellbind/float_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ def __neg__(self) -> FloatValue:
def __pos__(self) -> Self:
return self

def clamp(self, min_value: FloatLike, max_value: FloatLike) -> FloatValue:
return ClampFloatValue(self, min_value, max_value)


class MappedFloatValue(Generic[_S], DerivedValue[_S, float], FloatValue):
def __init__(self, value: Value[_S], transform: Callable[[_S], float]) -> None:
Expand Down Expand Up @@ -186,6 +189,18 @@ def transform_two(self, left: float, right: float) -> _U:
raise NotImplementedError


class CombinedThreeFloatValues(CombinedFloatValues[_U], Generic[_U], ABC):
def __init__(self, left: FloatLike, middle: FloatLike, right: FloatLike):
super().__init__(left, middle, right)

def transform(self, values: Sequence[float]) -> _U:
return self.transform_three(values[0], values[1], values[2])

@abstractmethod
def transform_three(self, left: float, middle: float, right: float) -> _U:
raise NotImplementedError


class AddFloatValues(CombinedFloatValues[float], FloatValue):
def transform(self, values: Sequence[float]) -> float:
return sum(values)
Expand Down Expand Up @@ -244,3 +259,11 @@ def __init__(self, left: FloatLike, right: FloatLike, op: Callable[[float, float

def transform_two(self, left: float, right: float) -> bool:
return self._op(left, right)


class ClampFloatValue(CombinedThreeFloatValues[float], FloatValue):
def __init__(self, value: FloatLike, min_value: FloatLike, max_value: FloatLike) -> None:
super().__init__(value, min_value, max_value)

def transform_three(self, value: float, min_value: float, max_value: float) -> float:
return max(min_value, min(max_value, value))
19 changes: 16 additions & 3 deletions src/spellbind/int_values.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from __future__ import annotations
from typing_extensions import Self, TypeVar

import math
import operator
from abc import ABC
from typing import overload, Generic, Callable

from typing_extensions import Self, TypeVar

from spellbind.bool_values import BoolValue
from spellbind.float_values import FloatValue, MultiplyFloatValues, DivideValues, SubtractFloatValues, \
AddFloatValues, CompareNumbersValues
from spellbind.values import Value, CombinedMixedValues, SimpleVariable, CombinedTwoValues, DerivedValue, Constant
from spellbind.bool_values import BoolValue
from spellbind.values import Value, CombinedMixedValues, SimpleVariable, CombinedTwoValues, DerivedValue, Constant, \
CombinedThreeValues

IntLike = int | Value[int]
FloatLike = IntLike | float | FloatValue
Expand Down Expand Up @@ -130,6 +132,9 @@ def __neg__(self) -> IntValue:
def __pos__(self) -> Self:
return self

def clamp(self, min_value: IntLike, max_value: IntLike) -> IntValue:
return ClampIntValue(self, min_value, max_value)


class MappedIntValue(Generic[_S], DerivedValue[_S, int], IntValue):
def __init__(self, value: Value[_S], transform: Callable[[_S], int]) -> None:
Expand Down Expand Up @@ -219,3 +224,11 @@ def transform(self, value: float) -> int:
class RoundFloatToIntValue(DerivedValue[float, int], IntValue):
def transform(self, value: float) -> int:
return round(value)


class ClampIntValue(CombinedThreeValues[int, int], IntValue):
def __init__(self, value: IntLike, min_value: IntLike, max_value: IntLike) -> None:
super().__init__(value, min_value, max_value)

def transform_three(self, value: int, min_value: int, max_value: int) -> int:
return max(min_value, min(max_value, value))
12 changes: 12 additions & 0 deletions src/spellbind/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,3 +323,15 @@ def transform(self, *args: _S) -> _T:
@property
def value(self) -> _T:
return self._value


class CombinedThreeValues(CombinedMixedValues[_S, _T], Generic[_S, _T], ABC):
def __init__(self, left: Value[_S] | _S, middle: Value[_S] | _S, right: Value[_S] | _S):
super().__init__(left, middle, right)

def transform(self, *values: _S) -> _T:
return self.transform_three(values[0], values[1], values[2])

@abstractmethod
def transform_three(self, left: _S, middle: _S, right: _S) -> _T:
raise NotImplementedError
84 changes: 84 additions & 0 deletions tests/test_values/test_float_values/test_float_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,87 @@ def test_add_int_values_garbage_collected():
v1.value = 4.5 # trigger removal of weak references
assert len(v0._on_change._subscriptions) == 0
assert len(v1._on_change._subscriptions) == 0


def test_clamp_float_values_in_range():
value = FloatVariable(15.5)
min_val = FloatVariable(10.0)
max_val = FloatVariable(20.0)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 15.5


def test_clamp_float_values_below_min():
value = FloatVariable(5.2)
min_val = FloatVariable(10.0)
max_val = FloatVariable(20.0)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 10.0


def test_clamp_float_values_above_max():
value = FloatVariable(25.8)
min_val = FloatVariable(10.0)
max_val = FloatVariable(20.0)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 20.0


def test_clamp_float_values_with_literals_in_range():
value = FloatVariable(15.5)

clamped = value.clamp(10.0, 20.0)
assert clamped.value == 15.5


def test_clamp_float_values_with_literals_below_min():
value = FloatVariable(5.2)

clamped = value.clamp(10.0, 20.0)
assert clamped.value == 10.0


def test_clamp_float_values_with_literals_above_max():
value = FloatVariable(25.8)

clamped = value.clamp(10.0, 20.0)
assert clamped.value == 20.0


def test_clamp_float_values_reactive_value_changes():
value = FloatVariable(15.5)
min_val = FloatVariable(10.0)
max_val = FloatVariable(20.0)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 15.5

value.value = 5.2
assert clamped.value == 10.0

value.value = 25.8
assert clamped.value == 20.0

value.value = 12.3
assert clamped.value == 12.3


def test_clamp_float_values_reactive_bounds_changes():
value = FloatVariable(15.5)
min_val = FloatVariable(10.0)
max_val = FloatVariable(20.0)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 15.5

min_val.value = 18.0
assert clamped.value == 18.0

min_val.value = 11.0
assert clamped.value == 15.5

max_val.value = 12.0
assert clamped.value == 12.0
86 changes: 85 additions & 1 deletion tests/test_values/test_int_values/test_int_values.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from spellbind.int_values import IntConstant, MaxIntValues, MinIntValues
from spellbind.int_values import IntConstant, MaxIntValues, MinIntValues, IntVariable
from spellbind.values import SimpleVariable


Expand Down Expand Up @@ -49,3 +49,87 @@ def test_min_int_values_with_literals():

a.value = 5
assert min_val.value == 5


def test_clamp_int_values_in_range():
value = IntVariable(15)
min_val = IntVariable(10)
max_val = IntVariable(20)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 15


def test_clamp_int_values_below_min():
value = IntVariable(5)
min_val = IntVariable(10)
max_val = IntVariable(20)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 10


def test_clamp_int_values_above_max():
value = IntVariable(25)
min_val = IntVariable(10)
max_val = IntVariable(20)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 20


def test_clamp_int_values_with_literals_in_range():
value = IntVariable(15)

clamped = value.clamp(10, 20)
assert clamped.value == 15


def test_clamp_int_values_with_literals_below_min():
value = IntVariable(5)

clamped = value.clamp(10, 20)
assert clamped.value == 10


def test_clamp_int_values_with_literals_above_max():
value = IntVariable(25)

clamped = value.clamp(10, 20)
assert clamped.value == 20


def test_clamp_int_values_reactive_value_changes():
value = IntVariable(15)
min_val = IntVariable(10)
max_val = IntVariable(20)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 15

value.value = 5
assert clamped.value == 10

value.value = 25
assert clamped.value == 20

value.value = 12
assert clamped.value == 12


def test_clamp_int_values_reactive_bounds_changes():
value = IntVariable(15)
min_val = IntVariable(10)
max_val = IntVariable(20)

clamped = value.clamp(min_val, max_val)
assert clamped.value == 15

min_val.value = 18
assert clamped.value == 18

min_val.value = 11
assert clamped.value == 15

max_val.value = 12
assert clamped.value == 12