Skip to content

Commit

Permalink
Implement sorting units by dimension order
Browse files Browse the repository at this point in the history
Once upon a time, ISO 80000 attempted to codify preferences for ordering units when describing quantities, for example `kW h` (not `h Kw`).  While they have withdrawn the standard, there are many publications that state a preference for ordering units when describing quantities.

Pint allows users to choose to sort units alphabetically or not (thus `kW * h` becomes `h * kW`, whereas `kW * s` retmains `kW * s` becase `kW` sorts as less than `s`).

This PR adds a `sort_dims` parameter to `pint.formatting.formatter` which can be used in conjunction with alphabetical sorting.  `sort_dims` imposes a "most significant dimension" order on the units, which are then sorted alphebetically (or not) within each dimension type.

This addresses #1841.  It is intended as a prototype to evaluate as pint's formatting roadmap evolves.  In particular, it needs a way to make `sort_dims` more accessible to the user.

Signed-off-by: Michael Tiemann <[email protected]>
  • Loading branch information
MichaelTiemannOSC committed Oct 22, 2023
1 parent 2bdb258 commit 9320a32
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 0 deletions.
61 changes: 61 additions & 0 deletions pint/formatting.py
Original file line number Diff line number Diff line change
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 Down Expand Up @@ -228,6 +229,7 @@ def format_latex(unit: UnitsContainer, registry: UnitRegistry, **options) -> str
division_fmt=r"\frac[{}][{}]",
power_fmt="{}^[{}]",
parentheses_fmt=r"\left({}\right)",
registry=registry,
**options,
)
return formatted.replace("[", "{").replace("]", "}")
Expand Down Expand Up @@ -259,6 +261,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 +276,7 @@ def format_default(unit: UnitsContainer, registry: UnitRegistry, **options) -> s
division_fmt=" / ",
power_fmt="{} ** {}",
parentheses_fmt=r"({})",
registry=registry,
**options,
)

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


dim_order = [ '[substance]', '[mass]', '[current]', '[luminosity]', '[length]', '[time]', '[temperature]' ]
def dim_sort(units: Iterable[list[str]], registry: UnitRegistry):
"""Sort a list of units by dimensional order.
Parameters
----------
units : list
a list of unit names (without 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.
"""
ret_dict = dict()
many = len(units) > 1
for name in units:
cname = registry.get_name(name)
if not cname:
continue
dim_types = iter(dim_order)
while True:
try:
dim = next(dim_types)
if dim in registry.get_dimensionality(cname):
if dim not in ret_dict:
ret_dict[dim] = list()
ret_dict[dim].append(cname)
break
except StopIteration:
raise KeyError(f"Unit {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 All @@ -304,6 +351,8 @@ def formatter(
babel_length: str = "long",
babel_plural_form: str = "one",
sort: bool = True,
sort_dims: bool = False,
registry: Optional[UnitRegistry] = None,
) -> str:
"""Format a list of (name, exponent) pairs.
Expand Down Expand Up @@ -334,6 +383,12 @@ 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
registry : UnitRegistry, optional
The registry to use if `sort_dims` is True
Returns
-------
Expand Down Expand Up @@ -393,6 +448,12 @@ 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
17 changes: 17 additions & 0 deletions pint/testsuite/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -1150,3 +1150,20 @@ 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)

0 comments on commit 9320a32

Please sign in to comment.