1// Package romannum is a pure port of the classic Roman-numeral converter kata:2// ToRoman / FromRoman, valid for 1..3999. Deterministic — no time, randomness3// 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 at6// [r/moul/x/daily/romannumdemo](/r/moul/x/daily/romannumdemo/v0).7package romannum89import (10 "strconv"11 "strings"12)1314// romanUnits is the greedy subtractive-notation table, largest value first.15var romanUnits = []struct {16 val int17 sym string18}{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}2324// 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.Builder31 for _, u := range romanUnits {32 for n >= u.val {33 b.WriteString(u.sym)34 n -= u.val35 }36 }37 return b.String()38}3940// 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 n48}4950// 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 155 case 'V':56 return 557 case 'X':58 return 1059 case 'L':60 return 5061 case 'C':62 return 10063 case 'D':64 return 50065 case 'M':66 return 100067 }68 return 069}7071// parseRoman is the pure, panic-free core used by FromRoman.72// It returns (value, true) only for a canonical numeral: it accepts input73// 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, false78 }79 total, prev := 0, 080 for i := len(s) - 1; i >= 0; i-- {81 v := charVal(s[i])82 if v == 0 {83 return 0, false84 }85 if v < prev {86 total -= v87 } else {88 total += v89 prev = v90 }91 }92 if total < 1 || total > 3999 || ToRoman(total) != s {93 return 0, false94 }95 return total, true96}97Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.