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