Search Apps Documentation Source Content File Folder Download Copy Actions Download 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}