Skip to content

Commit

Permalink
feat: sort by dimension in formatting
Browse files Browse the repository at this point in the history
This PR adds the ability to sort units by the dimensionality  when formatting to string.

Close #1926, #1841
  • Loading branch information
MichaelTiemannOSC authored Mar 8, 2024
1 parent 3cc2d36 commit e3f24ab
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 8 deletions.
4 changes: 3 additions & 1 deletion CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ Pint Changelog
0.24 (unreleased)
-----------------

- Nothing changed yet.
- Add `dim_sort` function to _formatter_helpers.
- Add `dim_order` and `default_sort_func` properties to FullFormatter.
(PR #1926, fixes Issue #1841)


0.23 (2023-12-08)
Expand Down
64 changes: 62 additions & 2 deletions pint/delegates/formatter/_format_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,67 @@ def format_compound_unit(
if locale is not None:
out = localized_form(out, use_plural, length or "long", locale)

if registry:
out = registry.formatter.default_sort_func(out, registry)

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 +367,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 +380,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
28 changes: 25 additions & 3 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, Iterable, 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 @@ -24,7 +24,12 @@
from ._to_register import REGISTERED_FORMATTERS

if TYPE_CHECKING:
from ...facets.plain import PlainQuantity, PlainUnit, MagnitudeT
from ...facets.plain import (
GenericPlainRegistry,
PlainQuantity,
PlainUnit,
MagnitudeT,
)
from ...facets.measurement import Measurement
from ...compat import Locale

Expand All @@ -38,6 +43,23 @@ 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]], GenericPlainRegistry],
Iterable[tuple[str, Number]],
]
] = lambda self, x, registry: sorted(x)

locale: Optional[Locale] = None
babel_length: Literal["short", "long", "narrow"] = "long"
Expand Down
2 changes: 1 addition & 1 deletion pint/delegates/formatter/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ def format_unit(
self, unit: PlainUnit, uspec: str = "", **babel_kwds: Unpack[BabelKwds]
) -> str:
units = format_compound_unit(unit, uspec, **babel_kwds)

return formatter(
units,
as_ratio=True,
Expand All @@ -87,6 +86,7 @@ def format_unit(
division_fmt=r"{}/{}",
power_fmt=r"{}<sup>{}</sup>",
parentheses_fmt=r"({})",
sort_func=None,
)

def format_quantity(
Expand Down
1 change: 1 addition & 0 deletions pint/delegates/formatter/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ def format_unit(
division_fmt=r"\frac[{}][{}]",
power_fmt="{}^[{}]",
parentheses_fmt=r"\left({}\right)",
sort_func=None,
)
return formatted.replace("[", "{").replace("]", "}")

Expand Down
4 changes: 3 additions & 1 deletion pint/delegates/formatter/plain.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ def format_unit(
division_fmt=" / ",
power_fmt="{} ** {}",
parentheses_fmt=r"({})",
sort_func=None,
)

def format_quantity(
Expand Down Expand Up @@ -175,6 +176,7 @@ def format_unit(
division_fmt="/",
power_fmt="{}**{}",
parentheses_fmt=r"({})",
sort_func=None,
)

def format_quantity(
Expand Down Expand Up @@ -259,7 +261,6 @@ def format_unit(
self, unit: PlainUnit, uspec: str = "", **babel_kwds: Unpack[BabelKwds]
) -> str:
units = format_compound_unit(unit, uspec, **babel_kwds)

return formatter(
units,
as_ratio=True,
Expand All @@ -269,6 +270,7 @@ def format_unit(
power_fmt="{}{}",
parentheses_fmt="({})",
exp_call=pretty_fmt_exponent,
sort_func=None,
)

def format_quantity(
Expand Down
44 changes: 44 additions & 0 deletions pint/testsuite/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,3 +1155,47 @@ 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):
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
assert f"{x}" == result, f"Failed for {spec}, {result}"


@pytest.mark.xfail
def test_issues_1841_xfail():
from pint import formatting as fmt
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?
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 e3f24ab

Please sign in to comment.