ucon 0.6.1__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ucon-0.6.1 → ucon-0.6.3}/PKG-INFO +1 -1
- {ucon-0.6.1 → ucon-0.6.3}/ROADMAP.md +1 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_unit_parsing.py +86 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/units.py +31 -2
- {ucon-0.6.1 → ucon-0.6.3}/ucon.egg-info/PKG-INFO +1 -1
- {ucon-0.6.1 → ucon-0.6.3}/.github/workflows/publish.yaml +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/.github/workflows/tests.yaml +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/.gitignore +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/LICENSE +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/Makefile +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/NOTICE +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/README.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/decisions/001-unity-distance-metric-for-nearest-scale.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/decisions/002-composite-units.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/decisions/003-composable-unit-algebra.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/decisions/004-unit-algebra-naming.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/decisions/005-pseudo-dimension-tuple-values.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/decisions/006-pydantic-integration-pattern.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/examples/basis-transform-fantasy-units.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/explainers/exponent-scale-relationship.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/explainers/type-operation-matrix.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/explainers/why-algebraic-closure-matters.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/explainers/why-type-safety-matters.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/proposals/project_unified-algebraic-core.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/proposals/support-for-fractional-exponents.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/docs/proposals/unified-unit-presentation.md +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/pyproject.toml +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/requirements.txt +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/setup.cfg +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/setup.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/__init__.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/__init__.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/conversion/__init__.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/conversion/test_graph.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/conversion/test_map.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/mcp/__init__.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/mcp/test_server.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_algebra.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_basis_transform.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_core.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_default_graph_conversions.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_dimensionless_units.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_graph_basis_transform.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_logmap.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_nines.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_pickle.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_pydantic.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_quantity.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_rebased_unit.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_uncertainty.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_unit_system.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_units.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/tests/ucon/test_vector_fraction.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/__init__.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/algebra.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/core.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/graph.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/maps.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/mcp/__init__.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/mcp/server.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/pydantic.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon/quantity.py +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon.egg-info/SOURCES.txt +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon.egg-info/dependency_links.txt +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon.egg-info/entry_points.txt +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon.egg-info/requires.txt +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/ucon.egg-info/top_level.txt +0 -0
- {ucon-0.6.1 → ucon-0.6.3}/uv.lock +0 -0
|
@@ -244,6 +244,7 @@ Building on v0.5.x baseline:
|
|
|
244
244
|
- [x] `parse("kg * m / s^2")` → `UnitProduct` (completed in v0.6.0 via `get_unit_by_name()`)
|
|
245
245
|
- [x] Alias resolution (`meters`, `metre`, `m` all work) (completed in v0.6.0)
|
|
246
246
|
- [ ] Uncertainty parsing: `parse("1.234 ± 0.005 m")`
|
|
247
|
+
- [ ] Revisit priority alias architecture (v0.6.x uses `_PRIORITY_ALIASES` / `_PRIORITY_SCALED_ALIASES` for `min`, `mcg`; consider "exact match first" or longest-match strategy if list grows)
|
|
247
248
|
|
|
248
249
|
**Outcomes:**
|
|
249
250
|
- Human-friendly unit input for interactive and configuration use cases
|
|
@@ -286,5 +286,91 @@ class TestWhitespaceHandling(unittest.TestCase):
|
|
|
286
286
|
self.assertEqual(result, units.meter)
|
|
287
287
|
|
|
288
288
|
|
|
289
|
+
class TestPriorityAliases(unittest.TestCase):
|
|
290
|
+
"""Test that priority aliases are matched before prefix decomposition.
|
|
291
|
+
|
|
292
|
+
Some aliases like 'min' could be misinterpreted as prefix+unit
|
|
293
|
+
(e.g., 'm' + 'in' = milli-inch). Priority aliases ensure these
|
|
294
|
+
are matched exactly first.
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
def test_min_is_minute_not_milli_inch(self):
|
|
298
|
+
"""'min' should parse as minute (time), not milli-inch (length)."""
|
|
299
|
+
from ucon.core import Dimension
|
|
300
|
+
result = get_unit_by_name("min")
|
|
301
|
+
self.assertEqual(result, units.minute)
|
|
302
|
+
self.assertEqual(result.dimension, Dimension.time)
|
|
303
|
+
|
|
304
|
+
def test_min_in_composite(self):
|
|
305
|
+
"""'min' should work correctly in composite units."""
|
|
306
|
+
result = get_unit_by_name("mL/min")
|
|
307
|
+
self.assertIsInstance(result, UnitProduct)
|
|
308
|
+
# Volume / time dimension
|
|
309
|
+
from ucon.core import Dimension
|
|
310
|
+
expected_dim = Dimension.volume / Dimension.time
|
|
311
|
+
self.assertEqual(result.dimension, expected_dim)
|
|
312
|
+
|
|
313
|
+
def test_mL_per_min_conversion(self):
|
|
314
|
+
"""Conversion using 'min' should work correctly."""
|
|
315
|
+
from ucon.core import Number
|
|
316
|
+
rate_per_hour = Number(120, unit=get_unit_by_name("mL/h"))
|
|
317
|
+
rate_per_min = rate_per_hour.to(get_unit_by_name("mL/min"))
|
|
318
|
+
self.assertAlmostEqual(rate_per_min.quantity, 2.0, places=9)
|
|
319
|
+
|
|
320
|
+
def test_milli_prefix_still_works(self):
|
|
321
|
+
"""Normal milli- prefix parsing should still work."""
|
|
322
|
+
result = get_unit_by_name("mL")
|
|
323
|
+
self.assertIsInstance(result, UnitProduct)
|
|
324
|
+
self.assertAlmostEqual(result.fold_scale(), 0.001, places=10)
|
|
325
|
+
|
|
326
|
+
def test_inch_still_works(self):
|
|
327
|
+
"""Inch unit should still be accessible."""
|
|
328
|
+
result = get_unit_by_name("in")
|
|
329
|
+
self.assertEqual(result, units.inch)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class TestPriorityScaledAliases(unittest.TestCase):
|
|
333
|
+
"""Test priority scaled aliases for domain-specific conventions.
|
|
334
|
+
|
|
335
|
+
Some domains use non-standard abbreviations that include an implicit
|
|
336
|
+
scale, like 'mcg' for microgram in medical contexts.
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
def test_mcg_is_microgram(self):
|
|
340
|
+
"""'mcg' should parse as microgram (medical convention)."""
|
|
341
|
+
from ucon.core import Dimension
|
|
342
|
+
result = get_unit_by_name("mcg")
|
|
343
|
+
self.assertIsInstance(result, UnitProduct)
|
|
344
|
+
self.assertEqual(result.dimension, Dimension.mass)
|
|
345
|
+
self.assertAlmostEqual(result.fold_scale(), 1e-6, places=15)
|
|
346
|
+
|
|
347
|
+
def test_mcg_to_mg(self):
|
|
348
|
+
"""Conversion from mcg to mg should work."""
|
|
349
|
+
from ucon.core import Number
|
|
350
|
+
dose = Number(500, unit=get_unit_by_name("mcg"))
|
|
351
|
+
result = dose.to(get_unit_by_name("mg"))
|
|
352
|
+
self.assertAlmostEqual(result.quantity, 0.5, places=9)
|
|
353
|
+
|
|
354
|
+
def test_mcg_to_ug(self):
|
|
355
|
+
"""mcg and µg should be equivalent."""
|
|
356
|
+
from ucon.core import Number
|
|
357
|
+
dose = Number(1, unit=get_unit_by_name("mcg"))
|
|
358
|
+
result = dose.to(get_unit_by_name("µg"))
|
|
359
|
+
self.assertAlmostEqual(result.quantity, 1.0, places=9)
|
|
360
|
+
|
|
361
|
+
def test_mcg_in_composite(self):
|
|
362
|
+
"""'mcg' should work in composite units."""
|
|
363
|
+
result = get_unit_by_name("mcg/mL")
|
|
364
|
+
self.assertIsInstance(result, UnitProduct)
|
|
365
|
+
from ucon.core import Dimension
|
|
366
|
+
self.assertEqual(result.dimension, Dimension.density)
|
|
367
|
+
|
|
368
|
+
def test_mcg_per_kg_per_min(self):
|
|
369
|
+
"""'mcg/kg/min' style dosing units (requires chained division support)."""
|
|
370
|
+
# This tests mcg works; chained division is a separate issue
|
|
371
|
+
result = get_unit_by_name("mcg")
|
|
372
|
+
self.assertIsInstance(result, UnitProduct)
|
|
373
|
+
|
|
374
|
+
|
|
289
375
|
if __name__ == '__main__':
|
|
290
376
|
unittest.main()
|
|
@@ -204,6 +204,15 @@ def have(name: str) -> bool:
|
|
|
204
204
|
_UNIT_REGISTRY: Dict[str, Unit] = {}
|
|
205
205
|
_UNIT_REGISTRY_CASE_SENSITIVE: Dict[str, Unit] = {}
|
|
206
206
|
|
|
207
|
+
# Priority aliases that must match exactly before prefix decomposition.
|
|
208
|
+
# Prevents ambiguous parses like "min" -> milli-inch instead of minute.
|
|
209
|
+
_PRIORITY_ALIASES: set = {'min'}
|
|
210
|
+
|
|
211
|
+
# Priority scaled aliases that map to a specific (unit, scale) tuple.
|
|
212
|
+
# Used for medical conventions like "mcg" -> (gram, Scale.micro).
|
|
213
|
+
# Populated by _build_registry() after units are defined.
|
|
214
|
+
_PRIORITY_SCALED_ALIASES: Dict[str, Tuple[Unit, Scale]] = {}
|
|
215
|
+
|
|
207
216
|
# Scale prefix mapping (shorthand -> Scale)
|
|
208
217
|
# Sorted by length descending for greedy matching
|
|
209
218
|
_SCALE_PREFIXES: Dict[str, Scale] = {
|
|
@@ -256,6 +265,9 @@ def _build_registry() -> None:
|
|
|
256
265
|
_UNIT_REGISTRY[alias.lower()] = obj
|
|
257
266
|
_UNIT_REGISTRY_CASE_SENSITIVE[alias] = obj
|
|
258
267
|
|
|
268
|
+
# Register priority scaled aliases (medical conventions)
|
|
269
|
+
_PRIORITY_SCALED_ALIASES['mcg'] = (gram, Scale.micro) # microgram
|
|
270
|
+
|
|
259
271
|
|
|
260
272
|
def _parse_exponent(s: str) -> Tuple[str, float]:
|
|
261
273
|
"""
|
|
@@ -294,7 +306,10 @@ def _lookup_factor(s: str) -> Tuple[Unit, Scale]:
|
|
|
294
306
|
"""
|
|
295
307
|
Look up a single unit factor, handling scale prefixes.
|
|
296
308
|
|
|
297
|
-
Prioritizes prefix+unit interpretation over direct unit lookup
|
|
309
|
+
Prioritizes prefix+unit interpretation over direct unit lookup,
|
|
310
|
+
except for priority aliases (like 'min', 'mcg') which are checked first
|
|
311
|
+
to avoid ambiguous parses or to handle domain-specific conventions.
|
|
312
|
+
|
|
298
313
|
This means "kg" returns (gram, Scale.kilo) rather than (kilogram, Scale.one).
|
|
299
314
|
|
|
300
315
|
Examples:
|
|
@@ -303,6 +318,8 @@ def _lookup_factor(s: str) -> Tuple[Unit, Scale]:
|
|
|
303
318
|
- 'km' -> (meter, Scale.kilo)
|
|
304
319
|
- 'kg' -> (gram, Scale.kilo)
|
|
305
320
|
- 'mL' -> (liter, Scale.milli)
|
|
321
|
+
- 'min' -> (minute, Scale.one) # priority alias, not milli-inch
|
|
322
|
+
- 'mcg' -> (gram, Scale.micro) # medical convention for microgram
|
|
306
323
|
|
|
307
324
|
Returns:
|
|
308
325
|
Tuple of (unit, scale).
|
|
@@ -310,7 +327,19 @@ def _lookup_factor(s: str) -> Tuple[Unit, Scale]:
|
|
|
310
327
|
Raises:
|
|
311
328
|
UnknownUnitError: If the unit cannot be resolved.
|
|
312
329
|
"""
|
|
313
|
-
#
|
|
330
|
+
# Check priority scaled aliases first (e.g., "mcg" -> microgram)
|
|
331
|
+
if s in _PRIORITY_SCALED_ALIASES:
|
|
332
|
+
return _PRIORITY_SCALED_ALIASES[s]
|
|
333
|
+
|
|
334
|
+
# Check priority aliases (prevents "min" -> milli-inch)
|
|
335
|
+
if s in _PRIORITY_ALIASES:
|
|
336
|
+
if s in _UNIT_REGISTRY_CASE_SENSITIVE:
|
|
337
|
+
return _UNIT_REGISTRY_CASE_SENSITIVE[s], Scale.one
|
|
338
|
+
s_lower = s.lower()
|
|
339
|
+
if s_lower in _UNIT_REGISTRY:
|
|
340
|
+
return _UNIT_REGISTRY[s_lower], Scale.one
|
|
341
|
+
|
|
342
|
+
# Try scale prefix + unit (prioritize decomposition)
|
|
314
343
|
# Only case-sensitive matching for remainder (e.g., "fT" = femto-tesla, "ft" = foot)
|
|
315
344
|
for prefix in _SCALE_PREFIXES_SORTED:
|
|
316
345
|
if s.startswith(prefix) and len(s) > len(prefix):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ucon-0.6.1 → ucon-0.6.3}/NOTICE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|