Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

luhn.gno

3.03 Kb · 106 lines
  1// Package luhn implements the Luhn mod-10 checksum (Hans Peter Luhn, 1954) as
  2// a pure, reusable package: the check used by credit-card numbers, IMEIs and
  3// many national ID schemes to catch typos and single-digit transpositions.
  4//
  5// It is a *checksum*, not a security primitive: it detects every single-digit
  6// error and almost every adjacent transposition, but it is trivial to forge and
  7// says nothing about whether an identifier actually exists. Never use it to
  8// authorize anything on-chain.
  9//
 10// No clocks, no randomness, no chain imports — same input, same output.
 11//
 12// A live demo of this package is at
 13// [r/moul/x/daily/luhndemo](/r/moul/x/daily/luhndemo/v0).
 14package luhn
 15
 16import (
 17	"strings"
 18)
 19
 20// MaxLen bounds an input so validation gas stays predictable.
 21const MaxLen = 64
 22
 23// Valid reports whether s carries a correct Luhn check digit.
 24//
 25// Spaces and hyphens are ignored, so "4539 1488 0343 6467" and
 26// "4539-1488-0343-6467" both work. Any other non-digit makes it false, as does
 27// an empty/1-digit input or one longer than MaxLen. A string of all zeros is
 28// technically Luhn-valid and is accepted — reject it in the caller if your
 29// domain needs to.
 30func Valid(s string) bool {
 31	digits, ok := clean(s)
 32	if !ok || len(digits) < 2 {
 33		return false
 34	}
 35	return sum(digits)%10 == 0
 36}
 37
 38// CheckDigit returns the digit that must be appended to payload to make the
 39// whole string Luhn-valid, and whether the payload was usable.
 40func CheckDigit(payload string) (int, bool) {
 41	digits, ok := clean(payload)
 42	if !ok || len(digits) == 0 {
 43		return 0, false
 44	}
 45	// The check digit sits in the "doubled" position of the final number, so
 46	// compute the sum as if a 0 had already been appended.
 47	total := sum(append(digits, 0))
 48	return (10 - total%10) % 10, true
 49}
 50
 51// Append returns payload with its Luhn check digit appended (digits only,
 52// separators stripped), and whether the payload was usable.
 53func Append(payload string) (string, bool) {
 54	d, ok := CheckDigit(payload)
 55	if !ok {
 56		return "", false
 57	}
 58	digits, _ := clean(payload)
 59	var b strings.Builder
 60	for _, x := range digits {
 61		b.WriteByte(byte('0' + x))
 62	}
 63	b.WriteByte(byte('0' + d))
 64	return b.String(), true
 65}
 66
 67// clean turns s into its digit values, ignoring spaces and hyphens. ok is false
 68// if any other character appears or the input exceeds MaxLen.
 69func clean(s string) ([]int, bool) {
 70	if len(s) > MaxLen {
 71		return nil, false
 72	}
 73	digits := make([]int, 0, len(s))
 74	for i := 0; i < len(s); i++ {
 75		c := s[i]
 76		switch {
 77		case c >= '0' && c <= '9':
 78			digits = append(digits, int(c-'0'))
 79		case c == ' ' || c == '-':
 80			// separator: ignore
 81		default:
 82			return nil, false
 83		}
 84	}
 85	return digits, true
 86}
 87
 88// sum computes the Luhn total: walking right to left, every second digit is
 89// doubled, and a double of 10 or more has 9 subtracted (equivalent to adding
 90// its two decimal digits).
 91func sum(digits []int) int {
 92	total := 0
 93	double := false
 94	for i := len(digits) - 1; i >= 0; i-- {
 95		d := digits[i]
 96		if double {
 97			d *= 2
 98			if d > 9 {
 99				d -= 9
100			}
101		}
102		total += d
103		double = !double
104	}
105	return total
106}