Functional Weave
Code in TypeScript

validation.luhn@1.0.0

impl/python.py

2,577 bytes · the Python implementation · view raw

from typing import List


def is_luhn(value: str) -> bool:
    """The Luhn (mod 10) checksum, as used by payment card numbers, IMEIs,
    SIM ICCIDs and a long tail of national identifiers.

    Luhn is a *transcription* check. It catches a single mistyped digit and
    most adjacent transpositions before a request leaves the building. It says
    nothing about whether the card exists, is open, belongs to the person
    typing it, or has any money behind it. Only the acquirer can answer that,
    so never render "card valid" on the strength of this function.
    """
    # Non-strings are answered rather than raised at: "is this valid?" is a
    # question, and "no" is a complete answer to it.
    if not isinstance(value, str):
        return False

    digits: List[int] = []
    for ch in value:
        # Card numbers are printed and pasted in four-digit groups, so
        # tolerating the separators here saves every caller the same strip.
        if ch == " " or ch == "-":
            continue
        if ch < "0" or ch > "9":
            return False
        digits.append(ord(ch) - 48)

    # A lone digit satisfies the arithmetic whenever it is 0, which would make
    # this function a rubber stamp for a stray keystroke. No identifier scheme
    # issues one-digit numbers, so two is the honest floor.
    if len(digits) < 2:
        return False

    total = 0
    for position, d in enumerate(reversed(digits)):
        if position % 2 == 1:
            d *= 2
            # Doubling can only reach 18, so subtracting 9 is the same as
            # summing the two decimal digits, without the string round trip.
            if d > 9:
                d -= 9
        total += d
    return total % 10 == 0


def luhn_check_digit(prefix: str) -> int:
    """The digit that would make ``prefix`` pass the Luhn check.

    Useful for generating test data and for completing a partially known
    number; it is the inverse of the check above, not a second opinion on it.
    Returns -1 when the prefix is not usable digits at all.
    """
    digits: List[int] = []
    for ch in prefix:
        if ch == " " or ch == "-":
            continue
        if ch < "0" or ch > "9":
            return -1
        digits.append(ord(ch) - 48)
    if not digits:
        return -1

    total = 0
    # The check digit will occupy position 0, so the prefix starts at 1.
    for position, d in enumerate(reversed(digits)):
        if position % 2 == 0:
            d *= 2
            if d > 9:
                d -= 9
        total += d
    return (10 - (total % 10)) % 10