DEV Community

Cover image for Roman Numerals in Go: The Self‑Correcting One‑Pass Trick
Archdemon
Archdemon

Posted on

Roman Numerals in Go: The Self‑Correcting One‑Pass Trick

Today we’ll look at a classic interview problem: converting a Roman numeral to an integer.

To be honest, this is not something most of us use in real life. In 10+ years of programming, I haven’t needed it once. But it does show up in interviews, and more importantly, it’s a great problem for learning how to reason about algorithms instead of memorising tricks.

Let’s take it step by step.


Understanding the Problem

You’re given a string made up of Roman numeral characters:

I, V, X, L, C, D, M
Enter fullscreen mode Exit fullscreen mode

Each character has a fixed numeric value:

  • I = 1
  • V = 5
  • X = 10
  • L = 50
  • C = 100
  • D = 500
  • M = 1000

Your task is simple:

Convert the Roman numeral string into its integer value.

The problem guarantees that:

  • the input is always valid
  • you don’t need to worry about malformed Roman numerals

That lets us focus entirely on the logic.


A Straightforward (Brute‑Force) Approach

The most natural way to solve this is to lean directly on the Roman numeral rules.

Some pairs are special:

  • IV = 4
  • IX = 9
  • XL = 40
  • XC = 90
  • CD = 400
  • CM = 900

Everything else is just addition.

So a brute‑force approach looks like this:

  1. Walk through the string from left to right
  2. If the current character and the next character form a subtractive pair, add that value and skip both
  3. Otherwise, add the value of the current character and move on

This works. It’s clear. And for this problem, it’s completely acceptable.

But there’s a small cost:

  • you have to explicitly encode all subtractive pairs
  • you need lookahead (i + 1), which adds branching and edge checks

At this point, it’s reasonable to pause and ask:

Can we do this in a single pass, without treating some cases as special?

That question leads to a more interesting solution.


The Core Idea

Here’s the idea we’ll build everything on:

Always add. Correct only when proven wrong.

Roman numerals follow two simple rules:

  1. Values normally add
  2. If a smaller value appears before a larger one, the smaller value should be subtracted

Examples:

  • VI = 5 + 1 = 6
  • IV = 5 − 1 = 4
  • XL = 50 − 10 = 40

Instead of handling these cases up front, we’ll assume everything adds — and fix things only when we realise that assumption was wrong.


Pause for a Moment

Before looking at code, stop here and think about this question:

If I always add values, how would I undo a mistake when I later discover that a value should have been subtracted?

Keep that question in mind. The answer is the key to the whole solution.


The Invariant (This Is Everything)

The algorithm relies on one simple invariant:

At the start of each iteration, the running total already includes the previous symbol exactly once.

Everything that follows is just a consequence of that statement.


Why the Correction Works

Let’s walk through a concrete example: "XL".

Step 1: 'X'

  • Value = 10
  • There is no previous symbol yet
  • We add 10

Running total:

10
Enter fullscreen mode Exit fullscreen mode

Nothing to fix.

Step 2: 'L'

  • Value = 50
  • The previous value was 10
  • According to Roman rules, that 10 should have been subtracted

But here’s the important part:

We already added 10 in the previous step.

To correct this, we need to:

  1. remove the earlier +10
  2. apply -10 instead

That’s a net change of:

-2 * previous_value
Enter fullscreen mode Exit fullscreen mode

So the update becomes:

current_value - 2 * previous_value
Enter fullscreen mode Exit fullscreen mode

Which gives:

50 - 2*10 = 30
Enter fullscreen mode Exit fullscreen mode

Add that to the running total:

10 + 30 = 40
Enter fullscreen mode Exit fullscreen mode

Correct.


Why We Subtract the Previous Value

This part is subtle and important.

We are not correcting the total.
We are correcting the contribution of the previous symbol.

  • previous is just the numeric value of the symbol we saw last
  • the running total already includes it once

That’s why this line works even on the first iteration:

current - 2*previous
Enter fullscreen mode Exit fullscreen mode

When previous is 0, there’s nothing to correct.


The Code (Go)

Once the idea is clear, the code becomes very straightforward.

func romanToInt(s string) int {
    num, prev := 0, 0

    for _, r := range s {
        curr := val(r)

        if curr <= prev {
            num += curr
        } else {
            num += curr - 2*prev
        }

        prev = curr
    }

    return num
}

func val(r rune) int {
    switch r {
    case 'I': return 1
    case 'V': return 5
    case 'X': return 10
    case 'L': return 50
    case 'C': return 100
    case 'D': return 500
    case 'M': return 1000
    }
    return 0
}
Enter fullscreen mode Exit fullscreen mode

Why This Is a Good Interview Solution

  • Single pass
  • O(1) extra space
  • No lookahead
  • No special cases

More importantly, it demonstrates that you can:

  • maintain a clear invariant
  • make simple assumptions
  • correct them cleanly when new information appears

One‑Sentence Mental Model

“I always add the current value. If I later realise the previous value should have been subtracted, I undo it by subtracting it twice.”

If that sentence makes sense, the solution will always make sense.


Final Thoughts

This problem isn’t really about Roman numerals.

It’s about writing algorithms that:

  • start with a simple assumption
  • stay linear and readable
  • correct themselves when needed

Once you see it that way, the 2 * previous no longer feels clever, it feels inevitable.


Final Thought

My goal here was to help you actually understand why this algorithm works and not just walk away with something to memorize.

Once that idea clicks, implementing it in your favorite language becomes a fun little exercise.

If this helped you see the problem differently, feel free to share it with someone who might enjoy the same “aha” moment. And if you have questions, feedback, or a different mental model altogether, drop a comment. Those conversations are half the fun.

Happy coding 👋

Top comments (0)