import math from typing import Tuple from .math_round_float import round_float from .text_format_decimal import format_decimal # yocto to yotta, as d3-format writes them (micro is U+00B5). _PREFIXES = ["y", "z", "a", "f", "p", "n", "ยต", "m", "", "k", "M", "G", "T", "P", "E", "Z", "Y"] _THOUSANDS = [1.0, 1e3, 1e6, 1e9, 1e12, 1e15, 1e18, 1e21, 1e24] _TENS = [1.0, 10.0, 100.0] def _scale_and_round(value: float, group: int, places: int) -> Tuple[float, int]: scaled = value / _THOUSANDS[group] if group >= 0 else value * _THOUSANDS[-group] if places < 0: unit = _TENS[-places] return round_float(scaled / unit, 0) * unit, 0 p = 12 if places > 12 else places return round_float(scaled, p), p def format_si(value: float, significant: int) -> str: """A number with an SI prefix and ``significant`` significant digits, trailing zeros dropped: 1200 is "1.2k", 0.0015 is "1.5m".""" if isinstance(significant, bool) or not isinstance(significant, int) or significant < 1 or significant > 15: raise ValueError("significant must be a whole number from 1 to 15, received %r" % (significant,)) if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value): raise ValueError("value must be a finite number, received %r" % (value,)) value = float(value) if value == 0: return "0" a = abs(value) exponent = 0 if a >= 1: p = 1.0 while p * 10 <= a: p *= 10 exponent += 1 else: q = 1.0 while a * q < 1: q *= 10 exponent -= 1 group = exponent // 3 group = max(-8, min(8, group)) rounded, places = _scale_and_round(value, group, significant - 1 - (exponent - 3 * group)) if abs(rounded) >= 1000 and group < 8: group += 1 rounded, places = _scale_and_round(value, group, significant - 1) return format_decimal(rounded, places, True, "") + _PREFIXES[group + 8]