import re from .math_basis_points_types import RateUnit # Explicit [0-9]: Python's \d also matches other scripts' digits. DECIMAL = re.compile(r"^-?[0-9]+(\.[0-9]+)?\Z") MAX_SAFE = 9007199254740991 def _exponent(unit: str) -> int: """Powers of ten between each unit and basis points.""" if unit == "basis-points": return 0 if unit == "percent": return 2 if unit == "ratio": return 4 raise ValueError('unknown rate unit "%s"' % (unit,)) def convert_rate(value: str, from_unit: RateUnit, to_unit: RateUnit) -> str: """Convert a rate between percent, basis points and a ratio. The units differ by powers of ten, so this moves the decimal point in the text itself: exact for any input, with no float anywhere. """ if not isinstance(value, str) or not DECIMAL.match(value): raise ValueError('"%s" is not a decimal number' % (value,)) shift = _exponent(from_unit) - _exponent(to_unit) negative = value.startswith("-") body = value[1:] if negative else value point = body.find(".") digits = body if point < 0 else body[:point] + body[point + 1 :] position = (len(body) if point < 0 else point) + shift if position > len(digits): digits = digits + "0" * (position - len(digits)) if position < 0: digits = "0" * (-position) + digits position = 0 whole = digits[:position].lstrip("0") or "0" fraction = digits[position:].rstrip("0") text = whole + "." + fraction if fraction else whole return "-" + text if negative and text != "0" else text def to_basis_points(value: str, unit: RateUnit) -> int: """An integer number of basis points, refusing a rate that is not whole.""" text = convert_rate(value, unit, "basis-points") if "." in text: raise ValueError("%s %s is not a whole number of basis points" % (value, unit)) bp = int(text) if bp > MAX_SAFE or bp < -MAX_SAFE: raise ValueError("%s %s is too large for integer basis points" % (value, unit)) return bp def from_basis_points(basis_points: int, unit: RateUnit) -> str: """Render integer basis points in another unit.""" if isinstance(basis_points, bool) or not isinstance(basis_points, int): raise TypeError("basis_points must be an integer, received %r" % (basis_points,)) return convert_rate(str(basis_points), "basis-points", unit)