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

romannum.gno

4.68 Kb · 175 lines
  1// Package romannum is an on-chain port of the classic Roman-numeral converter
  2// kata: ToRoman/FromRoman. Pure and deterministic — no time, randomness or I/O.
  3package romannum
  4
  5import (
  6	"strconv"
  7	"strings"
  8)
  9
 10// romanUnits is the greedy subtractive-notation table, largest value first.
 11var romanUnits = []struct {
 12	val int
 13	sym string
 14}{
 15	{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
 16	{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
 17	{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"},
 18}
 19
 20// ToRoman renders an integer in 1..3999 as a Roman numeral.
 21// It panics if n is out of range.
 22func ToRoman(n int) string {
 23	if n < 1 || n > 3999 {
 24		panic("romannum: out of range (want 1..3999): " + strconv.Itoa(n))
 25	}
 26	var b strings.Builder
 27	for _, u := range romanUnits {
 28		for n >= u.val {
 29			b.WriteString(u.sym)
 30			n -= u.val
 31		}
 32	}
 33	return b.String()
 34}
 35
 36// FromRoman parses a Roman numeral back to an integer.
 37// It panics on any malformed input (e.g. "IIII", "IC", "VV").
 38func FromRoman(s string) int {
 39	n, ok := parseRoman(s)
 40	if !ok {
 41		panic("romannum: invalid roman numeral: " + s)
 42	}
 43	return n
 44}
 45
 46// charVal maps a single Roman digit to its value, or 0 if unknown.
 47func charVal(c byte) int {
 48	switch c {
 49	case 'I':
 50		return 1
 51	case 'V':
 52		return 5
 53	case 'X':
 54		return 10
 55	case 'L':
 56		return 50
 57	case 'C':
 58		return 100
 59	case 'D':
 60		return 500
 61	case 'M':
 62		return 1000
 63	}
 64	return 0
 65}
 66
 67// parseRoman is the pure, panic-free core used by FromRoman and Render.
 68// It returns (value, true) only for a canonical numeral: it accepts input
 69// iff ToRoman(value) reproduces it exactly, which rejects malformed forms.
 70func parseRoman(s string) (int, bool) {
 71	s = strings.ToUpper(strings.TrimSpace(s))
 72	if s == "" {
 73		return 0, false
 74	}
 75	total, prev := 0, 0
 76	for i := len(s) - 1; i >= 0; i-- {
 77		v := charVal(s[i])
 78		if v == 0 {
 79			return 0, false
 80		}
 81		if v < prev {
 82			total -= v
 83		} else {
 84			total += v
 85			prev = v
 86		}
 87	}
 88	if total < 1 || total > 3999 || ToRoman(total) != s {
 89		return 0, false
 90	}
 91	return total, true
 92}
 93
 94// examples are the classic milestones shown by the root Render.
 95var examples = []int{1, 4, 9, 14, 40, 49, 90, 400, 900, 1994, 2024, 3888, 3999}
 96
 97// Render drives gnoweb:
 98//   - ""        or "/"          → a table of example conversions
 99//   - "/<n>"                    → integer → Roman
100//   - "/r/<roman>"              → Roman → integer
101func Render(path string) string {
102	arg := strings.Trim(strings.TrimSpace(path), "/")
103
104	switch {
105	case arg == "":
106		return renderTable()
107	case strings.HasPrefix(strings.ToLower(arg), "r/"):
108		return renderFromRoman(arg[2:])
109	default:
110		return renderToRoman(arg)
111	}
112}
113
114func renderToRoman(arg string) string {
115	n, err := strconv.Atoi(strings.TrimSpace(arg))
116	if err != nil {
117		return "# Roman numeral converter\n\nCould not parse `" + arg +
118			"` as an integer.\n\n" + usage()
119	}
120	if n < 1 || n > 3999 {
121		return "# Roman numeral converter\n\n`" + strconv.Itoa(n) +
122			"` is out of range — Roman numerals here cover **1..3999**.\n\n" + usage()
123	}
124	var b strings.Builder
125	b.WriteString("# " + strconv.Itoa(n) + " → " + ToRoman(n) + "\n\n")
126	b.WriteString("| decimal | roman |\n|---:|:---|\n")
127	b.WriteString("| " + strconv.Itoa(n) + " | **" + ToRoman(n) + "** |\n\n")
128	if n > 1 {
129		b.WriteString("[« " + strconv.Itoa(n-1) + "](/r/moul/x/daily/romannum/v1:" +
130			strconv.Itoa(n-1) + ") · ")
131	}
132	if n < 3999 {
133		b.WriteString("[" + strconv.Itoa(n+1) + " »](/r/moul/x/daily/romannum/v1:" +
134			strconv.Itoa(n+1) + ")")
135	}
136	b.WriteString("\n\n" + usage())
137	return b.String()
138}
139
140func renderFromRoman(arg string) string {
141	arg = strings.TrimSpace(arg)
142	n, ok := parseRoman(arg)
143	if !ok {
144		return "# Roman numeral converter\n\n`" + arg +
145			"` is not a valid Roman numeral (1..3999).\n\n" + usage()
146	}
147	up := strings.ToUpper(arg)
148	var b strings.Builder
149	b.WriteString("# " + up + " → " + strconv.Itoa(n) + "\n\n")
150	b.WriteString("| roman | decimal |\n|:---|---:|\n")
151	b.WriteString("| **" + up + "** | " + strconv.Itoa(n) + " |\n\n")
152	b.WriteString(usage())
153	return b.String()
154}
155
156func renderTable() string {
157	var b strings.Builder
158	b.WriteString("# Roman numeral converter\n\n")
159	b.WriteString("A tiny on-chain port of the classic `ToRoman` / `FromRoman` kata. ")
160	b.WriteString("Pure and deterministic.\n\n")
161	b.WriteString("## Examples\n\n")
162	b.WriteString("| decimal | roman |\n|---:|:---|\n")
163	for _, n := range examples {
164		b.WriteString("| [" + strconv.Itoa(n) + "](/r/moul/x/daily/romannum/v1:" +
165			strconv.Itoa(n) + ") | " + ToRoman(n) + " |\n")
166	}
167	b.WriteString("\n" + usage())
168	return b.String()
169}
170
171func usage() string {
172	return "## Try it\n\n" +
173		"- `:/<n>` — integer → Roman, e.g. `:/2024` → " + ToRoman(2024) + "\n" +
174		"- `:/r/<roman>` — Roman → integer, e.g. `:/r/MMXXIV` → 2024\n"
175}