1// Package luhndemo is a small gnoweb demo of the Luhn mod-10 checksum provided2// by the [p/moul/x/daily/luhn](/p/moul/x/daily/luhn/v0) library: put a number3// in the path and it tells you whether the check digit is right, and what it4// should have been.5//6// It contains no checksum logic of its own — validation, the check digit and7// the separator handling all come from the library. Read-only and stateless, so8// Render is fully deterministic.9package luhndemo1011import (12 "strconv"13 "strings"1415 "gno.land/p/moul/x/daily/luhn/v0"16)1718// samples are shown on the root page: a valid number, a typo'd one, and a19// transposition — the two error classes Luhn is designed to catch.20var samples = []struct {21 number string22 note string23}{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}2930// Render renders the checker for gnoweb.31//32// Render("") / Render("/") -> usage + worked samples33// Render("/<digits>") -> verdict for that number34func Render(path string) string {35 in := parseInput(path)3637 var b strings.Builder38 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")4142 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 }6061 b.WriteString("## `")62 b.WriteString(in)63 b.WriteString("`\n\n")6465 if luhn.Valid(in) {66 b.WriteString("**Valid** — the check digit matches.\n")67 return b.String()68 }6970 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}8990// 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 s98}99100// 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], true105 }106 }107 return "", false108}109110func verdict(ok bool) string {111 if ok {112 return "✅ valid"113 }114 return "❌ invalid"115}116Render(path string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.