diff --git a/pint/delegates/formatter/_format_helpers.py b/pint/delegates/formatter/_format_helpers.py index 2ed4ba985..868663b99 100644 --- a/pint/delegates/formatter/_format_helpers.py +++ b/pint/delegates/formatter/_format_helpers.py @@ -268,6 +268,61 @@ def format_compound_unit( return out +def dim_sort(items: Iterable[tuple[str, Number]], registry: UnitRegistry): + """Sort a list of units by dimensional order (from `registry.formatter.dim_order`). + + Parameters + ---------- + items : tuple + a list of tuples containing (unit names, exponent values). + registry : UnitRegistry + the registry to use for looking up the dimensions of each unit. + + Returns + ------- + list + the list of units sorted by most significant dimension first. + + Raises + ------ + KeyError + If unit cannot be found in the registry. + """ + + if registry is None: + return items + ret_dict = dict() + dim_order = registry.formatter.dim_order + for unit_name, unit_exponent in items: + cname = registry.get_name(unit_name) + if not cname: + continue + cname_dims = registry.get_dimensionality(cname) + if len(cname_dims) == 0: + cname_dims = {"[]": None} + dim_types = iter(dim_order) + while True: + try: + dim = next(dim_types) + if dim in cname_dims: + if dim not in ret_dict: + ret_dict[dim] = list() + ret_dict[dim].append( + ( + unit_name, + unit_exponent, + ) + ) + break + except StopIteration: + raise KeyError( + f"Unit {unit_name} (aka {cname}) has no recognized dimensions" + ) + + ret = sum([ret_dict[dim] for dim in dim_order if dim in ret_dict], []) + return ret + + def formatter( items: Iterable[tuple[str, Number]], as_ratio: bool = True, @@ -309,6 +364,8 @@ def formatter( (Default value = lambda x: f"{x:n}") sort : bool, optional True to sort the formatted units alphabetically (Default value = True) + sort_func : callable + If not None, `sort_func` returns its sorting of the formatted units Returns ------- @@ -320,14 +377,14 @@ def formatter( if sort is False: warn( "The boolean `sort` argument is deprecated. " - "Use `sort_fun` to specify the sorting function (default=sorted) " + "Use `sort_func` to specify the sorting function (default=sorted) " "or None to keep units in the original order." ) sort_func = None elif sort is True: warn( "The boolean `sort` argument is deprecated. " - "Use `sort_fun` to specify the sorting function (default=sorted) " + "Use `sort_func` to specify the sorting function (default=sorted) " "or None to keep units in the original order." ) sort_func = sorted diff --git a/pint/delegates/formatter/full.py b/pint/delegates/formatter/full.py index fae26d524..a8dc1ec56 100644 --- a/pint/delegates/formatter/full.py +++ b/pint/delegates/formatter/full.py @@ -11,9 +11,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Literal, Optional, Any +from typing import TYPE_CHECKING, Callable, Literal, Optional, Any import locale -from ...compat import babel_parse, Unpack +from ...compat import babel_parse, Number, Unpack from ...util import iterable from ..._typing import Magnitude @@ -38,6 +38,18 @@ class FullFormatter: _formatters: dict[str, Any] = {} default_format: str = "" + # TODO: This can be over-riden by the registry definitions file + dim_order = ( + "[substance]", + "[mass]", + "[current]", + "[luminosity]", + "[length]", + "[]", + "[time]", + "[temperature]", + ) + default_sort_func: Optional[Callable[Iterable[tuple[str, Number]]], Iterable[tuple[str, Number]]] = None locale: Optional[Locale] = None babel_length: Literal["short", "long", "narrow"] = "long" diff --git a/pint/delegates/formatter/html.py b/pint/delegates/formatter/html.py index 3dc14330c..ea88fb13b 100644 --- a/pint/delegates/formatter/html.py +++ b/pint/delegates/formatter/html.py @@ -87,6 +87,7 @@ def format_unit( division_fmt=r"{}/{}", power_fmt=r"{}{}", parentheses_fmt=r"({})", + sort_func=lambda x: unit._REGISTRY.formatter.default_sort_func(x, unit._REGISTRY), ) def format_quantity( diff --git a/pint/delegates/formatter/latex.py b/pint/delegates/formatter/latex.py index aacf8cdf5..cf2b57848 100644 --- a/pint/delegates/formatter/latex.py +++ b/pint/delegates/formatter/latex.py @@ -173,6 +173,9 @@ def format_unit( self, unit: PlainUnit, uspec: str = "", **babel_kwds: Unpack[BabelKwds] ) -> str: units = format_compound_unit(unit, uspec, **babel_kwds) + if unit._REGISTRY.formatter.default_sort_func: + # Lift the sorting by dimensions b/c the preprocessed units are unrecognizeable + units = unit._REGISTRY.formatter.default_sort_func(units, unit._REGISTRY) preprocessed = {rf"\mathrm{{{latex_escape(u)}}}": p for u, p in units} formatted = formatter( @@ -183,6 +186,7 @@ def format_unit( division_fmt=r"\frac[{}][{}]", power_fmt="{}^[{}]", parentheses_fmt=r"\left({}\right)", + sort_func=None, ) return formatted.replace("[", "{").replace("]", "}") diff --git a/pint/delegates/formatter/plain.py b/pint/delegates/formatter/plain.py index 4b9616631..01c352bf9 100644 --- a/pint/delegates/formatter/plain.py +++ b/pint/delegates/formatter/plain.py @@ -269,6 +269,7 @@ def format_unit( power_fmt="{}{}", parentheses_fmt="({})", exp_call=pretty_fmt_exponent, + sort_func=lambda x: unit._REGISTRY.formatter.default_sort_func(x, unit._REGISTRY), ) def format_quantity( diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 3db01fb4e..167d6bb4d 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -1155,3 +1155,52 @@ def test_issues_1505(): assert isinstance( ur.Quantity("m/s").magnitude, decimal.Decimal ) # unexpected fail (magnitude should be a decimal) + + +def test_issues_1841(subtests): + import pint + from pint.delegates.formatter._format_helpers import dim_sort + + ur = UnitRegistry() + ur.formatter.default_sort_func = dim_sort + + for x, spec, result in ( + (ur.Unit(UnitsContainer(hour=1,watt=1)), "P~", "W·h"), + (ur.Unit(UnitsContainer(ampere=1,volt=1)), "P~", "V·A"), + (ur.Unit(UnitsContainer(meter=1,newton=1)), "P~", "N·m"), + ): + with subtests.test(spec): + ur.default_format = spec + breakpoint() + assert f"{x}" == result, f"Failed for {spec}, {result}" + + +@pytest.mark.xfail +def test_issues_1841_xfail(): + import pint + from pint import formatting as fmt + import pint.delegates.formatter._format_helpers + from pint.delegates.formatter._format_helpers import dim_sort + + # sets compact display mode by default + ur = UnitRegistry() + ur.default_format = "~P" + ur.formatter.default_sort_func = dim_sort + + q = ur.Quantity("2*pi radian * hour") + + # Note that `radian` (and `bit` and `count`) are treated as dimensionless. + # And note that dimensionless quantities are stripped by this process, + # leading to errorneous output. Suggestions? + breakpoint() + assert ( + fmt.format_unit(q.u._units, spec="", registry=ur, sort_dims=True) + == "radian * hour" + ) + assert ( + fmt.format_unit(q.u._units, spec="", registry=ur, sort_dims=False) + == "hour * radian" + ) + + # this prints "2*pi hour * radian", not "2*pi radian * hour" unless sort_dims is True + # print(q)