Roman Numerals

Level: Beginner 15–30 min

Concepts: AlgorithmsNumbers

Solutions: C# | TypeScript | Python


Create a converter that turns a positive integer (1–3999) into its Roman numeral string.

Requirements

  1. Convert Arabic numbers to Roman numerals in the range 1 to 3999.
  2. Follow standard Roman numeral rules:
    • I = 1, V = 5, X = 10, L = 50, C = 100, D = 500, M = 1000
    • When a smaller value precedes a larger value, subtract the smaller (e.g., IV = 4, IX = 9, XL = 40, XC = 90, CD = 400, CM = 900)
    • Only the six subtractive pairs above appear; no IL, IC, IM, etc.

Test Cases

Arabic NumberRoman NumeralNotes
1IBasic conversion
4IVSubtractive — gear-shift moment
9IXSubtractive
40XLSubtractive at tens
90XCSubtractive at tens
400CDSubtractive at hundreds
900CMSubtractive at hundreds
1984MCMLXXXIVComposite across four orders of magnitude
3999MMMCMXCIXMaximum valid number

Stretch Goals (Not in the Reference)

The reference implementations cover Arabic → Roman conversion for the nine scenarios above and stop. If you want to extend further:

  • Roman → Arabic (the reverse direction)
  • Input validation — rejecting numbers less than 1 or greater than 3999, non-integers, etc.
  • Roman-input validation — invalid characters, repeated symbols beyond three (e.g., “IIII”, “VV”), invalid subtractive combinations (e.g., “IL”, “IC”)

The canonical TDD teaching arc for this kata is Arabic → Roman only — that is the direction where the subtractives-in-the-table insight lands cleanly. The reverse direction and validation each deserve their own kata session.

Tips

  • Start with the simplest conversions first (1, 2, 3).
  • Notice when 4 arrives that “concatenate I n times” is a dead end — the table of (value, symbol) pairs with subtractives baked in is the move.
  • Add larger values gradually (10, 40, 100, 400, 1000) — the subtractives (9, 90, 900) should pass on arrival once the table has them.

Reference Walkthrough

Full C#, TypeScript, and Python implementations live at tddbuddy-reference-katas/roman-numerals — the same nine scenarios across all three languages, walked commit-by-commit through the TDD cycle.

The reference covers Arabic → Roman only — the reverse direction and input validation are stretch goals and are not implemented there.

This is a Pedagogy mode kata — the walkthroughs step through the TDD cycle commit-by-commit. Watch the gear shift from low (fake-it, hardcoded branches, input-keyed dictionary) to middle once the 4 → IV scenario forces the refactor. The teaching point is the table that beats special cases: triangulating 1 → I, 2 → II, 3 → III tempts you into “repeat I n times”, and 4 breaks it flat. The right move is to promote the lookup from {input → output} to an ordered list of (value, symbol) pairs with subtractives baked in as first-class entries — [(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")] — and a greedy subtract loop. The payoff is the chain of five spec — commits at the end: 9, 90, 900, 1984, and 3999 all pass on arrival once the table is complete. Subtractives are not exceptions to the rule; they are entries in the table. See the repo’s Gears section for why middle gear is where the table lands.


  • Katas Are Rehearsal, Not Performance
    You don't practice TDD on production code. You practice on katas and bring the muscle memory to production. The gap between knowing TDD and doing TDD is reps.
  • The Contract Test Is the Only Witness the Agent Cannot Author
    A test the agent wrote against code the same agent wrote shares the agent's blind spot: a misunderstanding in the implementation becomes a matching misunderstanding in the test, the bar goes green, and the bug ships certified. Consumer-driven contract tests are the only category where a second team publishes the assertion the first team has to satisfy. That separation of authorship is exactly what tamper-resistant test design demanded, applied at the integration seam.
  • Cohesion Is the Coverage of the Agent Era
    Agent-generated codebases pass every structural-modularity metric while fragmenting the domain concept the team was trying to encode. Coverage measured whether a line ran. Mutation score measured whether an assertion pinned behavior. Cohesion is the next axis, and it measures whether the codebase's concepts are traversable end-to-end.