mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-18 13:05:42 +00:00
[Feature] Engineering Units (#6539)
* Conversion: Support conversion from "engineering notation" * Add unit tests for scientific notation * Update docs for unit conversion
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
"""Helper functions for converting between units."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -9,7 +10,6 @@ import pint
|
||||
|
||||
_unit_registry = None
|
||||
|
||||
|
||||
logger = logging.getLogger('inventree')
|
||||
|
||||
|
||||
@ -70,6 +70,64 @@ def reload_unit_registry():
|
||||
return reg
|
||||
|
||||
|
||||
def from_engineering_notation(value):
|
||||
"""Convert a provided value to 'natural' representation from 'engineering' notation.
|
||||
|
||||
Ref: https://en.wikipedia.org/wiki/Engineering_notation
|
||||
|
||||
In "engineering notation", the unit (or SI prefix) is often combined with the value,
|
||||
and replaces the decimal point.
|
||||
|
||||
Examples:
|
||||
- 1K2 -> 1.2K
|
||||
- 3n05 -> 3.05n
|
||||
- 8R6 -> 8.6R
|
||||
|
||||
And, we should also take into account any provided trailing strings:
|
||||
|
||||
- 1K2 ohm -> 1.2K ohm
|
||||
- 10n005F -> 10.005nF
|
||||
"""
|
||||
value = str(value).strip()
|
||||
|
||||
pattern = f'(\d+)([a-zA-Z]+)(\d+)(.*)'
|
||||
|
||||
if match := re.match(pattern, value):
|
||||
left, prefix, right, suffix = match.groups()
|
||||
return f'{left}.{right}{prefix}{suffix}'
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def convert_value(value, unit):
|
||||
"""Attempt to convert a value to a specified unit.
|
||||
|
||||
Arguments:
|
||||
value: The value to convert
|
||||
unit: The target unit to convert to
|
||||
|
||||
Returns:
|
||||
The converted value (ideally a pint.Quantity value)
|
||||
|
||||
Raises:
|
||||
Exception if the value cannot be converted to the specified unit
|
||||
"""
|
||||
ureg = get_unit_registry()
|
||||
|
||||
# Convert the provided value to a pint.Quantity object
|
||||
value = ureg.Quantity(value)
|
||||
|
||||
# Convert to the specified unit
|
||||
if unit:
|
||||
if is_dimensionless(value):
|
||||
magnitude = value.to_base_units().magnitude
|
||||
value = ureg.Quantity(magnitude, unit)
|
||||
else:
|
||||
value = value.to(unit)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
"""Validate that the provided value is a valid physical quantity.
|
||||
|
||||
@ -94,34 +152,29 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
if not value:
|
||||
raise ValidationError(_('No value provided'))
|
||||
|
||||
# Create a "backup" value which be tried if the first value fails
|
||||
# e.g. value = "10k" and unit = "ohm" -> "10kohm"
|
||||
# e.g. value = "10m" and unit = "F" -> "10mF"
|
||||
# Construct a list of values to "attempt" to convert
|
||||
attempts = [value]
|
||||
|
||||
# Attempt to convert from engineering notation
|
||||
eng = from_engineering_notation(value)
|
||||
attempts.append(eng)
|
||||
|
||||
# Append the unit, if provided
|
||||
# These are the "final" attempts to convert the value, and *must* appear after previous attempts
|
||||
if unit:
|
||||
backup_value = value + unit
|
||||
else:
|
||||
backup_value = None
|
||||
attempts.append(f'{value}{unit}')
|
||||
attempts.append(f'{eng}{unit}')
|
||||
|
||||
ureg = get_unit_registry()
|
||||
value = None
|
||||
|
||||
try:
|
||||
value = ureg.Quantity(value)
|
||||
|
||||
if unit:
|
||||
if is_dimensionless(value):
|
||||
magnitude = value.to_base_units().magnitude
|
||||
value = ureg.Quantity(magnitude, unit)
|
||||
else:
|
||||
value = value.to(unit)
|
||||
|
||||
except Exception:
|
||||
if backup_value:
|
||||
try:
|
||||
value = ureg.Quantity(backup_value)
|
||||
except Exception:
|
||||
value = None
|
||||
else:
|
||||
# Run through the available "attempts", take the first successful result
|
||||
for attempt in attempts:
|
||||
try:
|
||||
value = convert_value(attempt, unit)
|
||||
break
|
||||
except Exception as exc:
|
||||
value = None
|
||||
pass
|
||||
|
||||
if value is None:
|
||||
if unit:
|
||||
@ -129,6 +182,8 @@ def convert_physical_value(value: str, unit: str = None, strip_units=True):
|
||||
else:
|
||||
raise ValidationError(_('Invalid quantity supplied'))
|
||||
|
||||
ureg = get_unit_registry()
|
||||
|
||||
# Calculate the "magnitude" of the value, as a float
|
||||
# If the value is specified strangely (e.g. as a fraction or a dozen), this can cause issues
|
||||
# So, we ensure that it is converted to a floating point value
|
||||
|
@ -58,6 +58,43 @@ class ConversionTest(TestCase):
|
||||
q = InvenTree.conversion.convert_physical_value(val, 'm')
|
||||
self.assertAlmostEqual(q, expected, 3)
|
||||
|
||||
def test_engineering_units(self):
|
||||
"""Test that conversion works with engineering notation."""
|
||||
# Run some basic checks over the helper function
|
||||
tests = [
|
||||
('3', '3'),
|
||||
('3k3', '3.3k'),
|
||||
('123R45', '123.45R'),
|
||||
('10n5F', '10.5nF'),
|
||||
]
|
||||
|
||||
for val, expected in tests:
|
||||
self.assertEqual(
|
||||
InvenTree.conversion.from_engineering_notation(val), expected
|
||||
)
|
||||
|
||||
# Now test the conversion function
|
||||
tests = [('33k3ohm', 33300), ('123kohm45', 123450), ('10n005', 0.000000010005)]
|
||||
|
||||
for val, expected in tests:
|
||||
output = InvenTree.conversion.convert_physical_value(
|
||||
val, 'ohm', strip_units=True
|
||||
)
|
||||
self.assertAlmostEqual(output, expected, 12)
|
||||
|
||||
def test_scientific_notation(self):
|
||||
"""Test that scientific notation is handled correctly."""
|
||||
tests = [
|
||||
('3E2', 300),
|
||||
('-12.3E-3', -0.0123),
|
||||
('1.23E-3', 0.00123),
|
||||
('99E9', 99000000000),
|
||||
]
|
||||
|
||||
for val, expected in tests:
|
||||
output = InvenTree.conversion.convert_physical_value(val, strip_units=True)
|
||||
self.assertAlmostEqual(output, expected, 6)
|
||||
|
||||
def test_base_units(self):
|
||||
"""Test conversion to specified base units."""
|
||||
tests = {
|
||||
|
Reference in New Issue
Block a user