Roman Numerals
The challenge
Given a positive integer, return its Roman numeral representation as a string.
Modern Roman numerals are written by expressing each decimal digit separately, starting with the leftmost, and skipping any digit with a value of zero. You cannot write numerals like IM for 999 — subtraction is only ever a single smaller numeral immediately before a larger one (IV, IX, XL, XC, CD, CM).
Examples:
1990→"MCMXC"(1000 →"M"+ 900 →"CM"+ 90 →"XC")2008→"MMVIII"(2000 →"MM"+ 8 →"VIII")99→"XCIX"(90 →"XC"+ 9 →"IX")47→"XLVII"(40 →"XL"+ 7 →"VII")
Remember to follow the red-green-refactor cycle and, if you're using an AI coding assistant, keep it on a tight leash — one test at a time.
Suggested test list
Build this one up gradually — don't jump straight to a general algorithm. A common approach is to earn each symbol one at a time:
1→"I".2or3→ repeated symbols ("II","III").4→"IV"— this is where simple repetition breaks down and forces a design decision.5→"V".9→"IX".10,40,50,90→ introduceX,XL,L,XC.100,400,500,900→ introduceC,CD,D,CM.1000→"M".- A number that exercises several groups at once, e.g.
1994→"MCMXCIV". - The worked examples above (
1990,2008,99,47) as regression tests once your general solution exists. - What should happen for
0, or a negative number? The challenge says the input is a positive integer — decide (and test) what your function does if that assumption is violated. - What's the largest number your solution can represent? Standard Roman numerals top out at
3999(MMMCMXCIX) before you'd need repeatingMs beyond what's conventionally written — decide and test what your function does at and beyond that boundary.
Your task
Implement the solution using strict TDD, one test from the list at a time. Resist the urge to write the general subtractive-pairs algorithm on your very first test — let the tests force you towards it.
💡 Show a reference solution (only after you've completed your own via TDD)
package romannumerals
var valueSymbols = []struct {
Value int
Symbol 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"},
}
func ToRoman(number int) string {
result := ""
for _, vs := range valueSymbols {
for number >= vs.Value {
result += vs.Symbol
number -= vs.Value
}
}
return result
}
// example tests, e.g. with Go's testing package
func TestToRoman(t *testing.T) {
cases := []struct {
Input int
Expected string
}{
{1, "I"},
{4, "IV"},
{9, "IX"},
{47, "XLVII"},
{99, "XCIX"},
{1990, "MCMXC"},
{2008, "MMVIII"},
}
for _, c := range cases {
got := ToRoman(c.Input)
if got != c.Expected {
t.Errorf("ToRoman(%d): got %q, want %q", c.Input, got, c.Expected)
}
}
}
Notice how the greedy, "largest value that fits" approach naturally produces the subtractive forms (IV, CM, etc.) for free, once 900/CM, 400/CD, 90/XC, 40/XL, 9/IX, and 4/IV are in the lookup table alongside the "plain" values.