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