Skip to content

Commit

Permalink
Make sort_dims default and sorting user-defineable
Browse files Browse the repository at this point in the history
Per suggestions from @andrewgsavage make `sort_dims` default.  Also allow users to specify dimensional sorting in registry definition.

Also fix a number of sub-tests not previously tested.  Tests all pass without any drama.

Signed-off-by: Michael Tiemann <[email protected]>
  • Loading branch information
MichaelTiemannOSC committed Oct 22, 2023
1 parent cdf7e65 commit 1d7327a
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 33 deletions.
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions pint/default_en.txt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
@defaults
group = international
system = mks
dim_order = [ "[substance]", "[mass]", "[current]", "[luminosity]", "[length]", "[]", "[time]", "[temperature]", ]
@end


Expand Down
6 changes: 5 additions & 1 deletion pint/facets/plain/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
79 changes: 47 additions & 32 deletions pint/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down Expand Up @@ -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.
Expand All @@ -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 = " * ",
Expand All @@ -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.
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)]
Expand Down
54 changes: 54 additions & 0 deletions pint/testsuite/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit 1d7327a

Please sign in to comment.