Repository: dalito/ucumvert

Issue:
Bad conversion : milli international unit per liter
Hi, I think there is a problem when converting a IU with a prefix to a Pint unit:

Input : `m[IU]/L` (from the UCUM examples list, #403)
- Expected output : `milliinternational_unit / liter`
- Actual output : `meter * international_unit / liter`

Note that `k[IU]/L` is also not converted correctly to Pint.

---------

I would be glad to help, but I'm not sure where to start since the Pint documentation says regarding prefixes that 

> [...] It is important to note that prefixed defined in this way can be used with any unit, including non-metric ones (e.g. kiloinch is valid for Pint)

So I'm not sure how `mX` is not interpreted as `milliX` 🤔

Discussion:
@dalito: Thanks for reporting.

This is fixed in #25. It looks like it was caused by an issue in pint for custom units. pint does not recognize the prefix for aliased custom units if the prefix itself could be a valid unit (like "m", "k", "M" etc.) - I tested with pint 0.23.0 and the current 0.24.3.

If you use the interactive mode (`ucumvert -i`), you see how the parser interprets the input. For `mX` the parser rejects the whole string because it does not contain a valid unit after `m`.

Solution patch:
diff --git a/src/ucumvert/ucum_pint.py b/src/ucumvert/ucum_pint.py
index 7d1efc2..b6d31ef 100644
--- a/src/ucumvert/ucum_pint.py
+++ b/src/ucumvert/ucum_pint.py
@@ -152,7 +152,12 @@ def component(self, args):
     def simple_unit(self, args):
         # print("DBGsu>", repr(args), len(args))
         if len(args) == 2:  # prefix is present  # noqa: PLR2004
-            return self.ureg(args[0] + args[1])
+            # Work around a pint bug: parsing of abbreviated custom unit with prefix
+            #   that could be a unit (k,m,M) does not detect the prefix but 2 units.
+            #   e.g. m[IU] --> <Quantity(1, 'meter * [IU]')> instead of <Quantity(1, 'milli[IU]')>
+            # Therefore, this fails (pint 0.23):
+            #   return self.ureg(args[0] + args[1])
+            return self.ureg(args[0] + str(self.ureg(args[1]).units))
 
         # Substitute UCUM atoms that cannot be defined in pint as units or aliases.
         return self.ureg(MAPPINGS_UCUM_TO_PINT.get(args[0], args[0]))


Test patch:
diff --git a/tests/test_ucum_pint.py b/tests/test_ucum_pint.py
index 3227373..b5744a2 100644
--- a/tests/test_ucum_pint.py
+++ b/tests/test_ucum_pint.py
@@ -1,3 +1,5 @@
+from pathlib import Path
+
 import pytest
 from lark import LarkError
 from pint import UnitRegistry
@@ -12,8 +14,6 @@
 from ucumvert.ucum_pint import find_ucum_codes_that_need_mapping
 from ucumvert.xml_util import get_metric_units, get_non_metric_units
 
-ureg = UnitRegistry()
-
 
 def get_unit_atoms():
     """List of all case-sensitive defined UCUM units in ucum-essence.xml"""
@@ -28,7 +28,7 @@ def test_find_ucum_codes_that_need_mapping():
 
 
 def test_ucum_to_pint(ucum_parser, ureg_std):
-    expected_quantity = ureg("kilogram")
+    expected_quantity = ureg_std("kilogram")
     parsed_data = ucum_parser.parse("kg")
     result = UcumToPintTransformer(ureg=ureg_std).transform(parsed_data)
     assert result == expected_quantity
@@ -99,7 +99,11 @@ def test_ucum_all_unit_atoms_pint_vs_str(
     assert ureg_ucumvert(result_str) == expected_quantity
 
 
-def test_ucum_preprocessor(ureg_ucumvert):
+def test_ucum_preprocessor():
+    # Don't use ureg_ucumvert from fixture here, because we want to modify it.
+    defdir = Path(__file__).resolve().parents[1] / "src" / "ucumvert"
+    ureg_ucumvert = UnitRegistry()
+    ureg_ucumvert.load_definitions(defdir / "pint_ucum_defs.txt")
     expected = ureg_ucumvert("m*kg")
     ureg_ucumvert.preprocessors.append(ucum_preprocessor)
     assert ureg_ucumvert("m.kg") == expected
@@ -111,3 +115,14 @@ def test_ucum_unitregistry():
     ureg = PintUcumRegistry()
     assert ureg.from_ucum("m.kg") == ureg("m*kg")
     assert ureg.from_ucum("Cel") == ureg("degC")
+
+
+def test_prefix_with_unit_that_is_also_a_prefix_issue24(ucum_parser, ureg_ucumvert):
+    parsed_data = ucum_parser.parse("m[IU]/L")
+
+    expected_quantity = ureg_ucumvert("milliinternational_unit/liter")
+    assert expected_quantity == UcumToPintTransformer().transform(parsed_data)
+    assert expected_quantity == 10**-3 * ureg_ucumvert("[IU]/L")
+
+    result_str = UcumToPintStrTransformer().transform(parsed_data)
+    assert result_str == "(((m[IU]) / (L)))"