Functional Weave
Code in Python

text.integer-to-words@1.0.0

impl/python.py

2,549 bytes · the Python implementation · view raw

from typing import Any, List

#: Largest integer a JavaScript number holds exactly; beyond it the three
#: languages disagree about what number was passed.
MAX_MAGNITUDE = 9007199254740991

_ONES = [
    "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
    "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
    "seventeen", "eighteen", "nineteen",
]
_TENS = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]

#: Short scale, as used in the UK since 1974: a billion is 10^9.
_SCALES = ["", "thousand", "million", "billion", "trillion", "quadrillion"]


def _below_hundred(n: int) -> str:
    if n < 20:
        return _ONES[n]
    unit = n % 10
    return _TENS[n // 10] if unit == 0 else "%s-%s" % (_TENS[n // 10], _ONES[unit])


def _group(n: int) -> str:
    """One group of three digits, 1 to 999."""
    hundreds, rest = divmod(n, 100)
    if hundreds == 0:
        return _below_hundred(rest)
    head = "%s hundred" % _ONES[hundreds]
    return head if rest == 0 else "%s and %s" % (head, _below_hundred(rest))


def _whole(value: Any) -> bool:
    # bool is an int in Python; True is not the number one here.
    return isinstance(value, int) and not isinstance(value, bool)


def integer_to_words(value: int) -> str:
    """Spell out a whole number in British English words, cheque style:
    1234 is "one thousand two hundred and thirty-four".
    """
    if not _whole(value):
        raise TypeError("integerToWords needs a whole number, received %r" % (value,))
    if value > MAX_MAGNITUDE or value < -MAX_MAGNITUDE:
        raise ValueError(
            "%d is out of range: integerToWords handles magnitudes up to %d" % (value, MAX_MAGNITUDE)
        )
    if value == 0:
        return "zero"

    # Groups of three, least significant first.
    groups: List[int] = []
    remaining = abs(value)
    while remaining > 0:
        groups.append(remaining % 1000)
        remaining //= 1000

    words: List[str] = []
    for i in range(len(groups) - 1, -1, -1):
        g = groups[i]
        if g == 0:
            continue
        text = _group(g)
        if i > 0:
            text += " " + _SCALES[i]
        elif len(groups) > 1 and g < 100:
            # British usage: "one thousand and one", "one million and fifty".
            # Only the final group gets it, and only without hundreds of its own.
            text = "and " + text
        words.append(text)

    body = " ".join(words)
    return "minus " + body if value < 0 else body