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}