From b25e0ff6836d25f16079bcc7d485365beb6f9146 Mon Sep 17 00:00:00 2001 From: John Welsh Date: Fri, 24 Jun 2022 15:54:24 +1000 Subject: [PATCH 1/6] Additions/Fixes for unit conversion/scaling Fixed possible fall-throughs in unit_convert(), but hardcoded range. Fixed regex in unit_scale to detect negative numbers, allow for extra spaces and allow case-insensitive unit matching. Added to unit and unit scaling sets. --- src/fixate/core/common.py | 41 +++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/fixate/core/common.py b/src/fixate/core/common.py index 1e31bdc2..45fd5d1b 100644 --- a/src/fixate/core/common.py +++ b/src/fixate/core/common.py @@ -21,17 +21,23 @@ "%", "Hertz", "Volts", + "Amps", + "A", "Percent", "PC", - "deg", "Deg", "C", "H", + "F", + "Ohm", + "Ohms", } UNIT_SCALE = { - "m": 10**-3, - "u": 10**-6, + "p": 10**-12, "n": 10**-9, + "u": 10**-6, + "m": 10**-3, + "": 1, "k": 10**3, "M": 10**6, "G": 10**9, @@ -134,7 +140,7 @@ def mode_builder(search_dict, repl_kwargs, *args, **kwargs): return ret_string -def unit_convert(value, min_primary_number, max_primary_number, as_int=False): +def unit_convert(value:int or float, min_primary_number:int or float, max_primary_number=None, as_int=False) -> str: """ :param value: An int or float to convert into a scaled unit @@ -151,21 +157,36 @@ def unit_convert(value, min_primary_number, max_primary_number, as_int=False): >>>unit_convert(100e6, 1, 999, as_int=True) '100M' """ + # Previous implementation had lots of holes: + # i.e. unit_convert(99.9e6, 0.1, 99) would fall through + # NOTE: since we are using eng.notation - hardcode range as 1e3 + max_primary_number = min_primary_number*1e3 + # TODO: should we enforce min_primary_number in (1e-3, 1), + # otherwise can get some odd display issues + for unit, scale in UNIT_SCALE.items(): - if min_primary_number * scale <= value <= max_primary_number * scale: + if min_primary_number * scale <= value < max_primary_number * scale: new_val = value / scale if as_int: new_val = int(new_val) - return "{}{}".format(new_val, unit) + return f"{new_val:.3g}{unit}" + + # Should only get here now if there doesn't exist appropriate UNIT_SCALE entry? + # Best to return the entry rather than throw exception? + return f"{value}" -def unit_scale(str_value, accepted_units=UNITS): + +def unit_scale(str_value, accepted_units=UNITS) -> int or float: """ :param str_value: A Value to search for a number and the acceptable units to then scale the original number :param accepted_units: Restricts the units to this sequence or if not parsed will use defaults specified in the UNITS set :return: + Value of input with embedded unit converted to respectivescaling + :raises: + InvalidScalarQuantityError - unable to process input string """ # If type is a number, no scaling required if type(str_value) in [int, float]: @@ -182,12 +203,12 @@ def unit_scale(str_value, accepted_units=UNITS): ) ) # Match Decimal and Integer Values - p = re.compile("\d+(\.\d+)?") + p = re.compile("-?\d+(\.\d+)?") num_match = p.search(str_value) if num_match: num = float(num_match.group()) - comp = "^ ?({unit_scale})(?=($|{units}))".format( + comp = "^ *({unit_scale}) ?(?i)(?=($|{units}))".format( units="|".join(accepted_units), unit_scale="|".join(UNIT_SCALE.keys()) ) p = re.compile(comp) @@ -453,4 +474,4 @@ def test(self): This method should be overridden with the test code This is the test sequence code Use chk functions to set the pass fail criteria for the test - """ + """ \ No newline at end of file From 42be8632fbae261e17436b0165eb364f35d53ca0 Mon Sep 17 00:00:00 2001 From: John Welsh Date: Mon, 4 Jul 2022 14:53:27 +1000 Subject: [PATCH 2/6] Update common.py Add support for negative values in unit_convert(). remove unused imports --- src/fixate/core/common.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/fixate/core/common.py b/src/fixate/core/common.py index 45fd5d1b..e355e45b 100644 --- a/src/fixate/core/common.py +++ b/src/fixate/core/common.py @@ -1,8 +1,6 @@ import re import sys import threading -import inspect -import ctypes import logging import warnings from functools import wraps @@ -162,10 +160,10 @@ def unit_convert(value:int or float, min_primary_number:int or float, max_primar # NOTE: since we are using eng.notation - hardcode range as 1e3 max_primary_number = min_primary_number*1e3 # TODO: should we enforce min_primary_number in (1e-3, 1), - # otherwise can get some odd display issues + # otherwise can get some odd display issues for unit, scale in UNIT_SCALE.items(): - if min_primary_number * scale <= value < max_primary_number * scale: + if min_primary_number * scale <= abs(value) < max_primary_number * scale: new_val = value / scale if as_int: new_val = int(new_val) From 66b6871fd6fe5729e92f8df5e530e99ced816fbc Mon Sep 17 00:00:00 2001 From: John Welsh Date: Mon, 4 Jul 2022 14:56:08 +1000 Subject: [PATCH 3/6] Update test_lib_common_unitscale.py Increase test coverage for unit_scale and unit_convert --- test/core/test_lib_common_unitscale.py | 79 +++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/test/core/test_lib_common_unitscale.py b/test/core/test_lib_common_unitscale.py index 09d0eed9..7b279933 100644 --- a/test/core/test_lib_common_unitscale.py +++ b/test/core/test_lib_common_unitscale.py @@ -1,5 +1,5 @@ import unittest -from fixate.core.common import unit_scale +from fixate.core.common import unit_scale, unit_convert, UNITS from fixate.core.exceptions import InvalidScalarQuantityError @@ -19,12 +19,18 @@ def test_micro_scale(self): def test_nano_scale(self): self.assertAlmostEqual(unit_scale("10nV", ["V"]), 10e-9) + def test_pico_scale(self): + self.assertAlmostEqual(unit_scale("10pV", ["V"]), 10e-12) + def test_giga_scale(self): self.assertAlmostEqual(unit_scale("10GV", ["V"]), 10e9) def test_no_scale(self): self.assertEqual(unit_scale("10V", ["V"]), 10) + def test_negative_value(self): + self.assertAlmostEqual(unit_scale("-12mV", UNITS), -12e-3) + def test_milli_scale_no_units(self): self.assertAlmostEqual(unit_scale("10m", ["V"]), 10e-3) @@ -40,12 +46,21 @@ def test_micro_scale_no_units(self): def test_nano_scale_no_units(self): self.assertAlmostEqual(unit_scale("10n", ["V"]), 10e-9) + def test_pico_scale_no_units(self): + self.assertAlmostEqual(unit_scale("10p", ["V"]), 10e-12) + def test_giga_scale_no_units(self): self.assertAlmostEqual(unit_scale("10G", ["V"]), 10e9) def test_no_scale_no_units(self): self.assertEqual(unit_scale("10", ["V"]), 10e0) + def test_negative_no_scale(self): + self.assertAlmostEqual(unit_scale("-10V", UNITS), -10) + + def test_negative_no_units(self): + self.assertAlmostEqual(unit_scale("-10m", UNITS), -10e-3) + def test_number_invalid_suffix(self): with self.assertRaises(InvalidScalarQuantityError): self.assertEqual(unit_scale("10 abcd", ["V"]), 10e0) @@ -63,3 +78,65 @@ def test_invalid_string(self): def test_none(self): self.assertEqual(unit_scale(None), None) + + +class TestUnitConvert(unittest.TestCase): + """Use of unit_convert: unit_convert(100e6, 1, 999)""" + + def test_no_scale(self): + self.assertEqual(unit_convert(10.1, 1, 999), "10.1") + + def test_no_scale_int(self): + self.assertEqual(unit_convert(10.9, 1, 999, as_int=True), "10") + + def test_round_down(self): + self.assertEqual(unit_convert(10.83, 1, 999), "10.8") + + # NOTE: unpredictable behaviour with rounding of floats, i.e.: + # unit_convert(10.95, 1) = '10.9' + # unit_convert(10.05, 1) = '10.1' + + def test_milli_scale(self): + self.assertEqual(unit_convert(10e-3, 1, 999), "10m") + + def test_mega_scale(self): + self.assertEqual(unit_convert(10e6, 1, 999), "10M") + + def test_kilo_scale(self): + self.assertEqual(unit_convert(10e3, 1, 999), "10k") + + def test_micro_scale(self): + self.assertEqual(unit_convert(10e-6, 1, 999), "10u") + + def test_nano_scale(self): + self.assertEqual(unit_convert(10e-9, 1, 999), "10n") + + def test_pico_scale(self): + self.assertEqual(unit_convert(10e-12, 1, 999), "10p") + + def test_giga_scale(self): + self.assertEqual(unit_convert(10e9, 1, 999), "10G") + + def test_smaller_base(self): + self.assertEqual(unit_convert(100e-6, 0.1, 99), "0.1m") + + def test_smaller_base2(self): + self.assertEqual(unit_convert(19.8e-6, 0.01, 99), "0.0198m") + + def test_smaller_base3(self): + self.assertEqual(unit_convert(9.8e-6, 0.001, 9), "0.0098m") + + def test_negative_value(self): + self.assertEqual(unit_convert(-10e-3, 1, 999), "-10m") + + def test_out_of_range(self): + """Change in future if exception raised instead""" + self.assertEqual(unit_convert(10e12, 1, 999), "10000000000000.0") + + def test_none(self): + with self.assertRaises(TypeError): + unit_convert(None, None) + + def test_string_input_invalid(self): + with self.assertRaises(TypeError): + unit_convert("10", 1, 99) From 6a1863d82c1017d007ad6a4245c0cb892aa9018b Mon Sep 17 00:00:00 2001 From: John Welsh Date: Mon, 4 Jul 2022 17:38:50 +1000 Subject: [PATCH 4/6] add unit_scale tests Add more test coverage for unit_scale() --- src/fixate/core/common.py | 2 +- test/core/test_lib_common_unitscale.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/fixate/core/common.py b/src/fixate/core/common.py index e355e45b..83493f43 100644 --- a/src/fixate/core/common.py +++ b/src/fixate/core/common.py @@ -170,7 +170,7 @@ def unit_convert(value:int or float, min_primary_number:int or float, max_primar return f"{new_val:.3g}{unit}" # Should only get here now if there doesn't exist appropriate UNIT_SCALE entry? - # Best to return the entry rather than throw exception? + # Best to return the entry rather than throw exception or create warning? return f"{value}" diff --git a/test/core/test_lib_common_unitscale.py b/test/core/test_lib_common_unitscale.py index 7b279933..54db298d 100644 --- a/test/core/test_lib_common_unitscale.py +++ b/test/core/test_lib_common_unitscale.py @@ -61,6 +61,18 @@ def test_negative_no_scale(self): def test_negative_no_units(self): self.assertAlmostEqual(unit_scale("-10m", UNITS), -10e-3) + def test_space_before_units(self): + self.assertAlmostEqual(unit_scale("-10 mHz", UNITS), -10e-3) + + def test_multiple_spaces_before_units(self): + self.assertAlmostEqual(unit_scale("-10 mV", UNITS), -10e-3) + + def test_space_between_units(self): + self.assertAlmostEqual(unit_scale("-10 k V", UNITS), -10e3) + + def test_leading_spaces(self): + self.assertAlmostEqual(unit_scale(" -10 mV ", UNITS), -10e-3) + def test_number_invalid_suffix(self): with self.assertRaises(InvalidScalarQuantityError): self.assertEqual(unit_scale("10 abcd", ["V"]), 10e0) From 00488e7796830422acec06d1ed520037cb2e954f Mon Sep 17 00:00:00 2001 From: John Welsh Date: Mon, 11 Jul 2022 15:30:41 +1000 Subject: [PATCH 5/6] Update common.py black formatting --- src/fixate/core/common.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/fixate/core/common.py b/src/fixate/core/common.py index 83493f43..44f31b96 100644 --- a/src/fixate/core/common.py +++ b/src/fixate/core/common.py @@ -138,7 +138,12 @@ def mode_builder(search_dict, repl_kwargs, *args, **kwargs): return ret_string -def unit_convert(value:int or float, min_primary_number:int or float, max_primary_number=None, as_int=False) -> str: +def unit_convert( + value: int or float, + min_primary_number: int or float, + max_primary_number=None, + as_int=False, +) -> str: """ :param value: An int or float to convert into a scaled unit @@ -158,8 +163,8 @@ def unit_convert(value:int or float, min_primary_number:int or float, max_primar # Previous implementation had lots of holes: # i.e. unit_convert(99.9e6, 0.1, 99) would fall through # NOTE: since we are using eng.notation - hardcode range as 1e3 - max_primary_number = min_primary_number*1e3 - # TODO: should we enforce min_primary_number in (1e-3, 1), + max_primary_number = min_primary_number * 1e3 + # TODO: should we enforce min_primary_number in (1e-3, 1) # otherwise can get some odd display issues for unit, scale in UNIT_SCALE.items(): @@ -174,7 +179,6 @@ def unit_convert(value:int or float, min_primary_number:int or float, max_primar return f"{value}" - def unit_scale(str_value, accepted_units=UNITS) -> int or float: """ :param str_value: @@ -472,4 +476,4 @@ def test(self): This method should be overridden with the test code This is the test sequence code Use chk functions to set the pass fail criteria for the test - """ \ No newline at end of file + """ From 89a239bbcdf6bde78ea5c134223636062ff37bcd Mon Sep 17 00:00:00 2001 From: John Welsh Date: Mon, 11 Jul 2022 15:59:44 +1000 Subject: [PATCH 6/6] Update common.py Add comments and log errors in unit_convert() --- src/fixate/core/common.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/fixate/core/common.py b/src/fixate/core/common.py index 44f31b96..aa3a06c8 100644 --- a/src/fixate/core/common.py +++ b/src/fixate/core/common.py @@ -173,9 +173,12 @@ def unit_convert( if as_int: new_val = int(new_val) return f"{new_val:.3g}{unit}" + # TODO - thinking I should change this to .6g? + # Just need to prevent displaying whole float + logger.error("Could not convert to units: %f", value) # Should only get here now if there doesn't exist appropriate UNIT_SCALE entry? - # Best to return the entry rather than throw exception or create warning? + # Best to return the entry rather than throw exception? return f"{value}"