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}