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

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}