docs for muutils v0.8.5
View Source on GitHub

muutils.misc.numerical


  1from __future__ import annotations
  2
  3
  4_SHORTEN_MAP: dict[int | float, str] = {
  5    1e3: "K",
  6    1e6: "M",
  7    1e9: "B",
  8    1e12: "t",
  9    1e15: "q",
 10    1e18: "Q",
 11}
 12
 13_SHORTEN_TUPLES: list[tuple[int | float, str]] = sorted(
 14    ((val, suffix) for val, suffix in _SHORTEN_MAP.items()),
 15    key=lambda x: -x[0],
 16)
 17
 18
 19_REVERSE_SHORTEN_MAP: dict[str, int | float] = {v: k for k, v in _SHORTEN_MAP.items()}
 20
 21
 22def shorten_numerical_to_str(
 23    num: int | float,
 24    small_as_decimal: bool = True,
 25    precision: int = 1,
 26) -> str:
 27    """shorten a large numerical value to a string
 28    1234 -> 1K
 29
 30    precision guaranteed to 1 in 10, but can be higher. reverse of `str_to_numeric`
 31    """
 32
 33    # small values are returned as is
 34    num_abs: float = abs(num)
 35    if num_abs < 1e3:
 36        return str(num)
 37
 38    # iterate over suffixes from largest to smallest
 39    for i, (val, suffix) in enumerate(_SHORTEN_TUPLES):
 40        if num_abs > val or i == len(_SHORTEN_TUPLES) - 1:
 41            if (num_abs < val * 10) and small_as_decimal:
 42                return f"{num / val:.{precision}f}{suffix}"
 43            elif num_abs < val * 1e3:
 44                return f"{int(round(num / val))}{suffix}"
 45
 46    return f"{num:.{precision}f}"
 47
 48
 49def str_to_numeric(
 50    quantity: str,
 51    mapping: None | bool | dict[str, int | float] = True,
 52) -> int | float:
 53    """Convert a string representing a quantity to a numeric value.
 54
 55    The string can represent an integer, python float, fraction, or shortened via `shorten_numerical_to_str`.
 56
 57    # Examples:
 58    ```
 59    >>> str_to_numeric("5")
 60    5
 61    >>> str_to_numeric("0.1")
 62    0.1
 63    >>> str_to_numeric("1/5")
 64    0.2
 65    >>> str_to_numeric("-1K")
 66    -1000.0
 67    >>> str_to_numeric("1.5M")
 68    1500000.0
 69    >>> str_to_numeric("1.2e2")
 70    120.0
 71    ```
 72
 73    """
 74
 75    # check is string
 76    if not isinstance(quantity, str):
 77        raise TypeError(
 78            f"quantity must be a string, got '{type(quantity) = }' '{quantity = }'"
 79        )
 80
 81    # basic int conversion
 82    try:
 83        quantity_int: int = int(quantity)
 84        return quantity_int
 85    except ValueError:
 86        pass
 87
 88    # basic float conversion
 89    try:
 90        quantity_float: float = float(quantity)
 91        return quantity_float
 92    except ValueError:
 93        pass
 94
 95    # mapping
 96    _mapping: dict[str, int | float]
 97    if mapping is True or mapping is None:
 98        _mapping = _REVERSE_SHORTEN_MAP
 99    else:
100        _mapping = mapping  # type: ignore[assignment]
101
102    quantity_original: str = quantity
103
104    quantity = quantity.strip()
105
106    result: int | float
107    multiplier: int | float = 1
108
109    # detect if it has a suffix
110    suffixes_detected: list[bool] = [suffix in quantity for suffix in _mapping]
111    n_suffixes_detected: int = sum(suffixes_detected)
112    if n_suffixes_detected == 0:
113        # no suffix
114        pass
115    elif n_suffixes_detected == 1:
116        # find multiplier
117        for suffix, mult in _mapping.items():
118            if quantity.endswith(suffix):
119                # remove suffix, store multiplier, and break
120                quantity = quantity[: -len(suffix)].strip()
121                multiplier = mult
122                break
123        else:
124            raise ValueError(f"Invalid suffix in {quantity_original}")
125    else:
126        # multiple suffixes
127        raise ValueError(f"Multiple suffixes detected in {quantity_original}")
128
129    # fractions
130    if "/" in quantity:
131        try:
132            assert quantity.count("/") == 1, "too many '/'"
133            # split and strip
134            num, den = quantity.split("/")
135            num = num.strip()
136            den = den.strip()
137            num_sign: int = 1
138            # negative numbers
139            if num.startswith("-"):
140                num_sign = -1
141                num = num[1:]
142            # assert that both are digits
143            assert (
144                num.isdigit() and den.isdigit()
145            ), "numerator and denominator must be digits"
146            # return the fraction
147            result = num_sign * (
148                int(num) / int(den)
149            )  # this allows for fractions with suffixes, which is weird, but whatever
150        except AssertionError as e:
151            raise ValueError(f"Invalid fraction {quantity_original}: {e}") from e
152
153    # decimals
154    else:
155        try:
156            result = int(quantity)
157        except ValueError:
158            try:
159                result = float(quantity)
160            except ValueError as e:
161                raise ValueError(
162                    f"Invalid quantity {quantity_original} ({quantity})"
163                ) from e
164
165    return result * multiplier

def shorten_numerical_to_str( num: int | float, small_as_decimal: bool = True, precision: int = 1) -> str:
23def shorten_numerical_to_str(
24    num: int | float,
25    small_as_decimal: bool = True,
26    precision: int = 1,
27) -> str:
28    """shorten a large numerical value to a string
29    1234 -> 1K
30
31    precision guaranteed to 1 in 10, but can be higher. reverse of `str_to_numeric`
32    """
33
34    # small values are returned as is
35    num_abs: float = abs(num)
36    if num_abs < 1e3:
37        return str(num)
38
39    # iterate over suffixes from largest to smallest
40    for i, (val, suffix) in enumerate(_SHORTEN_TUPLES):
41        if num_abs > val or i == len(_SHORTEN_TUPLES) - 1:
42            if (num_abs < val * 10) and small_as_decimal:
43                return f"{num / val:.{precision}f}{suffix}"
44            elif num_abs < val * 1e3:
45                return f"{int(round(num / val))}{suffix}"
46
47    return f"{num:.{precision}f}"

shorten a large numerical value to a string 1234 -> 1K

precision guaranteed to 1 in 10, but can be higher. reverse of str_to_numeric

def str_to_numeric( quantity: str, mapping: None | bool | dict[str, int | float] = True) -> int | float:
 50def str_to_numeric(
 51    quantity: str,
 52    mapping: None | bool | dict[str, int | float] = True,
 53) -> int | float:
 54    """Convert a string representing a quantity to a numeric value.
 55
 56    The string can represent an integer, python float, fraction, or shortened via `shorten_numerical_to_str`.
 57
 58    # Examples:
 59    ```
 60    >>> str_to_numeric("5")
 61    5
 62    >>> str_to_numeric("0.1")
 63    0.1
 64    >>> str_to_numeric("1/5")
 65    0.2
 66    >>> str_to_numeric("-1K")
 67    -1000.0
 68    >>> str_to_numeric("1.5M")
 69    1500000.0
 70    >>> str_to_numeric("1.2e2")
 71    120.0
 72    ```
 73
 74    """
 75
 76    # check is string
 77    if not isinstance(quantity, str):
 78        raise TypeError(
 79            f"quantity must be a string, got '{type(quantity) = }' '{quantity = }'"
 80        )
 81
 82    # basic int conversion
 83    try:
 84        quantity_int: int = int(quantity)
 85        return quantity_int
 86    except ValueError:
 87        pass
 88
 89    # basic float conversion
 90    try:
 91        quantity_float: float = float(quantity)
 92        return quantity_float
 93    except ValueError:
 94        pass
 95
 96    # mapping
 97    _mapping: dict[str, int | float]
 98    if mapping is True or mapping is None:
 99        _mapping = _REVERSE_SHORTEN_MAP
100    else:
101        _mapping = mapping  # type: ignore[assignment]
102
103    quantity_original: str = quantity
104
105    quantity = quantity.strip()
106
107    result: int | float
108    multiplier: int | float = 1
109
110    # detect if it has a suffix
111    suffixes_detected: list[bool] = [suffix in quantity for suffix in _mapping]
112    n_suffixes_detected: int = sum(suffixes_detected)
113    if n_suffixes_detected == 0:
114        # no suffix
115        pass
116    elif n_suffixes_detected == 1:
117        # find multiplier
118        for suffix, mult in _mapping.items():
119            if quantity.endswith(suffix):
120                # remove suffix, store multiplier, and break
121                quantity = quantity[: -len(suffix)].strip()
122                multiplier = mult
123                break
124        else:
125            raise ValueError(f"Invalid suffix in {quantity_original}")
126    else:
127        # multiple suffixes
128        raise ValueError(f"Multiple suffixes detected in {quantity_original}")
129
130    # fractions
131    if "/" in quantity:
132        try:
133            assert quantity.count("/") == 1, "too many '/'"
134            # split and strip
135            num, den = quantity.split("/")
136            num = num.strip()
137            den = den.strip()
138            num_sign: int = 1
139            # negative numbers
140            if num.startswith("-"):
141                num_sign = -1
142                num = num[1:]
143            # assert that both are digits
144            assert (
145                num.isdigit() and den.isdigit()
146            ), "numerator and denominator must be digits"
147            # return the fraction
148            result = num_sign * (
149                int(num) / int(den)
150            )  # this allows for fractions with suffixes, which is weird, but whatever
151        except AssertionError as e:
152            raise ValueError(f"Invalid fraction {quantity_original}: {e}") from e
153
154    # decimals
155    else:
156        try:
157            result = int(quantity)
158        except ValueError:
159            try:
160                result = float(quantity)
161            except ValueError as e:
162                raise ValueError(
163                    f"Invalid quantity {quantity_original} ({quantity})"
164                ) from e
165
166    return result * multiplier

Convert a string representing a quantity to a numeric value.

The string can represent an integer, python float, fraction, or shortened via shorten_numerical_to_str.

Examples:

>>> str_to_numeric("5")
5
>>> str_to_numeric("0.1")
0.1
>>> str_to_numeric("1/5")
0.2
>>> str_to_numeric("-1K")
-1000.0
>>> str_to_numeric("1.5M")
1500000.0
>>> str_to_numeric("1.2e2")
120.0