Roman Numeral Converter — Go source
Convert integers up to 3,999,999 to Roman numerals and back. Vinculum overline above 3,999, canonical-form validation, a step-by-step greedy breakdown, and 14 language sources. Runs entirely in your browser.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package roman is the Go twin of CosmoDev's src/lib/roman-numeral.ts (dual
// source: web lib is TypeScript, CLI lib is Go — kept in lock-step). Pure +
// deterministic, never panics. Table-driven tests share vectors with the TS suite.
//
// Range 1..3,999,999. Above 3,999 the canonical form uses VINCULUM notation:
// a combining overline (U+0305) on each symbol multiplies its value by 1,000,
// so the canonical string is overline(thousands) + plain remainder.
package roman
import "strings"
// Overline is the combining vinculum mark (U+0305): a bar above a glyph
// multiplies its value by 1,000.
const Overline = "\u0305"
// macron (U+0304) is visually near-identical; accepted on input, normalized.
const macron = "\u0304"
// MaxRoman is the largest representable value: 3,999 thousands + 999 remainder.
const MaxRoman = 3_999_999
var ordered = []struct {
n int
s string
}{
{1000, "M"}, {900, "CM"}, {500, "D"}, {400, "CD"},
{100, "C"}, {90, "XC"}, {50, "L"}, {40, "XL"},
{10, "X"}, {9, "IX"}, {5, "V"}, {4, "IV"}, {1, "I"},
}
var letterValues = map[rune]int{
'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000,
}
const overlineRune = '\u0305'
// overline attaches the combining mark to every rune (value x 1,000).
func overline(s string) string {
var b strings.Builder
for _, r := range s {
b.WriteRune(r)
b.WriteString(Overline)
}
return b.String()
}
// toRomanBase greedily renders 1..3,999.
func toRomanBase(v int) string {
out := strings.Builder{}
for _, e := range ordered {
for v >= e.n {
out.WriteString(e.s)
v -= e.n
}
}
return out.String()
}
// ToRoman converts an integer (1..3,999,999) to a Roman numeral, or "" if out
// of range. Above 3,999 the thousands part carries a combining overline.
func ToRoman(n int) string {
if n < 1 || n > MaxRoman {
return ""
}
if n <= 3999 {
return toRomanBase(n)
}
thousands, rest := n/1000, n%1000
out := overline(toRomanBase(thousands))
if rest > 0 {
out += toRomanBase(rest)
}
return out
}
// scanValue values a known-valid MDCLXVI string with one left-to-right scan:
// a smaller letter before a larger one subtracts (IV = 4, CM = 900). Returns
// junk for non-canonical strings — the round-trip in FromRoman is the gate.
func scanValue(s string) int {
rs := []rune(s)
total := 0
for i, r := range rs {
v := letterValues[r]
if i+1 < len(rs) && letterValues[rs[i+1]] > v {
total -= v
} else {
total += v
}
}
return total
}
// FromRoman parses a canonical Roman numeral (plain or vinculum), returning
// ok=false if invalid or non-canonical (e.g. "IIII", "VV", plain "MMMM" for
// 4,000). The round-trip check enforces canonical form.
func FromRoman(s string) (int, bool) {
input := strings.ReplaceAll(strings.ToUpper(strings.TrimSpace(s)), macron, Overline)
rs := []rune(input)
var over, plain strings.Builder
for i := 0; i < len(rs); i++ {
if _, ok := letterValues[rs[i]]; !ok {
return 0, false
}
if i+1 < len(rs) && rs[i+1] == overlineRune {
over.WriteRune(rs[i])
i++
} else {
plain.WriteRune(rs[i])
}
}
total := 0
if over.Len() > 0 {
total += scanValue(over.String()) * 1000
}
if plain.Len() > 0 {
total += scanValue(plain.String())
}
if total < 1 || total > MaxRoman || ToRoman(total) != input {
return 0, false
}
return total, true
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →