Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dim sort #1926

Merged
merged 7 commits into from
Mar 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Loading