โ† Back to DevBytes

Solving Roman to Integer in Go: Step-by-Step Guide

Introduction to Roman to Integer Conversion

The Roman to Integer problem is one of the most popular algorithmic challenges on platforms like LeetCode. It asks you to convert a string representing a Roman numeral into its corresponding integer value. While Roman numerals may seem like an ancient relic, this problem is an excellent exercise in string parsing, conditional logic, and understanding how positional value systems work.

In this tutorial, we will walk through solving the Roman to Integer problem using the Go programming language. We will cover the rules of Roman numerals, break down the algorithm step by step, implement a clean solution, and discuss best practices along the way.

Understanding Roman Numerals

Before writing any code, it is essential to understand how Roman numerals work. Roman numerals are represented by seven different symbols, each with a fixed integer value:

Normally, Roman numerals are written from largest to smallest value, moving left to right. For example, LVIII equals 58 because L (50) + V (5) + III (3) = 58.

However, there is a special rule: when a smaller value appears before a larger value, the smaller value is subtracted rather than added. This is called subtractive notation. There are six valid instances of this rule:

For example, IX equals 9, not 11, because I comes before X and is therefore subtracted. Similarly, MCMXCIV equals 1994: M (1000) + CM (900) + XC (90) + IV (4).

Why This Problem Matters

The Roman to Integer problem is more than just a trivia exercise. It teaches several fundamental programming concepts:

These skills transfer directly to real-world tasks such as parsing configuration files, processing tokens, and validating user input.

Designing the Algorithm

The most elegant approach to solving this problem is to iterate through the string from left to right, comparing each symbol with the next one. If the current symbol's value is less than the next symbol's value, we subtract the current value from the total. Otherwise, we add it.

Here is the step-by-step logic:

This approach works because the subtractive rule only ever involves two adjacent characters. By always looking one character ahead, we can determine whether the current character represents addition or subtraction.

Implementing the Solution in Go

Now let us translate this algorithm into Go code. We will create a function called romanToInt that takes a string and returns an integer.

package main

import "fmt"

func romanToInt(s string) int {
    // Map each Roman numeral symbol to its integer value
    values := map[byte]int{
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }

    total := 0
    n := len(s)

    for i := 0; i < n; i++ {
        // If the current value is less than the next value, subtract it
        if i+1 < n && values[s[i]] < values[s[i+1]] {
            total -= values[s[i]]
        } else {
            // Otherwise, add it
            total += values[s[i]]
        }
    }

    return total
}

func main() {
    testCases := []string{"III", "LVIII", "MCMXCIV", "IX", "IV", "MMXXIV"}

    for _, tc := range testCases {
        fmt.Printf("%s -> %d\n", tc, romanToInt(tc))
    }
}

When you run this program, you should see the following output:

III -> 3
LVIII -> 58
MCMXCIV -> 1994
IX -> 9
IV -> 4
MMXXIV -> 2024

Let us trace through the example MCMXCIV to verify the logic:

The final result is 1994, which is correct.

Handling Edge Cases and Input Validation

The basic solution assumes the input is a valid Roman numeral string. In production code, you should validate the input to handle edge cases gracefully. Let us enhance our function with validation:

package main

import (
    "errors"
    "fmt"
)

var ErrInvalidRomanNumeral = errors.New("invalid Roman numeral")

func romanToIntValidated(s string) (int, error) {
    if len(s) == 0 {
        return 0, ErrInvalidRomanNumeral
    }

    values := map[byte]int{
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }

    total := 0
    n := len(s)

    for i := 0; i < n; i++ {
        current, ok := values[s[i]]
        if !ok {
            return 0, fmt.Errorf("%w: invalid character '%c'", ErrInvalidRomanNumeral, s[i])
        }

        if i+1 < n {
            next, ok := values[s[i+1]]
            if !ok {
                return 0, fmt.Errorf("%w: invalid character '%c'", ErrInvalidRomanNumeral, s[i+1])
            }

            if current < next {
                total -= current
            } else {
                total += current
            }
        } else {
            total += current
        }
    }

    return total, nil
}

func main() {
    inputs := []string{"III", "ABC", "", "XLII"}

    for _, input := range inputs {
        result, err := romanToIntValidated(input)
        if err != nil {
            fmt.Printf("Error for %q: %v\n", input, err)
        } else {
            fmt.Printf("%q -> %d\n", input, result)
        }
    }
}

This version returns an error if the input is empty or contains characters that are not valid Roman numeral symbols. Using Go's error wrapping with %w allows callers to check for specific error types while preserving context.

Alternative Approach: Right-to-Left Traversal

Another common approach is to traverse the string from right to left. In this method, you keep track of the previous value. If the current value is less than the previous value, you subtract it; otherwise, you add it. This approach can feel more intuitive to some developers:

func romanToIntReverse(s string) int {
    values := map[byte]int{
        'I': 1,
        'V': 5,
        'X': 10,
        'L': 50,
        'C': 100,
        'D': 500,
        'M': 1000,
    }

    total := 0
    prev := 0

    for i := len(s) - 1; i >= 0; i-- {
        current := values[s[i]]
        if current < prev {
            total -= current
        } else {
            total += current
        }
        prev = current
    }

    return total
}

Both approaches have the same time complexity of O(n) and space complexity of O(1), so the choice between them is largely a matter of personal preference and readability.

Best Practices

Use Maps for Constant-Time Lookups

Using a Go map to store symbol-value pairs provides O(1) lookup time. This is cleaner and more maintainable than a long chain of switch or if statements. If you ever need to support additional symbols, you only need to update the map.

Validate Input Early

Always check for empty strings and invalid characters before processing. Failing fast prevents subtle bugs and makes debugging easier. In Go, returning errors rather than panicking is the idiomatic way to handle invalid input.

Keep Functions Focused

Your romanToInt function should do one thing: convert a Roman numeral to an integer. Avoid mixing parsing logic with I/O operations like printing or logging. This makes the function easier to test and reuse.

Write Table-Driven Tests

Go's testing package is well-suited for table-driven tests. Here is an example test file:

package main

import "testing"

func TestRomanToInt(t *testing.T) {
    tests := []struct {
        input    string
        expected int
    }{
        {"III", 3},
        {"IV", 4},
        {"IX", 9},
        {"LVIII", 58},
        {"MCMXCIV", 1994},
        {"MMXXIV", 2024},
        {"I", 1},
        {"MMMCMXCIX", 3999},
    }

    for _, tt := range tests {
        result := romanToInt(tt.input)
        if result != tt.expected {
            t.Errorf("romanToInt(%q) = %d; expected %d", tt.input, result, tt.expected)
        }
    }
}

Table-driven tests make it easy to add new test cases and clearly document the expected behavior of your function.

Avoid Premature Optimization

The straightforward O(n) solution is already optimal for this problem. Do not waste time trying to micro-optimize the loop or replace the map with a switch statement unless profiling shows a real bottleneck. Clarity should always come first.

Conclusion

Solving the Roman to Integer problem in Go is a great way to practice string manipulation, map lookups, and conditional logic. By understanding the subtractive notation rule and implementing a simple left-to-right traversal, you can build a clean and efficient solution. Remember to validate your input, write comprehensive tests, and keep your code readable. The patterns you learn here โ€” lookup tables, adjacent comparisons, and error handling โ€” will serve you well across countless other programming challenges.

๐Ÿ›  Tools from DevBytes

Inventory Tracker Pro โ€” Excel inventory system, low-stock alerts ยท $19
AI Dev Kit for Mac โ€” local AI dev environment templates ยท $9.99
KeyMapper for Mac โ€” custom keyboard shortcut toolkit ยท $7.99

โ† Back to all articles