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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}