Calculations with Roman Numerals using C#

In a previous article, I wrote how to parse Roman Numerals using C#. This article will focus on how to calculate with the class intuitively. It will show how to implement implicit casting and the add and subtraction operator overloads. Fun stuff that's probably useful in other projects.

It would be great to do things like this:

RomanNumeral X = "X";
string result = X + "IV" + 8;

The original code was written in 2017. Currently it is 2024, so I've updated the code to .NET 8. To make things easier, we made sure that we cannot parse a RomanNumeral to null, to make nullable easier to implement.

Implement implicit casting

The first thing we must implement is implicit casting. This will enable the class to interact with strings and integers without having to cast them to a RomanNumeral first. Let's look at the code:

public partial class RomanNumeral
{
    public static implicit operator int(RomanNumeral r)
    {
        return r.Number;
    }

    public static implicit operator string(RomanNumeral r)
    {
        return r.ToString();
    }

    public static implicit operator RomanNumeral(int r)
    {
        return new RomanNumeral(r);
    }

    public static implicit operator RomanNumeral(string r)
    {
        return Parse(r);
    }
}

What are the drawbacks of this method? Well, the Parse might return a null when the string is not parsable (l33t is not a valid Roman Numeral). This might result in a null-reference exception. The constructor method will throw an exception when anything below 0 is passed. This is something you might want to work around.

Plus and minus operator overloading

Now let's implement the arithmetical operators +, -/* and % for the RomanNumeral class. Notice that negative numbers are not supported in this example. We'll default to 0 (or nulla as the Romans would call it).

public partial class RomanNumeral
{
    public static int operator +(int r1, RomanNumeral r2)
    {
        var r = new RomanNumeral(r1) + r2;
        return r.Number;
    }

    public static string operator +(string r1, RomanNumeral r2)
    {
        var r = Parse(r1) + r2;
        return r.ToString();
    }

    public static RomanNumeral operator +(RomanNumeral r1, string r2)
    {
        return r1 + Parse(r2);
    }

    public static RomanNumeral operator +(RomanNumeral r1, int r2)
    {
        var n = r1.Number + r2;
        return new RomanNumeral(n);
    }

    public static RomanNumeral operator +(RomanNumeral r1, RomanNumeral r2)
    {
        var n = r1.Number + r2.Number;
        return new RomanNumeral(n);
    }

    public static int operator -(int r1, RomanNumeral r2)
    {
        var r = new RomanNumeral(r1) - r2;
        return r.Number;
    }

    public static string operator -(string r1, RomanNumeral r2)
    {
        var r = Parse(r1) - r2;
        return r.ToString();
    }

    public static RomanNumeral operator -(RomanNumeral r1, RomanNumeral r2)
    {
        var n = r1.Number - r2.Number;

        if (n < 0)
        {
            n = 0;
        }

        return new RomanNumeral(n);
    }
}

Because we've implemented implicit conversions first the class handles operation on strings and integers automagically, as these xUnit tests show:

RomanNumeral I = "I";
RomanNumeral IV = "IV";

int a = IV - 1;
int b = 4 - I;
int c = IV - "I";
int d = (RomanNumeral) "IV" - I;
int e = IV - I;

Assert.Equivalent(3, a);
Assert.Equivalent(3, b);
Assert.Equivalent(3, c);
Assert.Equivalent(3, d);
Assert.Equivalent(3, e);

string f = IV - 1;
string g = (RomanNumeral) 4 - I;
string h = IV - "I";
string i = "IV" - I;
string j = IV - I;

Assert.Equivalent("III", f);
Assert.Equivalent("III", g);
Assert.Equivalent("III", h);
Assert.Equivalent("III", i);
Assert.Equivalent("III", j);

RomanNumeral k = IV - 1;
RomanNumeral l = 4 - I;
RomanNumeral m = IV - "I";
RomanNumeral n = "IV" - I;
RomanNumeral o = IV - I;

Assert.Equivalent("III", k.ToString());
Assert.Equivalent("III", l.ToString());
Assert.Equivalent("III", m.ToString());
Assert.Equivalent("III", n.ToString());
Assert.Equivalent("III", o.ToString());

Beautiful, I'd say!

Comparison operators

Mathematical operations are one thing, but how about comparing Roman Numerals? Implementing an IComparable and IComparable<RomanNumeral> will allow sorting mechanisms to handle the class.

public partial class RomanNumeral : IComparable, IComparable<RomanNumeral>
{
    public int CompareTo(RomanNumeral? other)
    {
        if (other is null)
        {
            return 1;
        }

        return Number.CompareTo(other.Number);
    }

    public int CompareTo(object? obj)
    {
        return CompareTo(obj as RomanNumeral);
    }

    protected static int Compare(RomanNumeral r1, RomanNumeral r2)
    {
        if (object.ReferenceEquals(r1, r2))
        {
            return 0;
        }
        if (r1 is null)
        {
            return -1;
        }
        return r1.CompareTo(r2);
    }
}

Notice how I'm not using the == or the != operators. This will prevent infinite loops when we implement these operators using the compare methods. The object.ReferenceEquals will do the job.

Implementing the operator overloads

Now let's implement the ==!=<<=>>= operators using the statis Compare method.

public partial class RomanNumeral
{
    public static bool operator ==(RomanNumeral r1, RomanNumeral r2)
    {
        return Compare(r1, r2) == 0;
    }

    public static bool operator !=(RomanNumeral r1, RomanNumeral r2)
    {
        return Compare(r1, r2) != 0;
    }

    public static bool operator <(RomanNumeral r1, RomanNumeral r2)
    {
        return (Compare(r1, r2) < 0);
    }

    public static bool operator >(RomanNumeral r1, RomanNumeral r2)
    {
        return (Compare(r1, r2) > 0);
    }

    public static bool operator <=(RomanNumeral r1, RomanNumeral r2)
    {
        return (Compare(r1, r2) <= 0);
    }

    public static bool operator >=(RomanNumeral r1, RomanNumeral r2)
    {
        return (Compare(r1, r2) >= 0);
    }
}

Implement equals

The equality method can also be implemented using the CompareTo method. Remember that the GetHashCode should be overridden as well.

public partial class RomanNumeral : IComparable, IComparable<RomanNumeral>
{
    public override bool Equals(object? obj)
    {
        return CompareTo(obj) == 0;
    }

    public override int GetHashCode()
    {
        return this.Number.GetHashCode();
    }
}

Summary

I’ve shown how one can do arithmetic using Roman Numerals. Implicit casting and operator overloading are very helpful in this case. You can download the partial classes from GitHub and use it in your projects.

The full project can be found on GitHub: roman-numerals. I've published the various partial classes.

Changelog

  • rewrite to .NET 8.
  • initial article.
expand_less brightness_auto