Skip to content
Closed
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_order` property to BaseFormatter.
- Add `dim_sort` parameter to formatter.
(PR #1864, fixes Issue #1841)


0.23 (2023-12-08)
Expand Down
28 changes: 26 additions & 2 deletions docs/getting/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ a ``Quantity()`` object.
``Quantity()`` objects also work well with NumPy arrays, which you can
read about in the section on :doc:`NumPy support <numpy>`.

Some units are compound, such as [energy], which is stated in terms of
[mass] * [length]**2 / [time]**2. Earlier versions of Pint would sort unit names
alphabetically by default, leading to different orderings of units (old behavior):

```
"{:P}".format(ureg.parse_units('pound * ft**2 * second**-2')) would yield:
'foot²·pound/second²'
"{:P}".format(ureg.parse_units('kg * cm**2 * second**-2')) would yield:
'centimeter²·kilogram/second²'
```

Now by default it sorts by dimensions as proposed by ISO 80000, with [mass]
coming before [length], which also comes before [time]. The dimension order
can be changed in the registry (`dim_order` in `defaults`):

.. doctest::

>>> "{:P}".format(ureg.parse_units('pound * ft**2 * second**-2'))
'pound·foot²/second²'
>>> "{:P}".format(ureg.parse_units('kg * cm**2 * second**-2'))
'kilogram·centimeter²/second²'



Converting to different units
-----------------------------

Expand Down Expand Up @@ -180,11 +204,11 @@ but otherwise keeping your unit definitions intact.
>>> volume = 10*ureg.cc
>>> mass = density*volume
>>> print(mass)
14.0 cubic_centimeter * gram / centimeter ** 3
14.0 gram * cubic_centimeter / centimeter ** 3
>>> print(mass.to_reduced_units())
14.0 gram
>>> print(mass)
14.0 cubic_centimeter * gram / centimeter ** 3
14.0 gram * cubic_centimeter / centimeter ** 3
>>> mass.ito_reduced_units()
>>> print(mass)
14.0 gram
Expand Down
4 changes: 2 additions & 2 deletions pint/delegates/formatter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
"""


from .base_formatter import BabelFormatter
from .base_formatter import BaseFormatter, BabelFormatter


class Formatter(BabelFormatter):
class Formatter(BabelFormatter, BaseFormatter):
# TODO: this should derive from all relevant formaters to
# reproduce the current behavior of Pint.
pass
Expand Down
12 changes: 12 additions & 0 deletions pint/delegates/formatter/base_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@


class BaseFormatter:
# This default order for sorting dimensions was described in the proposed ISO 80000 specification.
dim_order = (
"[substance]",
"[mass]",
"[current]",
"[luminosity]",
"[length]",
"[]",
"[time]",
"[temperature]",
)

def format_quantity(
self, quantity: PlainQuantity[MagnitudeT], spec: str = ""
) -> str:
Expand Down
91 changes: 86 additions & 5 deletions pint/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
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.abc import Iterable
from numbers import Number

Expand Down Expand Up @@ -197,6 +197,7 @@ def format_pretty(unit: UnitsContainer, registry: UnitRegistry, **options) -> st
power_fmt="{}{}",
parentheses_fmt="({})",
exp_call=_pretty_fmt_exponent,
registry=registry,
**options,
)

Expand All @@ -219,15 +220,26 @@ 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 = [
(
rf"\mathrm{{{latex_escape(u)}}}",
p,
)
for u, p in sorted_units
]
formatted = formatter(
preprocessed.items(),
preprocessed,
as_ratio=True,
single_denominator=True,
product_fmt=r" \cdot ",
division_fmt=r"\frac[{}][{}]",
power_fmt="{}^[{}]",
parentheses_fmt=r"\left({}\right)",
sort=False,
sort_dims=False,
registry=registry,
**options,
)
return formatted.replace("[", "{").replace("]", "}")
Expand Down Expand Up @@ -259,6 +271,7 @@ def format_html(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
division_fmt=r"{}/{}",
power_fmt=r"{}<sup>{}</sup>",
parentheses_fmt=r"({})",
registry=registry,
**options,
)

Expand All @@ -273,6 +286,7 @@ def format_default(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
division_fmt=" / ",
power_fmt="{} ** {}",
parentheses_fmt=r"({})",
registry=registry,
**options,
)

Expand All @@ -287,12 +301,67 @@ def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
division_fmt="/",
power_fmt="{}**{}",
parentheses_fmt=r"({})",
registry=registry,
**options,
)


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 or len(items) <= 1:
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]],
items: Iterable[Tuple[str, Number]],
as_ratio: bool = True,
single_denominator: bool = False,
product_fmt: str = " * ",
Expand All @@ -304,6 +373,8 @@ def formatter(
babel_length: str = "long",
babel_plural_form: str = "one",
sort: bool = True,
sort_dims: bool = True,
registry: Optional[UnitRegistry] = None,
) -> str:
"""Format a list of (name, exponent) pairs.

Expand Down Expand Up @@ -334,6 +405,14 @@ def formatter(
(Default value = lambda x: f"{x:n}")
sort : bool, optional
True to sort the formatted units alphabetically (Default value = True)
sort_dims : bool, optional
True to sort the units dimentionally (Default value = False).
When dimensions have multiple units, sort by "most significant dimension" the unit contains
When both `sort` and `sort_dims` are True, sort alphabetically within sorted dimensions
ISO 80000 and other sources guide on how dimensions shoule be ordered; the user
can set their preference in the registry.
registry : UnitRegistry, optional
The registry to use if `sort_dims` is True

Returns
-------
Expand All @@ -354,6 +433,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 @@ -568,7 +649,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
6 changes: 3 additions & 3 deletions pint/testsuite/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,19 +70,19 @@ def func_registry():

@pytest.fixture(scope="class")
def class_registry():
"""Only use for those test that do not modify the registry."""
"""Only use for those tests that do not modify the registry."""
return pint.UnitRegistry()


@pytest.fixture(scope="module")
def module_registry():
"""Only use for those test that do not modify the registry."""
"""Only use for those tests that do not modify the registry."""
return pint.UnitRegistry()


@pytest.fixture(scope="session")
def sess_registry():
"""Only use for those test that do not modify the registry."""
"""Only use for those tests that do not modify the registry."""
return pint.UnitRegistry()


Expand Down
77 changes: 77 additions & 0 deletions pint/testsuite/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,3 +1155,80 @@ def test_issues_1505():
assert isinstance(
ur.Quantity("m/s").magnitude, decimal.Decimal
) # unexpected fail (magnitude should be a decimal)


def test_issues_1841():
import pint

# sets compact display mode
ur = UnitRegistry()
ur.default_format = "~P"

# creates quantity
q = ur.Quantity("1 kW * 1 h")

assert (
pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=True)
== "kilowatt * hour"
)
assert (
pint.formatting.format_unit(q.u._units, spec="", registry=ur, sort_dims=False)
== "hour * kilowatt"
)

# 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)