From d4fdc1f0d341a869487d6d5101f3c34d05d161c8 Mon Sep 17 00:00:00 2001 From: hari Date: Sat, 22 Aug 2026 14:00:35 +0530 Subject: [PATCH] fix(normalize): accept unit aliases during conversion (#939) Convert_units() was validating categories on raw input like "kg" or "ft" instead of the normalized unit name, so aliases got checked against a category list that only has canonical names in it. Any alias-based conversion that should've worked just raised ValidationError instead. Fixed by normalizing both units before the category check runs. Also added foot/yard/mile/gallon to the alias map - they already had conversion factors but weren't mapped to their canonical names, so they'd still have failed even after the above fix. Turned out there was a second bug hiding behind the first one: the category check defaults both sides to None, and None == None is True, so two aliases from different categories that neither resolved to a real category would silently pass instead of raising. kg -> ft would just return a number instead of erroring. Normalizing first fixes this too, since aliases now resolve to their actual categories and the mismatch gets caught. Added a regression test locking that second one down - kg->ft and gal->lb now raise ValidationError instead of silently converting. Fixes #931. --- semantica/normalize/number_normalizer.py | 24 +++++++++++++++++++---- tests/normalize/test_number_normalizer.py | 15 ++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/semantica/normalize/number_normalizer.py b/semantica/normalize/number_normalizer.py index 29911cb9..0846465d 100644 --- a/semantica/normalize/number_normalizer.py +++ b/semantica/normalize/number_normalizer.py @@ -370,8 +370,12 @@ class UnitConverter: Raises: ValidationError: If units are incompatible or not in same category """ - from_unit = from_unit.lower() - to_unit = to_unit.lower() + # Normalize aliases before validating categories and looking up factors. + # The public API documents abbreviations such as ``kg`` and ``km``; + # validating those raw aliases against the canonical category lists + # incorrectly rejected otherwise supported conversions. + from_unit = self.normalize_unit(from_unit) + to_unit = self.normalize_unit(to_unit) # Validate units if not self.validate_units(from_unit, to_unit): @@ -401,8 +405,8 @@ class UnitConverter: Returns: bool: True if units are compatible (same category), False otherwise """ - from_unit = from_unit.lower() - to_unit = to_unit.lower() + from_unit = self.normalize_unit(from_unit) + to_unit = self.normalize_unit(to_unit) # Check if both units exist if ( @@ -498,6 +502,18 @@ class UnitConverter: "ml": "milliliter", "milliliter": "milliliter", "milliliters": "milliliter", + "ft": "foot", + "foot": "foot", + "feet": "foot", + "yd": "yard", + "yard": "yard", + "yards": "yard", + "mi": "mile", + "mile": "mile", + "miles": "mile", + "gal": "gallon", + "gallon": "gallon", + "gallons": "gallon", } return unit_map.get(unit_lower, unit_lower) diff --git a/tests/normalize/test_number_normalizer.py b/tests/normalize/test_number_normalizer.py index a7cf359c..2e06260c 100644 --- a/tests/normalize/test_number_normalizer.py +++ b/tests/normalize/test_number_normalizer.py @@ -1,4 +1,6 @@ import unittest + +from semantica.utils.exceptions import ValidationError from semantica.normalize.number_normalizer import ( NumberNormalizer, UnitConverter, @@ -32,6 +34,19 @@ class TestUnitConverter(unittest.TestCase): # 1 kg = 1000 g self.assertEqual(self.converter.convert_units(1, "kg", "g"), 1000.0) + def test_convert_accepts_aliases_for_category_validation(self): + # Aliases are part of the documented API, not just parsing syntax. + self.assertEqual(self.converter.convert_units(1, "feet", "m"), 0.3048) + self.assertEqual(self.converter.convert_units(1, "gal", "liter"), 3.78541) + + def test_convert_rejects_mismatched_categories_even_for_aliases(self): + # Both units normalize to canonical names first, so the category + # check sees real categories and rejects cross-category conversions. + with self.assertRaises(ValidationError): + self.converter.convert_units(1, "kg", "ft") + with self.assertRaises(ValidationError): + self.converter.convert_units(1, "gal", "lb") + def test_normalize_unit(self): self.assertEqual(self.converter.normalize_unit("km"), "kilometer") self.assertEqual(self.converter.normalize_unit("kgs"), "kilogram")