Source code for Stoner.tools.formatting

# -*- coding: utf-8 -*-
"""Functions for formatting data."""

__all__ = ["format_error", "format_val", "quantize", "tex_escape", "ordinal"]

import re
from typing import Optional, Any
from html import escape as html_escape
from numpy import log10, floor, abs, isnan, round  # pylint: disable=redefined-builtin
from ..compat import bytes2str
from ..core.Typing import Numeric, NumericArray

prefs = {
    "text": {
        3: "k",
        6: "M",
        9: "G",
        12: "T",
        15: "P",
        18: "E",
        21: "Z",
        24: "Y",
        -3: "m",
        -6: "u",
        -9: "n",
        -12: "p",
        -15: "f",
        -18: "a",
        -21: "z",
        -24: "y",
    },
    "latex": {
        3: "k",
        6: "M",
        9: "G",
        12: "T",
        15: "P",
        18: "E",
        21: "Z",
        24: "Y",
        -3: "m",
        -6: r"\mu",
        -9: "n",
        -12: "p",
        -15: "f",
        -18: "a",
        -21: "z",
        -24: "y",
    },
    "html": {
        3: "k",
        6: "M",
        9: "G",
        12: "T",
        15: "P",
        18: "E",
        21: "Z",
        24: "Y",
        -3: "m",
        -6: r"µ",
        -9: "n",
        -12: "p",
        -15: "f",
        -18: "a",
        -21: "z",
        -24: "y",
    },
}


[docs]def format_error(value: Numeric, error: Optional[Numeric] = None, **kargs: Any) -> str: r"""Format answer with the uncertaintly to 1sf and the value to no more sf's than the uncertainty. Args: value (float): The value to be formatted error (float): The uncertainty in the value Keyword Arguments: fmt (str): Specify the output format, options are: - "text" - plain text output - "latex" - latex output - "html" - html entities escape (bool): Specifies whether to escape the prefix and units for unprintable characters in non text formats (default False) mode (string): If "float" (default) the number is formatted as is, if "eng" the value and error is converted to the next samllest power of 1000 and the appropriate SI index appended. If mode is "sci" then a scientific, i.e. mantissa and exponent format is used. units (string): A suffix providing the units of the value. If si mode is used, then appropriate si prefixes are prepended to the units string. In LaTeX mode, the units string is embedded in \mathrm prefix (string): A prefix string that should be included before the value and error string. in LaTeX mode this is inside the math-mode markers, but not embedded in \mathrm. Returns: String containing the formatted number with the eorr to one s.f. and value to no more d.p. than the error. """ mode = kargs.get("mode", "float") units = kargs.get("units", "") prefix = kargs.get("prefix", "") latex = kargs.get("latex", False) fmt = kargs.get("fmt", "latex" if latex else "text") escape = kargs.get("escape", False) escape_func = {"latex": tex_escape, "html": html_escape}.get(fmt, lambda x: x) if error == 0.0 or isnan(error): # special case for zero uncertainty return format_val(value, **kargs) if escape: prefix = escape_func(prefix) units = escape_func(units) # Sort out special formatting for different modes if mode == "float": # Standard suffix_val = "" elif mode == "eng": # Use SI prefixes if -1 <= floor(log10(abs(value))) <= 3: v_mag = 0 else: v_mag = floor(log10(abs(value)) / 3.0) * 3.0 prefixes = prefs.get(fmt, prefs["text"]) if v_mag in prefixes: if fmt == "latex": suffix_val = rf"\,\mathrm{{{{{prefixes[v_mag]}}}}}" else: suffix_val = " " + prefixes[v_mag] value /= 10**v_mag error /= 10**v_mag else: # Implies 10^-3<x<10^3 suffix_val = "" elif mode == "sci": # Scientific mode - raise to common power of 10 v_mag = floor(log10(abs(value))) if fmt == "latex": suffix_val = rf"\times 10^{{{{{int(v_mag)}}}}}\," elif fmt == "html": suffix_val = f"&times; 10<sup>{int(v_mag)}</sup> " else: suffix_val = f"E{int(v_mag)} " value /= 10**v_mag error /= 10**v_mag else: # Bad mode raise RuntimeError(f"Unrecognised mode: {mode} in format_error") # Now do the rounding of the value based on error to 1 s.f. e2 = error u_mag = floor(log10(abs(error))) # work out the scale of the error error = round(error / 10**u_mag) * 10**u_mag # round the error, but this could round to 0.x0 u_mag = floor(log10(error)) # so go round the loop again error = round(e2 / 10**u_mag) * 10**u_mag # and get a new error magnitude value = round(value / 10**u_mag) * 10**u_mag u_mag = min(0, u_mag) # Force integer results to have no dp # Protect {} in units string units = units.replace("{", "{{").replace("}", "}}") prefix = prefix.replace("{", "{{").replace("}", "}}") if fmt == "latex": # Switch to latex math mode symbols val_fmt_str = rf"${prefix}{{:.{int(abs(u_mag))}f}}\pm " if units != "": suffix_fmt = rf"\mathrm{{{{{units}}}}}" else: suffix_fmt = "" suffix_fmt += "$" elif fmt == "html": # Switch to latex math mode symbols val_fmt_str = rf"{prefix}{{:.{int(abs(u_mag))}f}}&plusmin;" suffix_fmt = units else: # Plain text val_fmt_str = rf"{prefix}{{:.{int(abs(u_mag))}f}}+/-" suffix_fmt = units if u_mag < 0: # the error is less than 1, so con strain decimal places err_fmt_str = r"{:." + str(int(abs(u_mag))) + "f}" else: # We'll be converting it to an integer anyway err_fmt_str = r"{}" fmt_str = val_fmt_str + err_fmt_str + suffix_val + suffix_fmt if error >= 1.0: error = int(error) value = int(value) return fmt_str.format(value, error)
[docs]def format_val(value: Numeric, **kargs: Any) -> str: r"""Format a number as an SI quantity. Args: value(float): Value to format Keyword Arguments: fmt (str): Specify the output format, options are: - "text" - plain text output - "latex" - latex output - "html" - html entities escape (bool): Specifies whether to escape the prefix and units for unprintable characters in non text formats (default False) mode (string): If "float" (default) the number is formatted as is, if "eng" the value and error is converted to the next samllest power of 1000 and the appropriate SI index appended. If mode is "sci" then a scientific, i.e. mantissa and exponent format is used. units (string): A suffix providing the units of the value. If si mode is used, then appropriate si prefixes are prepended to the units string. In LaTeX mode, the units string is embedded in \mathrm prefix (string): A prefix string that should be included before the value and error string. in LaTeX mode this is inside the math-mode markers, but not embedded in \mathrm. Returns: (str): The formatted value. """ mode = kargs.pop("mode", "float") units = kargs.pop("units", "") prefix = kargs.pop("prefix", "") latex = kargs.pop("latex", False) fmt = kargs.pop("fmt", "latex" if latex else "text") places = kargs.pop("places", False) escape = kargs.pop("escape", False) escape_func = {"latex": tex_escape, "html": html_escape}.get(fmt, lambda x: x) if escape: prefix = escape_func(prefix) units = escape_func(units) if mode == "float": # Standard suffix_val = "" v_mag = floor(log10(abs(value))) elif mode == "eng": # Use SI prefixes v_mag = floor(log10(abs(value)) / 3.0) * 3.0 prefixes = prefs.get(fmt, prefs["text"]) if v_mag in prefixes: if fmt == "latex": suffix_val = rf"\mathrm{{{{{prefixes[v_mag]}}}}}" else: suffix_val = prefixes[v_mag] value /= 10**v_mag else: # Implies 10^-3<x<10^3 suffix_val = "" elif mode == "sci": # Scientific mode - raise to common power of 10 v_mag = floor(log10(abs(value))) if fmt == "latex": suffix_val = rf"\times 10^{{{{{int(v_mag)}}}}}" elif fmt == "html": suffix_val = f"&times; 10<sup>{int(v_mag)}</sup> " else: suffix_val = f"E{int(v_mag)} " value /= 10**v_mag else: # Bad mode raise RuntimeError(f"Unrecognised mode: {mode} in format_error") # Protect {} in units string units = units.replace("{", "{{").replace("}", "}}") prefix = prefix.replace("{", "{{").replace("}", "}}") if fmt == "latex": # Switch to latex math mode symbols val_fmt_str = rf"${prefix}{{}}" if units != "": suffix_fmt = rf"\mathrm{{{{{units}}}}}" else: suffix_fmt = "" suffix_fmt += "$" elif fmt == "html": # Switch to latex math mode symbols val_fmt_str = rf"{prefix}{{}}" suffix_fmt = units else: # Plain text val_fmt_str = rf"{prefix}{{}}" suffix_fmt = units fmt_str = val_fmt_str + suffix_val + suffix_fmt if places: value = round(value, places) ret = fmt_str.format(value) return ret
[docs]def quantize(number: NumericArray, quantum: Numeric) -> NumericArray: """Round a number to the nearest multiple of a quantum. Args: number (float,array): Number(s) to be rounded to the nearest qyuantum quantum (float): Quantum to round to Returns: number rounded to qunatum """ return round(number / quantum) * quantum
[docs]def tex_escape(text: str) -> str: """Escapes spacecial text characters in a string. Parameters: text (str): a plain text message Returns: the message escaped to appear correctly in LaTeX From `Stackoverflow <http://stackoverflow.com/questions/16259923/ how-can-i-escape-latex-special-characters-inside-django-templates>` """ conv = { "&": r"\&", "%": r"\%", "$": r"\$", "#": r"\#", "_": r"\_", "{": r"\{", "}": r"\}", "~": r"\textasciitilde{}", "^": r"\^{}", "\\": r"\textbackslash{}", "<": r"\textless", ">": r"\textgreater", } regex = re.compile("|".join(re.escape(bytes2str(key)) for key in sorted(conv.keys(), key=lambda item: -len(item)))) return regex.sub(lambda match: conv[match.group()], text)
[docs]def ordinal(value: int) -> str: """Format an integer into an ordinal string. Args: value (int): Number to be written as an ordinal string Return: (str): Ordinal String such as '1st','2nd' etc. """ if not isinstance(value, int): raise ValueError last_digit = value % 10 if value % 100 in [11, 12, 13]: suffix = "th" else: suffix = ["th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th"][last_digit] return f"{value}{suffix}"