Skip to content

Commit

Permalink
Reimplementation of sorting units by dimensions
Browse files Browse the repository at this point in the history
Adapt PR#1841 to the new Pint formatter.

Signed-off-by: Michael Tiemann <[email protected]>
  • Loading branch information
MichaelTiemannOSC committed Jan 23, 2024
1 parent 3cc2d36 commit b1b941b
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 4 deletions.
61 changes: 59 additions & 2 deletions pint/delegates/formatter/_format_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
-------
Expand All @@ -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
Expand Down
16 changes: 14 additions & 2 deletions pint/delegates/formatter/full.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand Down
1 change: 1 addition & 0 deletions pint/delegates/formatter/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ def format_unit(
division_fmt=r"{}/{}",
power_fmt=r"{}<sup>{}</sup>",
parentheses_fmt=r"({})",
sort_func=lambda x: unit._REGISTRY.formatter.default_sort_func(x, unit._REGISTRY),
)

def format_quantity(
Expand Down
4 changes: 4 additions & 0 deletions pint/delegates/formatter/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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("]", "}")

Expand Down
1 change: 1 addition & 0 deletions pint/delegates/formatter/plain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
49 changes: 49 additions & 0 deletions pint/testsuite/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

0 comments on commit b1b941b

Please sign in to comment.