diff --git a/adafruit_simplemath.py b/adafruit_simplemath.py index 10326f0..4f2aac7 100644 --- a/adafruit_simplemath.py +++ b/adafruit_simplemath.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: Copyright (c) 2021 Dan Halbert for Adafruit Industries LLC +# SPDX-FileCopyrightText: 2021 James Carr # # SPDX-License-Identifier: MIT """ @@ -8,7 +9,7 @@ Math utility functions -* Author(s): Adafruit Industries +* Author(s): Dan Halbert, James Carr Implementation Notes -------------------- @@ -27,12 +28,59 @@ def map_range( x: float, in_min: float, in_max: float, out_min: float, out_max: float ) -> float: """ - Maps a number from one range to another. Somewhat similar to the Arduino ``map()`` function, - but returns a floating point result, and constrains the output value to be between - ``out_min`` and ``out_max``. - If ``in_min`` is greater than ``in_max`` or ``out_min`` is greater than ``out_max``, - the corresponding range is reversed, allowing, for example, mapping a range of 0-10 to 50-0. + Maps a number from one range to another. Somewhat similar to the Arduino + :attr:`map()` function, but returns a floating point result, and + constrains the output value to be between :attr:`out_min` and + :attr:`out_max`. If :attr:`in_min` is greater than :attr:`in_max` or + :attr:`out_min` is greater than :attr:`out_max`, the corresponding range + is reversed, allowing, for example, mapping a range of 0-10 to 50-0. + See also :py:func:`map_unconstrained_range` + + .. code-block:: + + from adafruit_simplemath import map_range + + percent = 23 + screen_width = 320 # or board.DISPLAY.width + x = map_range(percent, 0, 100, 0, screen_width - 1) + print("X position", percent, "% from the left of screen is", x) + + :param float x: Value to convert + :param float in_min: Start value of input range. + :param float in_max: End value of input range. + :param float out_min: Start value of output range. + :param float out_max: End value of output range. + :return: Returns value mapped to new range. + :rtype: float + """ + + mapped = map_unconstrained_range(x, in_min, in_max, out_min, out_max) + return constrain(mapped, out_min, out_max) + + +def map_unconstrained_range( + x: float, in_min: float, in_max: float, out_min: float, out_max: float +) -> float: + """ + Maps a number from one range to another. Somewhat similar to the Arduino + :attr:`map()` function, but returns a floating point result, and + does not constrain the output value to be between :attr:`out_min` and + :attr:`out_max`. If :attr:`in_min` is greater than :attr:`in_max` or + :attr:`out_min` is greater than :attr:`out_max`, the corresponding range + is reversed, allowing, for example, mapping a range of 0-10 to 50-0. + + See also :py:func:`map_range` + + .. code-block:: + + from adafruit_simplemath import map_unconstrained_range + + celsius = -20 + fahrenheit = map_unconstrained_range(celsius, 0, 100, 32, 212) + print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") + + :param float x: Value to convert :param float in_min: Start value of input range. :param float in_max: End value of input range. :param float out_min: Start value of output range. @@ -50,22 +98,26 @@ def map_range( mapped = 0.5 mapped *= out_max - out_min mapped += out_min - if out_min <= out_max: - return max(min(mapped, out_max), out_min) - return min(max(mapped, out_max), out_min) + + return mapped def constrain(x: float, out_min: float, out_max: float) -> float: - """Constrains ``x`` to be within the inclusive range [``out_min``, ``out_max``]. - Sometimes called ``clip`` or ``clamp`` in other libraries. - ``out_min`` should be less than or equal to ``out_max``. - If ``x`` is less than ``out_min``, return ``out_min``. - If ``x`` is greater than ``out_max``, return ``out_max``. - Otherwise just return ``x``. + """Constrains :attr:`x` to be within the inclusive range + [:attr:`out_min`, :attr:`out_max`]. Sometimes called :attr:`clip` or + :attr:`clamp` in other libraries. :attr:`out_min` should be less than or + equal to :attr:`out_max`. + If :attr:`x` is less than :attr:`out_min`, return :attr:`out_min`. + If :attr:`x` is greater than :attr:`out_max`, return :attr:`out_max`. + Otherwise just return :attr:`x`. + If :attr:`max_value` is less than :attr:`min_value`, they will be swapped. + :param float x: Value to constrain :param float out_min: Lower bound of output range. :param float out_max: Upper bound of output range. :return: Returns value constrained to given range. :rtype: float """ - return max(out_min, min(x, out_max)) + if out_min <= out_max: + return max(min(x, out_max), out_min) + return min(max(x, out_max), out_min) diff --git a/examples/simplemath_simpletest.py b/examples/simplemath_simpletest.py index e73f6a6..de14f9e 100644 --- a/examples/simplemath_simpletest.py +++ b/examples/simplemath_simpletest.py @@ -1,13 +1,54 @@ -# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries +# SPDX-FileCopyrightText: 2021 Dan Halbert for Adafruit Industries +# SPDX-FileCopyrightText: 2021 James Carr # # SPDX-License-Identifier: Unlicense -from adafruit_simplemath import map_range, constrain +from adafruit_simplemath import map_range, map_unconstrained_range, constrain +print("map_range() examples") # Map, say, a sensor value, from a range of 0-255 to 0-1023. -print(map_range(30, 0, 255, 0, 1023)) +sensor_input_value = 30 +sensor_converted_value = map_range(sensor_input_value, 0, 255, 0, 1023) +print( + "Sensor input value:", + sensor_input_value, + "Converted value:", + sensor_converted_value, +) +percent = 23 +screen_width = 320 # or board.DISPLAY.width +x = map_range(percent, 0, 100, 0, screen_width - 1) +print("X position", percent, "% from the left of screen is", x) + +print("\nmap_unconstrained_range() examples") +celsius = 20 +fahrenheit = map_unconstrained_range(celsius, 0, 100, 32, 212) +print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") + +celsius = -20 +fahrenheit = map_unconstrained_range(celsius, 0, 100, 32, 212) +print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") + +print("\nconstrain() examples") # Constrain a value to a range. -print(constrain(0, 1, 3)) # prints 1 -print(constrain(4, 1, 3)) # prints 3 -print(constrain(2, 2, 3)) # prints 2 +def constrain_example(value, min_value, max_value): + constrained_value = constrain(value, min_value, max_value) + print( + "Constrain", + value, + "between [", + min_value, + "and", + max_value, + "] gives", + constrained_value, + ) + + +constrain_example(0, 1, 3) # expects 1 +constrain_example(0, 3, 1) # expects 1 +constrain_example(4, 1, 3) # expects 3 +constrain_example(4, 3, 1) # expects 3 +constrain_example(2, 2, 3) # expects 2 +constrain_example(2, 3, 2) # expects 2 diff --git a/tests/constrain_test.py b/tests/constrain_test.py index e52808d..3a935dd 100644 --- a/tests/constrain_test.py +++ b/tests/constrain_test.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2021 Dan Halbert for Adafruit Industries +# SPDX-FileCopyrightText: 2021 James Carr # # SPDX-License-Identifier: Unlicense @@ -10,3 +11,8 @@ def test_constrain(): assert constrain(10, 1, 10) == 10 assert constrain(0, 1, 10) == 1 assert constrain(11, 1, 10) == 10 + + # Check out_min > out_max + assert constrain(5, 10, 0) == 5 + assert constrain(-5, 10, 0) == 0 + assert constrain(15, 10, 0) == 10 diff --git a/tests/map_range_test.py b/tests/map_range_test.py index fee5025..3faacf7 100644 --- a/tests/map_range_test.py +++ b/tests/map_range_test.py @@ -13,3 +13,6 @@ def test_map_range(): assert map_range(1, 10, 0, 0, 5) == 4.5 assert map_range(1, 0, 10, 10, 0) == 9.0 assert map_range(10, 1, 10, 1, 20) == 20.0 + # Tests for out-of-range descending output order + assert map_range(11, 1, 10, 20, 1) == 1.0 + assert map_range(-1, 1, 10, 20, 1) == 20.0 diff --git a/tests/map_unconstrained_range_test.py b/tests/map_unconstrained_range_test.py new file mode 100644 index 0000000..5d21837 --- /dev/null +++ b/tests/map_unconstrained_range_test.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2021 James Carr +# +# SPDX-License-Identifier: Unlicense + +from adafruit_simplemath import map_unconstrained_range + + +def test_map_unconstrained_range(): + assert map_unconstrained_range(-40, 32, 212, 0, 100) == -40.0 + assert map_unconstrained_range(50, 32, 212, 0, 100) == 10.0 + assert map_unconstrained_range(392, 32, 212, 0, 100) == 200.0