"""
pint.formatter
~~~~~~~~~~~~~~
Format units for pint.
:copyright: 2016 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
from __future__ import annotations
import functools
import re
import warnings
from typing import Callable, Any, TYPE_CHECKING, TypeVar, Optional, Union
from collections.abc import Iterable
from numbers import Number
from .babel_names import _babel_lengths, _babel_units
from .compat import babel_parse, HAS_BABEL
if TYPE_CHECKING:
from .registry import UnitRegistry
from .util import ItMatrix, UnitsContainer
if HAS_BABEL:
import babel
Locale = babel.Locale
else:
Locale = TypeVar("Locale")
__JOIN_REG_EXP = re.compile(r"{\d*}")
FORMATTER = Callable[
[
Any,
],
str,
]
def _join(fmt: str, iterable: Iterable[Any]) -> str:
"""Join an iterable with the format specified in fmt.
The format can be specified in two ways:
- PEP3101 format with two replacement fields (eg. '{} * {}')
- The concatenating string (eg. ' * ')
Parameters
----------
fmt : str
iterable :
Returns
-------
str
"""
if not iterable:
return ""
if not __JOIN_REG_EXP.search(fmt):
return fmt.join(iterable)
miter = iter(iterable)
first = next(miter)
for val in miter:
ret = fmt.format(first, val)
first = ret
return first
_PRETTY_EXPONENTS = "⁰¹²³⁴⁵⁶⁷⁸⁹"
def _pretty_fmt_exponent(num: Number) -> str:
"""Format an number into a pretty printed exponent.
Parameters
----------
num : int
Returns
-------
str
"""
# unicode dot operator (U+22C5) looks like a superscript decimal
ret = f"{num:n}".replace("-", "⁻").replace(".", "\u22C5")
for n in range(10):
ret = ret.replace(str(n), _PRETTY_EXPONENTS[n])
return ret
#: _FORMATS maps format specifications to the corresponding argument set to
#: formatter().
_FORMATS: dict[str, dict[str, Any]] = {
"P": { # Pretty format.
"as_ratio": True,
"single_denominator": False,
"product_fmt": "·",
"division_fmt": "/",
"power_fmt": "{}{}",
"parentheses_fmt": "({})",
"exp_call": _pretty_fmt_exponent,
},
"L": { # Latex format.
"as_ratio": True,
"single_denominator": True,
"product_fmt": r" \cdot ",
"division_fmt": r"\frac[{}][{}]",
"power_fmt": "{}^[{}]",
"parentheses_fmt": r"\left({}\right)",
},
"Lx": {"siopts": "", "pm_fmt": " +- "}, # Latex format with SIunitx.
"H": { # HTML format.
"as_ratio": True,
"single_denominator": True,
"product_fmt": r" ",
"division_fmt": r"{}/{}",
"power_fmt": r"{}<sup>{}</sup>",
"parentheses_fmt": r"({})",
},
"": { # Default format.
"as_ratio": True,
"single_denominator": False,
"product_fmt": " * ",
"division_fmt": " / ",
"power_fmt": "{} ** {}",
"parentheses_fmt": r"({})",
},
"C": { # Compact format.
"as_ratio": True,
"single_denominator": False,
"product_fmt": "*", # TODO: Should this just be ''?
"division_fmt": "/",
"power_fmt": "{}**{}",
"parentheses_fmt": r"({})",
},
}
#: _FORMATTERS maps format names to callables doing the formatting
# TODO fix Callable typing
_FORMATTERS: dict[str, Callable] = {}
@register_unit_format("P")
def format_pretty(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
return formatter(
unit.items(),
as_ratio=True,
single_denominator=False,
product_fmt="·",
division_fmt="/",
power_fmt="{}{}",
parentheses_fmt="({})",
exp_call=_pretty_fmt_exponent,
**options,
)
[docs]def latex_escape(string: str) -> str:
"""
Prepend characters that have a special meaning in LaTeX with a backslash.
"""
return functools.reduce(
lambda s, m: re.sub(m[0], m[1], s),
(
(r"[\\]", r"\\textbackslash "),
(r"[~]", r"\\textasciitilde "),
(r"[\^]", r"\\textasciicircum "),
(r"([&%$#_{}])", r"\\\1"),
),
str(string),
)
@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()}
formatted = formatter(
preprocessed.items(),
as_ratio=True,
single_denominator=True,
product_fmt=r" \cdot ",
division_fmt=r"\frac[{}][{}]",
power_fmt="{}^[{}]",
parentheses_fmt=r"\left({}\right)",
**options,
)
return formatted.replace("[", "{").replace("]", "}")
@register_unit_format("Lx")
def format_latex_siunitx(
unit: UnitsContainer, registry: UnitRegistry, **options
) -> str:
if registry is None:
raise ValueError(
"Can't format as siunitx without a registry."
" This is usually triggered when formatting a instance"
' of the internal `UnitsContainer` with a spec of `"Lx"`'
" and might indicate a bug in `pint`."
)
formatted = siunitx_format_unit(unit, registry)
return rf"\si[]{{{formatted}}}"
@register_unit_format("H")
def format_html(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
return formatter(
unit.items(),
as_ratio=True,
single_denominator=True,
product_fmt=r" ",
division_fmt=r"{}/{}",
power_fmt=r"{}<sup>{}</sup>",
parentheses_fmt=r"({})",
**options,
)
@register_unit_format("D")
def format_default(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
return formatter(
unit.items(),
as_ratio=True,
single_denominator=False,
product_fmt=" * ",
division_fmt=" / ",
power_fmt="{} ** {}",
parentheses_fmt=r"({})",
**options,
)
@register_unit_format("C")
def format_compact(unit: UnitsContainer, registry: UnitRegistry, **options) -> str:
return formatter(
unit.items(),
as_ratio=True,
single_denominator=False,
product_fmt="*", # TODO: Should this just be ''?
division_fmt="/",
power_fmt="{}**{}",
parentheses_fmt=r"({})",
**options,
)
# Extract just the type from the specification mini-language: see
# http://docs.python.org/2/library/string.html#format-specification-mini-language
# We also add uS for uncertainties.
_BASIC_TYPES = frozenset("bcdeEfFgGnosxX%uS")
def _parse_spec(spec: str) -> str:
result = ""
for ch in reversed(spec):
if ch == "~" or ch in _BASIC_TYPES:
continue
elif ch in list(_FORMATTERS.keys()) + ["~"]:
if result:
raise ValueError("expected ':' after format specifier")
else:
result = ch
elif ch.isalpha():
raise ValueError("Unknown conversion specified " + ch)
else:
break
return result
def format_unit(unit, spec: str, registry=None, **options):
# registry may be None to allow formatting `UnitsContainer` objects
# in that case, the spec may not be "Lx"
if not unit:
if spec.endswith("%"):
return ""
else:
return "dimensionless"
if not spec:
spec = "D"
fmt = _FORMATTERS.get(spec)
if fmt is None:
raise ValueError(f"Unknown conversion specified: {spec}")
return fmt(unit, registry=registry, **options)
def extract_custom_flags(spec: str) -> str:
import re
if not spec:
return ""
# sort by length, with longer items first
known_flags = sorted(_FORMATTERS.keys(), key=len, reverse=True)
flag_re = re.compile("(" + "|".join(known_flags + ["~"]) + ")")
custom_flags = flag_re.findall(spec)
return "".join(custom_flags)
def remove_custom_flags(spec: str) -> str:
for flag in sorted(_FORMATTERS.keys(), key=len, reverse=True) + ["~"]:
if flag:
spec = spec.replace(flag, "")
return spec
def split_format(
spec: str, default: str, separate_format_defaults: bool = True
) -> tuple[str, str]:
mspec = remove_custom_flags(spec)
uspec = extract_custom_flags(spec)
default_mspec = remove_custom_flags(default)
default_uspec = extract_custom_flags(default)
if separate_format_defaults in (False, None):
# should we warn always or only if there was no explicit choice?
# Given that we want to eventually remove the flag again, I'd say yes?
if spec and separate_format_defaults is None:
if not uspec and default_uspec:
warnings.warn(
(
"The given format spec does not contain a unit formatter."
" Falling back to the builtin defaults, but in the future"
" the unit formatter specified in the `default_format`"
" attribute will be used instead."
),
DeprecationWarning,
)
if not mspec and default_mspec:
warnings.warn(
(
"The given format spec does not contain a magnitude formatter."
" Falling back to the builtin defaults, but in the future"
" the magnitude formatter specified in the `default_format`"
" attribute will be used instead."
),
DeprecationWarning,
)
elif not spec:
mspec, uspec = default_mspec, default_uspec
else:
mspec = mspec or default_mspec
uspec = uspec or default_uspec
return mspec, uspec
def vector_to_latex(vec: Iterable[Any], fmtfun: FORMATTER = ".2f".format) -> str:
return matrix_to_latex([vec], fmtfun)
def matrix_to_latex(matrix: ItMatrix, fmtfun: FORMATTER = ".2f".format) -> str:
ret: list[str] = []
for row in matrix:
ret += [" & ".join(fmtfun(f) for f in row)]
return r"\begin{pmatrix}%s\end{pmatrix}" % "\\\\ \n".join(ret)
def ndarray_to_latex_parts(
ndarr, fmtfun: FORMATTER = ".2f".format, dim: tuple[int, ...] = tuple()
):
if isinstance(fmtfun, str):
fmtfun = fmtfun.format
if ndarr.ndim == 0:
_ndarr = ndarr.reshape(1)
return [vector_to_latex(_ndarr, fmtfun)]
if ndarr.ndim == 1:
return [vector_to_latex(ndarr, fmtfun)]
if ndarr.ndim == 2:
return [matrix_to_latex(ndarr, fmtfun)]
else:
ret = []
if ndarr.ndim == 3:
header = ("arr[%s," % ",".join("%d" % d for d in dim)) + "%d,:,:]"
for elno, el in enumerate(ndarr):
ret += [header % elno + " = " + matrix_to_latex(el, fmtfun)]
else:
for elno, el in enumerate(ndarr):
ret += ndarray_to_latex_parts(el, fmtfun, dim + (elno,))
return ret
def ndarray_to_latex(
ndarr, fmtfun: FORMATTER = ".2f".format, dim: tuple[int, ...] = tuple()
) -> str:
return "\n".join(ndarray_to_latex_parts(ndarr, fmtfun, dim))