Fix: Money/Currency class in C# 2.0

    This is a correction to a previously posted code sample after a very much appreciated comment by Dave Gallucci. Additionally, I have removed the functionality of the Allocate method which always seemed a little misplaced to me. Dave’s comment gave me the opportunity to revisit this and helped add validation to a comment I made regarding unit test coverage. I have 100% test coverage for this code, but it still didn’t mean that this code was 100% free of a bug. I have since written a test to prove Dave’s case and validate the fix, but it goes to show that tests are limited to the thoughts of the developer writing them. A code review is always useful and you can’t replace it with a suite of tests. Thanks again Dave!
 

public sealed class Money : IEquatable<Money>, IComparable, IComparable<Money> {

    private static int[] cents = new int[] { 1, 10, 100, 1000 };

    CultureInfo cultureInfo;

    RegionInfo regionInfo;

    long amount;

    public Money() : this(0, CultureInfo.CurrentCulture) { }

    public Money(decimal amount) : this(amount, CultureInfo.CurrentCulture) { }

    public Money(long amount) : this(amount, CultureInfo.CurrentCulture) { }

    public Money(string cultureName) : this(new CultureInfo(cultureName)) { }

    public Money(decimal amount, string cultureName) : this(amount, new     CultureInfo(cultureName)) { }

    public Money(CultureInfo cultureInfo) : this(0, cultureInfo) { }

    public Money(decimal amount, CultureInfo cultureInfo) {

        if (cultureInfo == null) throw new ArgumentNullException("cultureInfo");

        this.cultureInfo = cultureInfo;

        this.regionInfo = new RegionInfo(cultureInfo.LCID);

        this.amount = Convert.ToInt64(Math.Round(amount * CentFactor));

    }

    public Money(long amount, CultureInfo cultureInfo) {

        if (cultureInfo == null) throw new ArgumentNullException("cultureInfo");

        this.cultureInfo = cultureInfo;

        this.regionInfo = new RegionInfo(cultureInfo.LCID);

        this.amount = amount * CentFactor;

    }

    private int CentFactor {

        get { return cents[cultureInfo.NumberFormat.CurrencyDecimalDigits]; }

    }

    public string EnglishCultureName {

        get { return cultureInfo.Name; }

    }

    public string ISOCurrencySymbol {

        get { return regionInfo.ISOCurrencySymbol; }

    }

    public decimal Amount {

        get { return System.Convert.ToDecimal(amount) / CentFactor; }

    }

    public int DecimalDigits {

        get { return cultureInfo.NumberFormat.CurrencyDecimalDigits; }

    }

    public static bool operator >(Money first, Money second) {

        AssertSameCurrency(first, second);

        return first.amount > second.amount;

    }

    public static bool operator >=(Money first, Money second) {

        AssertSameCurrency(first, second);

        return first.amount >= second.amount;

    }

    public static bool operator <=(Money first, Money second) {

        AssertSameCurrency(first, second);

        return first.amount <= second.amount;

    }

    public static bool operator <(Money first, Money second) {

        AssertSameCurrency(first, second);

        return first.amount < second.amount;

    }

    public static Money operator +(Money first, Money second) {

        AssertSameCurrency(first, second);

        return new Money(first.Amount + second.Amount, first.EnglishCultureName);

    }

    public static Money Add(Money first, Money second) {

        return first + second;

    }

    public static Money operator -(Money first, Money second) {

        AssertSameCurrency(first, second);

        return new Money(first.Amount – second.Amount, first.EnglishCultureName);

    }

    public static Money Subtract(Money first, Money second) {

        return first – second;

    }

    public static implicit operator Money(decimal amount) {

        return new Money(amount, CultureInfo.CurrentCulture);

    }

    public static implicit operator Money(long amount) {

        return new Money(amount, CultureInfo.CurrentCulture);

    }

    public override bool Equals(object obj) {

        return (obj is Money) && Equals((Money)obj);

    }

    public override int GetHashCode() {

        return amount.GetHashCode() ^ cultureInfo.GetHashCode();

    }

    private static void AssertSameCurrency(Money first, Money second) {

        if (first.ISOCurrencySymbol != second.ISOCurrencySymbol)

            throw new InvalidOperationException("Money type mismatch.");

    }

    public bool Equals(Money other) {

        if (object.ReferenceEquals(other, null)) return false;

        return ((ISOCurrencySymbol == other.ISOCurrencySymbol) && (amount == other.amount));

    }

    public static bool operator ==(Money first, Money second) {

        if (object.ReferenceEquals(first, second)) return true;

        if (object.ReferenceEquals(first, null) || object.ReferenceEquals(second, null)) return false;

        return (first.ISOCurrencySymbol == second.ISOCurrencySymbol && first.Amount == second.Amount);

    }

    public static bool operator !=(Money first, Money second) {

        return !first.Equals(second);

    }

    public static Money operator *(Money money, decimal value) {

        if (money == null) throw new ArgumentNullException("money");

            return new Money(Decimal.Floor(money.Amount * value), money.EnglishCultureName);

    }

    public static Money Multiply(Money money, decimal value) {

        return money * value;

    }

    public static Money operator /(Money money, decimal value) {

        if (money == null) throw new ArgumentNullException("money");

        return new Money(money.Amount / value, money.EnglishCultureName);

    }

    public static Money Divide(Money first, decimal value) {

        return first / value;

    }

    public Money Copy() {

        return new Money(Amount, cultureInfo);

    }

    public Money Clone() {

        return new Money(cultureInfo);

    }

    public int CompareTo(object obj) {

        if (obj == null) {

            return 1;

        }

        if (!(obj is Money)) {

            throw new ArgumentException("Argument must be money");

        }

        return CompareTo((Money)obj);

    }

    public int CompareTo(Money other) {

        if (this < other) {

            return -1;

        }

        if (this > other) {

            return 1;

        }

        return 0;

    }

    public override string ToString() {

        return Amount.ToString("C", cultureInfo);

    }

    public string ToString(string format) {

        return Amount.ToString(format, this.cultureInfo);

    }

    public static Money LocalCurrency {

        get { return new Money(CultureInfo.CurrentCulture); }

    }

}

Advertisements

2 thoughts on “Fix: Money/Currency class in C# 2.0

  1. You’re welcome! Thanks for posting it in the first place. I’m retrofitting a site using this class to handle all the money details.
     
    Best Regards,
     
    Dave

  2. Great! I’m doing the same thing before I saw it.
     
    Now I want to implement all the patterns in PEAA in c# 2.0.   Do you have interesting in it? Maybe we can work together ?  Contact me with my email.
     
    Thanks.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s