Login


Generating and Parsing Roman Numerals

By Jonathan Wood on 4/11/2011
Language: C#
Technology: .NET
Platform: Windows
License: CPOL
Views: 18,005
General Programming » Text Handling » General » Generating and Parsing Roman Numerals

Introduction

Although Roman numerals aren't used much these days, some applications may still need to generate them, parse them, or both.

Most of us are at least somewhat familiar with Roman numerals. The following table shows each Roman numeral and its corresponding value.

LetterValue
I1
V5
X10
L50
C100
D500
M1000

Each value is repeated up to three times before incorporating the next symbol. For example, III is 3. Where four are needed, the symbol appears once followed by the next symbol. For example, IV is 4.

Since M is the highest symbol, it would obviously be repeated as many times as needed. A variation on the table above is to put a horizontal line over a symbol. This signifies that the value is multiplied by 1,000. However, this syntax is awkward or impossible to enter on a modern keyboard and so is not normally used on computers.

Absent from the table above is a symbol for zero. However, there are cases where N (for the Latin word nulla) has been used for zero. We'll follow this convention in this article.

The RomanNumerals Class

Listing 1 shows my RomanNumerals class. This is a static class that will convert an integer value to Roman numerals. In addition, it will also convert Roman numerals back to an integer.

The code uses the _lookup table in these conversions. This table is populated in the constructor to contain each Roman numeral along with its corresponding value. In addition, since Roman numerals have a bit of an oddity with syntax such as "IV" for 4, these "double-character" values are also included in the table. The ToString() method, which converts an integer to a string of Roman numerals, becomes a fairly simple loop with the help of this table.

The TryParse() method also uses this table, but to convert a string back to an integer. This method is smart enough to handle both upper and lower case symbols, and leading and trailing whitespace. There is also a Parse() method that returns the result directly but raises an exception if the input cannot be converted. The Parse() method calls the TryParse() method so either can be used, depending on which syntax best suits your needs. The parsing methods are slightly more complex than the ToString() method.

Listing 1: The RomanNumerals Class

/// <summary>
/// Class to convert between integers and Roman numeral strings.
/// </summary>
static class RomanNumerals
{
    private class RomanInfo
    {
        public int Value { get; set; }
        public string Numerals { get; set; }
    }

    private static List<RomanInfo> _lookup;

    // Construction
    static RomanNumerals()
    {
        _lookup = new List<RomanInfo>();
        _lookup.Add(new RomanInfo() { Value = 1000, Numerals = "M" });
        _lookup.Add(new RomanInfo() { Value = 900, Numerals = "CM" });
        _lookup.Add(new RomanInfo() { Value = 500, Numerals = "D" });
        _lookup.Add(new RomanInfo() { Value = 400, Numerals = "CD" });
        _lookup.Add(new RomanInfo() { Value = 100, Numerals = "C" });
        _lookup.Add(new RomanInfo() { Value = 90, Numerals = "XC" });
        _lookup.Add(new RomanInfo() { Value = 50, Numerals = "L" });
        _lookup.Add(new RomanInfo() { Value = 40, Numerals = "XL" });
        _lookup.Add(new RomanInfo() { Value = 10, Numerals = "X" });
        _lookup.Add(new RomanInfo() { Value = 9, Numerals = "IX" });
        _lookup.Add(new RomanInfo() { Value = 5, Numerals = "V" });
        _lookup.Add(new RomanInfo() { Value = 4, Numerals = "IV" });
        _lookup.Add(new RomanInfo() { Value = 1, Numerals = "I" });
    }

    /// <summary>
    /// Converts an integer value to the equivalent string of
    /// Roman numerals.
    /// </summary>
    /// <param name="value">Value to convert</param>
    public static string ToString(int value)
    {
        // Although Roman numerals don't generally include
        // zero, we'll use 'N' (for the Latin word nulla)
        // if necessary
        if (value == 0)
            return "N";

        // Otherwise, use lookup table to build string
        StringBuilder builder = new StringBuilder();
        foreach (RomanInfo info in _lookup)
        {
            while (value >= info.Value)
            {
                builder.Append(info.Numerals);
                value -= info.Value;
            }
        }
        return builder.ToString();
    }

    /// <summary>
    /// Converts a string of Roman numeral characters to the
    /// equivalent integer value.
    /// </summary>
    /// <param name="s">String to convert</param>
    public static int Parse(string s)
    {
        int result;
        if (!TryParse(s, out result))
            throw new ArgumentException();
        return result;
    }

    /// <summary>
    /// Attempts to convert a string of Roman numeral characters to
    /// the equivalent integer value.
    /// </summary>
    /// <param name="s">String to conver</param>
    /// <param name="result">The result of the conversion</param>
    public static bool TryParse(string s, out int result)
    {
        // Normalize input
        s = s.Trim().ToUpper();

        int pos = 0;
        result = 0;

        // Special handling for zero
        if (s[pos] == 'N')
            pos++;

        // Use lookup table to parse string
        while (pos < s.Length && !Char.IsWhiteSpace(s[pos]))
        {
            RomanInfo info;

            // Test for double-character match
            if ((s.Length - pos) >= 2)
            {
                string pair = s.Substring(pos, 2);
                info = _lookup.Find(i => i.Numerals == pair);
                if (info != null)
                {
                    result += info.Value;
                    pos += 2;
                    continue;
                }
            }

            // Otherwise, text for single-character match
            string letter = s.Substring(pos, 1);
            info = _lookup.Find(i => i.Numerals == letter);
            if (info != null)
            {
                result += info.Value;
                pos++;
            }
            else
            {
                // Invalid character encountered
                return false;
            }
        }
        // Return true if any characters processed
        return (pos > 0);
    }
}

Conclusion

All methods are static so you call the methods directly without instantiating the class.

With the help of the lookup table, this code is pretty straight forward and can be understood without too much trouble. Either way, this class can be a handy addition to your own bag of tricks.

End-User License

Use of this article and any related source code or other files is governed by the terms and conditions of The Code Project Open License.

Author Information

Jonathan Wood

I'm a software/website developer working out of the greater Salt Lake City area in Utah. I've developed many websites including Black Belt Coder, Insider Articles, and others.