From edf90d6ef36f17af7d6f78770b8aed661f4b91bd Mon Sep 17 00:00:00 2001 From: James Carr Date: Tue, 13 Apr 2021 09:39:46 +0100 Subject: [PATCH 1/6] Add a constrained parameter to map_range(), defaulting to True. This allows for the conversion of unconstrained ranges, eg. Celsius to Fahrenheit. --- adafruit_simplemath.py | 56 ++++++++++++++++++++++++------- examples/simplemath_simpletest.py | 44 +++++++++++++++++++++--- 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/adafruit_simplemath.py b/adafruit_simplemath.py index 10326f0..94b39f7 100644 --- a/adafruit_simplemath.py +++ b/adafruit_simplemath.py @@ -24,22 +24,49 @@ def map_range( - x: float, in_min: float, in_max: float, out_min: float, out_max: float + x: float, + in_min: float, + in_max: float, + out_min: float, + out_max: float, + constrained: bool = True, ) -> 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 + optionally 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. + .. 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) + + celsius = 20 + fahrenheit = map_range(celsius, 0, 100, 32, 212, constrained=False) + print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") + + celsius = -20 + fahrenheit = map_range(celsius, 0, 100, 32, 212, False) + 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. :param float out_max: End value of output range. + :param bool constrained: Whether the output value should be constrained + between :attr:`out_min` and :attr:`out_max`. Defaults to `True`. :return: Returns value mapped to new range. :rtype: float """ + # pylint: disable=too-many-arguments in_range = in_max - in_min in_delta = x - in_min if in_range != 0: @@ -50,19 +77,24 @@ def map_range( mapped = 0.5 mapped *= out_max - out_min mapped += out_min + + if not constrained: + return mapped if out_min <= out_max: return max(min(mapped, out_max), out_min) return min(max(mapped, out_max), out_min) 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`. + :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. diff --git a/examples/simplemath_simpletest.py b/examples/simplemath_simpletest.py index e73f6a6..3b3a0ef 100644 --- a/examples/simplemath_simpletest.py +++ b/examples/simplemath_simpletest.py @@ -4,10 +4,46 @@ from adafruit_simplemath import map_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) + +celsius = 20 +fahrenheit = map_range(celsius, 0, 100, 32, 212, constrained=False) +print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") + +celsius = -20 +fahrenheit = map_range(celsius, 0, 100, 32, 212, False) +print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") + +print("constrain() 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(4, 1, 3) # expects 3 +constrain_example(2, 2, 3) # expects 2 From dbc4877304c2f37dff6cf3558f3014351972acf5 Mon Sep 17 00:00:00 2001 From: James Carr Date: Tue, 13 Apr 2021 09:56:34 +0100 Subject: [PATCH 2/6] Add tests that make use of constrained in map_range --- tests/map_range_test.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/map_range_test.py b/tests/map_range_test.py index fee5025..0bf9281 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 + assert map_range(392, 32, 212, 0, 100) == 100.0 + assert map_range(392, 32, 212, 0, 100, constrained=True) == 100.0 + assert map_range(392, 32, 212, 0, 100, constrained=False) == 200.0 From f4142be3ec60aec77f7163ad2867501e48e821b2 Mon Sep 17 00:00:00 2001 From: James Carr Date: Tue, 13 Apr 2021 21:31:38 +0100 Subject: [PATCH 3/6] * Auto-swap the limits passed to constrain if necessary (Closes #3) * Split map_range into map_range and unconstrained_map_range. * map_range will constrain the output value within the provided limits * unconstrained_map_range will only use the limits to define the linear equation * Add tests for constrain with auto-swap * Add tests for unconstrained_map_range --- adafruit_simplemath.py | 75 +++++++++++++++++++-------- examples/simplemath_simpletest.py | 15 ++++-- tests/constrain_test.py | 6 +++ tests/map_range_test.py | 3 -- tests/unconstrained_map_range_test.py | 11 ++++ 5 files changed, 81 insertions(+), 29 deletions(-) create mode 100644 tests/unconstrained_map_range_test.py diff --git a/adafruit_simplemath.py b/adafruit_simplemath.py index 94b39f7..e5edab0 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 -------------------- @@ -24,21 +25,18 @@ def map_range( - x: float, - in_min: float, - in_max: float, - out_min: float, - out_max: float, - constrained: bool = True, + 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 - optionally constrains the output value to be between :attr:`out_min` 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:`unconstrained_map_range` + .. code-block:: from adafruit_simplemath import map_range @@ -48,12 +46,51 @@ def map_range( x = map_range(percent, 0, 100, 0, screen_width - 1) print("X position", percent, "% from the left of screen is", x) - celsius = 20 - fahrenheit = map_range(celsius, 0, 100, 32, 212, constrained=False) - 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. + :param float out_max: End value of output range. + :return: Returns value mapped to new range. + :rtype: float + """ + # in_range = in_max - in_min + # in_delta = x - in_min + # if in_range != 0: + # mapped = in_delta / in_range + # elif in_delta != 0: + # mapped = in_delta + # else: + # mapped = 0.5 + # mapped *= out_max - out_min + # mapped += out_min + + mapped = unconstrained_map_range(x, in_min, in_max, out_min, out_max) + + if out_min <= out_max: + return max(min(mapped, out_max), out_min) + return min(max(mapped, out_max), out_min) + + +def unconstrained_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 + :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 unconstrained_map_range celsius = -20 - fahrenheit = map_range(celsius, 0, 100, 32, 212, False) + fahrenheit = unconstrained_map_range(celsius, 0, 100, 32, 212) print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") :param float x: Value to convert @@ -61,12 +98,9 @@ def map_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. - :param bool constrained: Whether the output value should be constrained - between :attr:`out_min` and :attr:`out_max`. Defaults to `True`. :return: Returns value mapped to new range. :rtype: float """ - # pylint: disable=too-many-arguments in_range = in_max - in_min in_delta = x - in_min if in_range != 0: @@ -78,11 +112,7 @@ def map_range( mapped *= out_max - out_min mapped += out_min - if not constrained: - return mapped - 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: @@ -93,6 +123,7 @@ def constrain(x: float, out_min: float, out_max: float) -> float: 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. @@ -100,4 +131,6 @@ def constrain(x: float, out_min: float, out_max: float) -> float: :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 3b3a0ef..3e0721e 100644 --- a/examples/simplemath_simpletest.py +++ b/examples/simplemath_simpletest.py @@ -1,8 +1,9 @@ -# 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, unconstrained_map_range, constrain print("map_range() examples") # Map, say, a sensor value, from a range of 0-255 to 0-1023. @@ -20,15 +21,16 @@ x = map_range(percent, 0, 100, 0, screen_width - 1) print("X position", percent, "% from the left of screen is", x) +print("\nunconstrained_map_range() examples") celsius = 20 -fahrenheit = map_range(celsius, 0, 100, 32, 212, constrained=False) +fahrenheit = unconstrained_map_range(celsius, 0, 100, 32, 212) print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") celsius = -20 -fahrenheit = map_range(celsius, 0, 100, 32, 212, False) +fahrenheit = unconstrained_map_range(celsius, 0, 100, 32, 212) print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") -print("constrain() examples") +print("\nconstrain() examples") # Constrain a value to a range. def constrain_example(value, min_value, max_value): constrained_value = constrain(value, min_value, max_value) @@ -45,5 +47,8 @@ def constrain_example(value, min_value, max_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 0bf9281..fee5025 100644 --- a/tests/map_range_test.py +++ b/tests/map_range_test.py @@ -13,6 +13,3 @@ 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 - assert map_range(392, 32, 212, 0, 100) == 100.0 - assert map_range(392, 32, 212, 0, 100, constrained=True) == 100.0 - assert map_range(392, 32, 212, 0, 100, constrained=False) == 200.0 diff --git a/tests/unconstrained_map_range_test.py b/tests/unconstrained_map_range_test.py new file mode 100644 index 0000000..cfecb61 --- /dev/null +++ b/tests/unconstrained_map_range_test.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2021 James Carr +# +# SPDX-License-Identifier: Unlicense + +from adafruit_simplemath import unconstrained_map_range + + +def test_unconstrained_map_range(): + assert unconstrained_map_range(-40, 32, 212, 0, 100) == -40.0 + assert unconstrained_map_range(50, 32, 212, 0, 100) == 10.0 + assert unconstrained_map_range(392, 32, 212, 0, 100) == 200.0 From 92036d16aa68e1746e2b3886a93b58df8350f2e2 Mon Sep 17 00:00:00 2001 From: James Carr Date: Wed, 14 Apr 2021 20:06:28 +0100 Subject: [PATCH 4/6] Remove commented out code --- adafruit_simplemath.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/adafruit_simplemath.py b/adafruit_simplemath.py index e5edab0..72f42a4 100644 --- a/adafruit_simplemath.py +++ b/adafruit_simplemath.py @@ -54,16 +54,6 @@ def map_range( :return: Returns value mapped to new range. :rtype: float """ - # in_range = in_max - in_min - # in_delta = x - in_min - # if in_range != 0: - # mapped = in_delta / in_range - # elif in_delta != 0: - # mapped = in_delta - # else: - # mapped = 0.5 - # mapped *= out_max - out_min - # mapped += out_min mapped = unconstrained_map_range(x, in_min, in_max, out_min, out_max) From e5355180eab018a9b3eeca5e47c4982683172743 Mon Sep 17 00:00:00 2001 From: James Carr Date: Wed, 14 Apr 2021 20:14:38 +0100 Subject: [PATCH 5/6] Call constrain from map_range. Add tests for out-of-range reversed order map_range. --- adafruit_simplemath.py | 5 +---- tests/map_range_test.py | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/adafruit_simplemath.py b/adafruit_simplemath.py index 72f42a4..329ceb3 100644 --- a/adafruit_simplemath.py +++ b/adafruit_simplemath.py @@ -56,10 +56,7 @@ def map_range( """ mapped = unconstrained_map_range(x, in_min, in_max, out_min, out_max) - - if out_min <= out_max: - return max(min(mapped, out_max), out_min) - return min(max(mapped, out_max), out_min) + return constrain(mapped, out_min, out_max) def unconstrained_map_range( 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 From 0218b0f1d42889d26c664f3eadd84b9be6ee4e5f Mon Sep 17 00:00:00 2001 From: James Carr Date: Wed, 14 Apr 2021 21:07:39 +0100 Subject: [PATCH 6/6] Rename unconstrained_map_range to map_unconstrained_range --- adafruit_simplemath.py | 10 +++++----- examples/simplemath_simpletest.py | 8 ++++---- tests/map_unconstrained_range_test.py | 11 +++++++++++ tests/unconstrained_map_range_test.py | 11 ----------- 4 files changed, 20 insertions(+), 20 deletions(-) create mode 100644 tests/map_unconstrained_range_test.py delete mode 100644 tests/unconstrained_map_range_test.py diff --git a/adafruit_simplemath.py b/adafruit_simplemath.py index 329ceb3..4f2aac7 100644 --- a/adafruit_simplemath.py +++ b/adafruit_simplemath.py @@ -35,7 +35,7 @@ def map_range( :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:`unconstrained_map_range` + See also :py:func:`map_unconstrained_range` .. code-block:: @@ -55,11 +55,11 @@ def map_range( :rtype: float """ - mapped = unconstrained_map_range(x, in_min, in_max, out_min, out_max) + mapped = map_unconstrained_range(x, in_min, in_max, out_min, out_max) return constrain(mapped, out_min, out_max) -def unconstrained_map_range( +def map_unconstrained_range( x: float, in_min: float, in_max: float, out_min: float, out_max: float ) -> float: """ @@ -74,10 +74,10 @@ def unconstrained_map_range( .. code-block:: - from adafruit_simplemath import unconstrained_map_range + from adafruit_simplemath import map_unconstrained_range celsius = -20 - fahrenheit = unconstrained_map_range(celsius, 0, 100, 32, 212) + fahrenheit = map_unconstrained_range(celsius, 0, 100, 32, 212) print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") :param float x: Value to convert diff --git a/examples/simplemath_simpletest.py b/examples/simplemath_simpletest.py index 3e0721e..de14f9e 100644 --- a/examples/simplemath_simpletest.py +++ b/examples/simplemath_simpletest.py @@ -3,7 +3,7 @@ # # SPDX-License-Identifier: Unlicense -from adafruit_simplemath import map_range, unconstrained_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. @@ -21,13 +21,13 @@ x = map_range(percent, 0, 100, 0, screen_width - 1) print("X position", percent, "% from the left of screen is", x) -print("\nunconstrained_map_range() examples") +print("\nmap_unconstrained_range() examples") celsius = 20 -fahrenheit = unconstrained_map_range(celsius, 0, 100, 32, 212) +fahrenheit = map_unconstrained_range(celsius, 0, 100, 32, 212) print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") celsius = -20 -fahrenheit = unconstrained_map_range(celsius, 0, 100, 32, 212) +fahrenheit = map_unconstrained_range(celsius, 0, 100, 32, 212) print(celsius, "degress Celsius =", fahrenheit, "degrees Fahrenheit") print("\nconstrain() examples") 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 diff --git a/tests/unconstrained_map_range_test.py b/tests/unconstrained_map_range_test.py deleted file mode 100644 index cfecb61..0000000 --- a/tests/unconstrained_map_range_test.py +++ /dev/null @@ -1,11 +0,0 @@ -# SPDX-FileCopyrightText: 2021 James Carr -# -# SPDX-License-Identifier: Unlicense - -from adafruit_simplemath import unconstrained_map_range - - -def test_unconstrained_map_range(): - assert unconstrained_map_range(-40, 32, 212, 0, 100) == -40.0 - assert unconstrained_map_range(50, 32, 212, 0, 100) == 10.0 - assert unconstrained_map_range(392, 32, 212, 0, 100) == 200.0