Functional Weave
Code in Rust

math.basis-points@1.0.0

impl/python.py

2,399 bytes · the Python implementation · view raw

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)