Skip to content
Draft
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
48 changes: 37 additions & 11 deletions src/fixate/core/common.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import re
import sys
import threading
import inspect
import ctypes
import logging
import warnings
from functools import wraps
Expand All @@ -21,17 +19,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,
Expand Down Expand Up @@ -134,7 +138,12 @@ 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
Expand All @@ -151,21 +160,38 @@ 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 <= abs(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}"
# 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?
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]:
Expand All @@ -182,12 +208,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)
Expand Down
91 changes: 90 additions & 1 deletion test/core/test_lib_common_unitscale.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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)

Expand All @@ -40,12 +46,33 @@ 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_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)
Expand All @@ -63,3 +90,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)