docs for muutils v0.6.17
View Source on GitHub

# muutils.misc.numerical

View Source
View on GitHub

      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:
View Source
View on GitHub

    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:
View Source
View on GitHub

     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