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