diff --git a/CHANGES b/CHANGES index 1cde05402..0b8d625d3 100644 --- a/CHANGES +++ b/CHANGES @@ -4,6 +4,8 @@ Pint Changelog 0.23 (unreleased) ----------------- +- Add `dim_sort` parameter to formatter. + (PR #1864, fixes Issue #1841) - Fixed Transformation type protocol. (PR #1805, PR #1832) - Documented to_preferred and created added an autoautoconvert_to_preferred registry option. diff --git a/pint/default_en.txt b/pint/default_en.txt index 5fc7f8265..391eeb37a 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -56,6 +56,7 @@ @defaults group = international system = mks + dim_order = [ "[substance]", "[mass]", "[current]", "[luminosity]", "[length]", "[]", "[time]", "[temperature]", ] @end diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py index 44bf29858..05cb055a0 100644 --- a/pint/facets/plain/definitions.py +++ b/pint/facets/plain/definitions.py @@ -11,9 +11,10 @@ import itertools import numbers import typing as ty +import ast from dataclasses import dataclass from functools import cached_property -from typing import Any, Optional +from typing import Any, List, Optional from ..._typing import Magnitude from ... import errors @@ -60,12 +61,15 @@ class DefaultsDefinition: group: ty.Optional[str] system: ty.Optional[str] + dim_order: ty.Optional[List[str]] def items(self): if self.group is not None: yield "group", self.group if self.system is not None: yield "system", self.system + if self.dim_order is not None: + yield "dim_order", ast.literal_eval(self.dim_order) @dataclass(frozen=True) diff --git a/pint/formatting.py b/pint/formatting.py index 90613d997..b158da623 100644 --- a/pint/formatting.py +++ b/pint/formatting.py @@ -13,7 +13,8 @@ import functools import re import warnings -from typing import Callable, Any, TYPE_CHECKING, TypeVar, Optional, Union +from typing import Callable, Any, TYPE_CHECKING, TypeVar, List, Optional, Tuple, Union +from collections import OrderedDict from collections.abc import Iterable from numbers import Number @@ -220,7 +221,18 @@ def latex_escape(string: str) -> str: @register_unit_format("L") def format_latex(unit: UnitsContainer, registry: UnitRegistry, **options) -> str: - preprocessed = {rf"\mathrm{{{latex_escape(u)}}}": p for u, p in unit.items()} + # Lift the sorting by dimensions b/c the preprocessed units are unrecognizeable + sorted_units = dim_sort(unit.items(), registry) + preprocessed_list = [ + ( + rf"\mathrm{{{latex_escape(u)}}}", + p, + ) + for u, p in sorted_units + ] + preprocessed = OrderedDict() + for k, v in preprocessed_list: + preprocessed[k] = v formatted = formatter( preprocessed.items(), as_ratio=True, @@ -229,6 +241,8 @@ def format_latex(unit: UnitsContainer, registry: UnitRegistry, **options) -> str division_fmt=r"\frac[{}][{}]", power_fmt="{}^[{}]", parentheses_fmt=r"\left({}\right)", + sort=False, + sort_dims=False, registry=registry, **options, ) @@ -296,24 +310,13 @@ def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> s ) -dim_order = [ - "[substance]", - "[mass]", - "[current]", - "[luminosity]", - "[length]", - "[time]", - "[temperature]", -] - - -def dim_sort(units: Iterable[list[str]], registry: UnitRegistry): +def dim_sort(items: Iterable[Tuple[str, Number]], registry: UnitRegistry): """Sort a list of units by dimensional order. Parameters ---------- - units : list - a list of unit names (without values). + 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. @@ -327,30 +330,46 @@ def dim_sort(units: Iterable[list[str]], registry: UnitRegistry): KeyError If unit cannot be found in the registry. """ + if registry is None or len(items) <= 1: + return items + # if len(items) == 2 and items[0][1] * items[1][1] < 0: + # return items ret_dict = dict() - len(units) > 1 - for name in units: + for name, value in items: cname = registry.get_name(name) if not cname: continue - dim_types = iter(dim_order) + cname_dims = registry.get_dimensionality(cname) + if len(cname_dims) == 0: + cname_dims = {"[]": None} + dim_types = iter(registry._defaults["dim_order"]) while True: try: dim = next(dim_types) - if dim in registry.get_dimensionality(cname): + if dim in cname_dims: if dim not in ret_dict: ret_dict[dim] = list() - ret_dict[dim].append(cname) + ret_dict[dim].append( + ( + name, + value, + ) + ) break except StopIteration: - raise KeyError(f"Unit {cname} has no recognized dimensions") + raise KeyError( + f"Unit {name} (aka {cname}) has no recognized dimensions" + ) - ret = sum([ret_dict[dim] for dim in dim_order if dim in ret_dict], []) + ret = sum( + [ret_dict[dim] for dim in registry._defaults["dim_order"] if dim in ret_dict], + [], + ) return ret def formatter( - items: Iterable[tuple[str, Number]], + items: Iterable[Tuple[str, Number]], as_ratio: bool = True, single_denominator: bool = False, product_fmt: str = " * ", @@ -362,7 +381,7 @@ def formatter( babel_length: str = "long", babel_plural_form: str = "one", sort: bool = True, - sort_dims: bool = False, + sort_dims: bool = True, registry: Optional[UnitRegistry] = None, ) -> str: """Format a list of (name, exponent) pairs. @@ -420,6 +439,8 @@ def formatter( if sort: items = sorted(items) + if sort_dims: + items = dim_sort(items, registry) for key, value in items: if locale and babel_length and babel_plural_form and key in _babel_units: _key = _babel_units[key] @@ -459,12 +480,6 @@ def formatter( else: neg_terms.append(power_fmt.format(key, fun(value))) - if sort_dims: - if len(pos_terms) > 1: - pos_terms = dim_sort(pos_terms, registry) - if len(neg_terms) > 1: - neg_terms = dim_sort(neg_terms, registry) - if not as_ratio: # Show as Product: positive * negative terms ** -1 return _join(product_fmt, pos_terms + neg_terms) @@ -640,7 +655,7 @@ def vector_to_latex(vec: Iterable[Any], fmtfun: FORMATTER = ".2f".format) -> str def matrix_to_latex(matrix: ItMatrix, fmtfun: FORMATTER = ".2f".format) -> str: - ret: list[str] = [] + ret: List[str] = [] for row in matrix: ret += [" & ".join(fmtfun(f) for f in row)] diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 3ee0f11df..7702ea2eb 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -1173,3 +1173,57 @@ def test_issues_1841(): # this prints "1 h·kW", not "1 kW·h" unless sort_dims is True # print(q) + + q = ur.Quantity("1 kV * A") + + assert ( + pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True) + == "kilovolt * ampere" + ) + assert ( + pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=False) + == "ampere * kilovolt" + ) + + # this prints "1 A·kV", not "1 kV·A" unless sort_dims is True + # print(q) + + q = ur.Quantity("1 N * m") + + assert ( + pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True) + == "newton * meter" + ) + assert ( + pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=False) + == "meter * newton" + ) + + # this prints "1 m·N", not "1 N·m" unless sort_dims is True + # print(q) + + +@pytest.mark.xfail +def test_issues_1841_xfail(): + import pint + + # sets compact display mode + ur = UnitRegistry() + ur.default_format = "~P" + + 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? + assert ( + pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True) + == "radian * hour" + ) + assert ( + pint.formatting.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)