P3.NET

Creating a Money Type

.NET does not have a type to represent monetary values. In general you will simply use Decimal as it has the necessary accuracy. This has some limitations though:

  • Without looking at surrounding code it is difficult to tell if a variable represents money or simply a large decimal value
  • Currency information is not part of the type so it is possible to incorrectly mix monetary values in systems that support multiple currencies at once
  • Displaying money requires that you use a format string to indicate the currency

There have been many implementations of money in .NET. This is my version. Please note that this code is new so bugs may exist. Additionally I’ve taken the requirements and implementation from my own needs. Your needs may differ so you may need to modify the code to behave differently for your applications.

Requirements

Before talking about the implementation I wanted to clarify the requirements for the Money type.

  • It must be clear to a developer that it represents monetary values
  • It must be immutable
  • It must be interchangeable with Decimal since that is the implementation used elsewhere in the framework
  • It must support currency, but not be required
  • Basic monetary arithmetic should be supported
  • Conversion between currencies should not be allowed
  • It must support standard value type features like equality, comparison and formatting

Basic Support

Setting up the base value type is straightforward. For now we need to store only the underlying Decimal value. Since the struct is immutable it must be specified in the constructor.

public struct Money
{
   public Money ( decimal value )
   {
       Value = value;
   }

   public Money ( double value ) : this((decimal)value)
   {
   }

   public decimal Value { get; private set; }
}

Currency Support

Adding support for currency is not difficult. We will simply track the currency using another property that is set at construction time. But, like DateTime, we do not want to burden code with the need for currency in applications that never use more than one so is not required. Money values that do not have any currency are assumed to be currency-neutral. This will impact how we work with them later.

The more interesting question is how to represent currency. In .NET there are 2 different types that can be used for currency: CultureInfo and RegionInfo. Both store information about currency such as the symbol, formatting requirements, etc. From my understanding of the differences CultureInfo represents all the cultural settings beyond just money. A user can customize the cultural settings such that trying to track values using it would be difficult. RegionInfo on the other hand contains information about a region including the name of the currency. Unlike CultureInfo a user will not change the region information. For purposes of representing monetary values RegionInfo seems like a better choice. It can be obtained from the current culture if needed. Note that when it comes to formatting money we will continue to use the cultural settings.

public struct Money
{
   public Money ( decimal value ) : this(value, null)
   {
   }

   public Money ( double value ) : this((decimal)value, null)
   {
   }

   public Money ( double value, RegionInfo currency ) : this((decimal)value, currency)
   {
   }

   public Money ( decimal value, RegionInfo currency ) : this()
   {
      Currency = currency;
      Value = value;
   }

   public RegionInfo Currency { get; private set; }

   public decimal Value { get; private set; }
}

A final note about currency support. Multiple regions can use the same currency (i.e. Euro). It is not sufficient to simply compare the values but rather we have to look at the currency name.

Arithmetic Support

For arithmetic we focus on the 4 binary operations: addition, subtraction, multiplication and division. For each operation the types may or may not be the same. For our purposes money could be combined with itself or other numeric values (decimal, double, float, signed integers). But not all these combinations make sense for all operations.

  • Money + Money = Money (assuming currencies are compatible)
  • Money + Numeric = Money (currency determined by Money)
  • Money – Money = Money (assuming currencies are compatible)
  • Money – Numeric = Money (currency determined by Money)
  • Money * Money = Error (money is a value, not a multiplier)
  • Money * Numeric` = Money (currency determined by Money)
  • Money / Money = Numeric (i.e. you have $20 and you see drinks for $4 each, how many can you buy?)
  • Money / Numeric` = Money (i.e. you have $20 and you need to buy gifts for 3 people, how much can you spend on each?)

For the special case of multiplication we will not allow decimal so that an implicit conversion does not occur between money values. For division the returned value will be decimal simply because we are dividing to decimal values.

In addition to the standard methods to support the above operations we also define the C# operators. The compiler will handle conversion of smaller numeric values to larger but we have to define at least 3: decimal, double and long.

public Money Add ( Money value )
{
    var currency = VerifyMatchingCurrency(this, value);

    return new Money(Value + value.Value, currency);
}

public Money Add ( decimal value )
{
    return new Money(Value + value, Currency);
}

public Money Add ( double value )
{
    return new Money(Value + (decimal)value, Currency);
}

public Money Add ( long value )
{
    return new Money(Value + (decimal)value, Currency);
}

public static Money operator+ ( Money left, Money right )
{
    return left.Add(right);
}

public static Money operator+ ( Money left, decimal right )
{
    return left.Add(right);
}

public static Money operator+ ( Money left, double right )
{
    return left.Add(right);
}

public static Money operator+ ( Money left, long right )
{
    return left.Add(right);
}

The remaining arithmetic operations follow a similar pattern.

In each case we will verify the currencies are compatible, if necessary. To make it easier to programmatically handle mixed currency we will define a simple MismatchedCurrencyException to represent this case.

private static RegionInfo VerifyMatchingCurrency ( Money left, Money right )
{
    if (left.Currency == null)
        return right.Currency;

    if (right.Currency == null)
        return left.Currency;

    if (left.Currency.CurrencyEnglishName == right.Currency.CurrencyEnglishName)
        return left.Currency;

    throw new MismatchedCurrencyException();
}

Equality

Supporting equality requires that we implement the IEquatable<T> interface. We also need to implement the standard .NET infrastructure methods like Equals and GetHashCode. Since we are creating an interchangeable type with decimal we will implement both IEquatable<money> and IEquatable<decimal>.

We will define equality as having the same value. If currency is included then it will be compared as well. We will set a ground rule for the remainder of this article that the neutral currency is considered equal to a non-neutral currency. For example if one value is 1 in Dollars and another value is 1 but has no currency then they are equal. If you don’t agree with this then you could retrieve the current culture’s settings and use that.

Another important distinction is in precision. For purposes of equality we will use the entire value rather than rounding the value to the nearest cent (or currency equivalent). Thus 1.234 is not equal to 1.235.

public static bool operator ==( Money left, Money right )
{
    return left.Equals(right);
}

public static bool operator ==( Money left, decimal right )
{
    return left.Equals(right);
}

public static bool operator !=( Money left, Money right )
{
    return !left.Equals(right);
}

public static bool operator !=( Money left, decimal right )
{
    return !left.Equals(right);
}

public override bool Equals ( object obj )
{
    if (obj is Money)
        return Equals((Money)obj);

    if (obj is decimal)
        return Equals((decimal)obj);

    return false;
}

public bool Equals ( Money other )
{
    var currency = VerifyMatchingCurrency(this, other);

    return Value.Equals(other.Value);
}

public bool Equals ( decimal other )
{
    return Value.Equals(other);
}

public override int GetHashCode ()
{
    if (Currency != null)
        return Tuple.Create(Currency, Value).GetHashCode();

    return Value.GetHashCode();
}

Comparison

Supporting comparison simply requires that we implement the IComparable<T> interface along with the standard C# operators. We will also support comparing to Decimal for convenience.

public int CompareTo ( Money other )
{
    VerifyMatchingCurrency(this, other);

    return this.Value.CompareTo(other.Value);
}

public int CompareTo ( decimal other )
{
    return this.Value.CompareTo(other);
}

public int CompareTo ( object other )
{
    if (other is Money)
        return CompareTo((Money)other);

    if (other is decimal)
        return CompareTo((decimal)other);

    throw new ArgumentException("Value must be money.");
}

Formatting

For formatting we will support the IFormattable interface. This is in addition to the standard ToString method. As mentioned earlier the currency comes into play here. We will set up a few rules for formatting.

  • If CultureInfo is provided using IFormatProvider then it will be used.
  • If the value has currency then we will try to get the culture settings for it.
  • The current culture’s settings will be used.

Getting the culture from the region info is not trivial so we’ll try a simple lookup. This may or may not work for all cases.

public override string ToString ()
{
    return ToString("c", null);
}

public string ToString ( string format )
{
    return ToString(format, null);
}

public string ToString ( string format, IFormatProvider provider )
{
    //Use currency
    if (String.IsNullOrEmpty(format))
        format = "c";

    //Use the provider, if any
    if (provider != null)
        return Value.ToString(format, provider);

    //If currency is provided then use it
    if (Currency != null)
    {
        var currencyName = Currency.Name;

        var culture = CultureInfo.GetCultures(CultureTypes.AllCultures).FirstOrDefault(c => c.Name == currencyName);
        if (culture == null)
            culture = CultureInfo.GetCultureInfo(currencyName);

        return Value.ToString(format, culture);
    };

    return Value.ToString(format);
}

Final Thoughts

As mentioned earlier, this is new code that I wrote based upon some existing code that I’ve been using. There may be bugs and/or cases that do not work properly. Feel free to report any issues you find. If you have thoughts on how to better handle currency (especially around formatting) then feel free to share.

Some possible improvements that are left as an exercise.

  • Support parsing by implementing Parse and TryParse
  • Add extension method to Decimal to convert to money
  • Implement better formatting support for different regions

Download the Code