luhndemo.gno
3.43 Kb · 114 lines
1// Package luhndemo is a small gnoweb demo of the Luhn mod-10 checksum provided
2// by the [p/moul/x/daily/luhn](/p/moul/x/daily/luhn/v0) library: put a number
3// in the path and it tells you whether the check digit is right, and what it
4// should have been.
5//
6// It contains no checksum logic of its own — validation, the check digit and
7// the separator handling all come from the library. Read-only and stateless, so
8// Render is fully deterministic.
9package luhndemo
10
11import (
12 "strconv"
13 "strings"
14
15 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/md/v0"
16 "gno.land/p/g1magld5ptgzaktqcexxq0pgq5lmdf0kz6re8hlv/x/daily/luhn/v0"
17)
18
19// samples are shown on the root page: a valid number, a typo'd one, and a
20// transposition — the two error classes Luhn is designed to catch.
21var samples = []struct {
22 number string
23 note string
24}{
25 {"79927398713", "the textbook example"},
26 {"79927398710", "same number, wrong check digit"},
27 {"4539148803436467", "a test card number"},
28 {"4539148803436647", "same card, two digits swapped"},
29}
30
31// Render renders the checker for gnoweb.
32//
33// Render("") / Render("/") -> usage + worked samples
34// Render("/<digits>") -> verdict for that number
35func Render(path string) string {
36 in := parseInput(path)
37
38 var b strings.Builder
39 b.WriteString("# Luhn Checksum\n\n")
40 b.WriteString("The mod-10 check behind card numbers and IMEIs, demoing the ")
41 b.WriteString("[`p/moul/x/daily/luhn`](/p/moul/x/daily/luhn/v0) library.\n\n")
42
43 if in == "" {
44 b.WriteString("Append a number to the path to check it — spaces and hyphens are ignored.\n\n")
45 b.WriteString("## Samples\n\n")
46 b.WriteString("| number | valid? | note |\n|---|---|---|\n")
47 for _, s := range samples {
48 b.WriteString("| `")
49 b.WriteString(s.number)
50 b.WriteString("` | ")
51 b.WriteString(verdict(luhn.Valid(s.number)))
52 b.WriteString(" | ")
53 b.WriteString(s.note)
54 b.WriteString(" |\n")
55 }
56 b.WriteString("\n> Luhn is a **typo check, not a security check**: it catches every ")
57 b.WriteString("single-digit error and nearly every swap of adjacent digits, but ")
58 b.WriteString("anyone can compute a valid number. Never authorize anything with it.\n")
59 return b.String()
60 }
61
62 b.WriteString("## " + md.InlineCode(in) + "\n\n")
63
64 if luhn.Valid(in) {
65 b.WriteString("**Valid** — the check digit matches.\n")
66 return b.String()
67 }
68
69 b.WriteString("**Not valid.**\n\n")
70 // Offer the fix: treat everything but the last digit as the payload.
71 if payload, ok := trimLast(in); ok {
72 if d, ok2 := luhn.CheckDigit(payload); ok2 {
73 b.WriteString("Keeping the leading digits, the check digit should be **")
74 b.WriteString(strconv.Itoa(d))
75 b.WriteString("**")
76 if fixed, ok3 := luhn.Append(payload); ok3 {
77 b.WriteString(" — i.e. `")
78 b.WriteString(fixed)
79 b.WriteString("`")
80 }
81 b.WriteString(".\n")
82 return b.String()
83 }
84 }
85 b.WriteString("It has no usable digits to check.\n")
86 return b.String()
87}
88
89// parseInput extracts the first path segment.
90func parseInput(path string) string {
91 s := strings.TrimSpace(path)
92 s = strings.TrimPrefix(s, "/")
93 if i := strings.IndexByte(s, '/'); i >= 0 {
94 s = s[:i]
95 }
96 return s
97}
98
99// trimLast drops the final digit, ignoring separators, to recover the payload.
100func trimLast(s string) (string, bool) {
101 for i := len(s) - 1; i >= 0; i-- {
102 if s[i] >= '0' && s[i] <= '9' {
103 return s[:i], true
104 }
105 }
106 return "", false
107}
108
109func verdict(ok bool) string {
110 if ok {
111 return "✅ valid"
112 }
113 return "❌ invalid"
114}