romannum.gno
2.19 Kb · 96 lines
1// Package romannum is a pure port of the classic Roman-numeral converter kata:
2// ToRoman / FromRoman, valid for 1..3999. Deterministic — no time, randomness
3// or I/O — and free of any realm coupling, so it is reusable as a library.
4//
5// A live demo of this package (an interactive integer↔Roman converter) is at
6// [r/moul/x/daily/romannumdemo](/r/moul/x/daily/romannumdemo/v0).
7package romannum
8
9import (
10 "strconv"
11 "strings"
12)
13
14// romanUnits is the greedy subtractive-notation table, largest value first.
15var romanUnits = []struct {
16 val int
17 sym string
18}{
19 {1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
20 {100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
21 {10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"},
22}
23
24// ToRoman renders an integer in 1..3999 as a Roman numeral.
25// It panics if n is out of range.
26func ToRoman(n int) string {
27 if n < 1 || n > 3999 {
28 panic("romannum: out of range (want 1..3999): " + strconv.Itoa(n))
29 }
30 var b strings.Builder
31 for _, u := range romanUnits {
32 for n >= u.val {
33 b.WriteString(u.sym)
34 n -= u.val
35 }
36 }
37 return b.String()
38}
39
40// FromRoman parses a Roman numeral back to an integer.
41// It panics on any malformed input (e.g. "IIII", "IC", "VV").
42func FromRoman(s string) int {
43 n, ok := parseRoman(s)
44 if !ok {
45 panic("romannum: invalid roman numeral: " + s)
46 }
47 return n
48}
49
50// charVal maps a single Roman digit to its value, or 0 if unknown.
51func charVal(c byte) int {
52 switch c {
53 case 'I':
54 return 1
55 case 'V':
56 return 5
57 case 'X':
58 return 10
59 case 'L':
60 return 50
61 case 'C':
62 return 100
63 case 'D':
64 return 500
65 case 'M':
66 return 1000
67 }
68 return 0
69}
70
71// parseRoman is the pure, panic-free core used by FromRoman.
72// It returns (value, true) only for a canonical numeral: it accepts input
73// iff ToRoman(value) reproduces it exactly, which rejects malformed forms.
74func parseRoman(s string) (int, bool) {
75 s = strings.ToUpper(strings.TrimSpace(s))
76 if s == "" {
77 return 0, false
78 }
79 total, prev := 0, 0
80 for i := len(s) - 1; i >= 0; i-- {
81 v := charVal(s[i])
82 if v == 0 {
83 return 0, false
84 }
85 if v < prev {
86 total -= v
87 } else {
88 total += v
89 prev = v
90 }
91 }
92 if total < 1 || total > 3999 || ToRoman(total) != s {
93 return 0, false
94 }
95 return total, true
96}