Click here to Skip to main content
15,885,032 members
Articles / Programming Languages / C# 4.0

A Money type for the CLR

Rate me:
Please Sign up or sign in to vote.
4.90/5 (62 votes)
18 Mar 2013Ms-PL14 min read 194K   2.5K   138  
A convenient, high-performance money structure for the CLR which handles arithmetic operations, currency types, formatting, and careful distribution and rounding without loss.
using System;
using System.Diagnostics;
using System.Globalization;

namespace System
{
    /// <summary>
    /// Represents a decimal amount of a specific <see cref="Currency"/>.
    /// </summary>
    [Serializable]
    [DebuggerDisplay("{getDebugView()}")]
    public struct Money : IEquatable<Money>,
                          IComparable<Money>,
                          IFormattable,
                          IConvertible,
                          IComparable
    {
        /// <summary>
        /// A zero value of money, regardless of currency.
        /// </summary>
        public static readonly Money Zero = new Money(0);

        /// <summary>
        /// A source of randomness for stochastic rounding.
        /// </summary>
        [ThreadStatic]
        private static Random _rng;

        /// <summary>
        /// The amount by which <see cref="_decimalFraction"/> has been scaled up to be a whole number.
        /// </summary>
        private const Decimal FractionScale = 1E9M;

        /// <summary>
        /// The <see cref="Core.Money.Currency"/> this amount represents money in.
        /// </summary>
        private readonly Currency? _currency;

        /// <summary>
        /// The whole units of currency.
        /// </summary>
        private readonly Int64 _units;

        /// <summary>
        /// The fractional units of currency.
        /// </summary>
        private readonly Int32 _decimalFraction;

        /// <summary>
        /// Initializes a new instance of the <see cref="Money"/> struct equal to <paramref name="value"/>.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        public Money(Decimal value)
        {
            checkValue(value);

            _units = (Int64)value;
            _decimalFraction = (Int32)Decimal.Round((value - _units) * FractionScale);

            if (_decimalFraction >= FractionScale)
            {
                _units += 1;
                _decimalFraction = _decimalFraction - (Int32)FractionScale;
            }

            _currency = Currency.FromCurrentCulture();
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Money"/> struct equal to <paramref name="value"/> 
        /// in <paramref name="currency"/>.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <param name="currency">
        /// The currency.
        /// </param>
        public Money(Decimal value, Currency currency)
            : this(value)
        {
            _currency = currency;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Money"/> struct.
        /// </summary>
        /// <param name="units">
        /// The units.
        /// </param>
        /// <param name="fraction">
        /// The fraction.
        /// </param>
        /// <param name="currency">
        /// The currency.
        /// </param>
        private Money(Int64 units, Int32 fraction, Currency currency)
        {
            _units = units;
            _decimalFraction = fraction;
            _currency = currency;
        }

        /// <summary>
        /// Gets the <see cref="Core.Money.Currency"/> which this money value is specified in.
        /// </summary>
        public Currency Currency
        {
            get { return _currency.GetValueOrDefault(Currency.FromCurrentCulture()); }
        }

        /// <summary>
        /// Implicitly converts a <see cref="Byte"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        public static implicit operator Money(Byte value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="SByte"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        [CLSCompliant(false)]
        public static implicit operator Money(SByte value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="Single"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        public static implicit operator Money(Single value)
        {
            return new Money((Decimal)value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="Double"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        public static implicit operator Money(Double value)
        {
            return new Money((Decimal)value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="Decimal"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        public static implicit operator Money(Decimal value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="Money"/> value to <see cref="Decimal"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Decimal"/> value which this <see cref="Money"/> value is equivalent to.
        /// </returns>
        public static implicit operator Decimal(Money value)
        {
            return value.computeValue();
        }

        /// <summary>
        /// Implicitly converts a <see cref="Int16"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        public static implicit operator Money(Int16 value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="Int32"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        public static implicit operator Money(Int32 value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="Int64"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        public static implicit operator Money(Int64 value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="UInt16"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        [CLSCompliant(false)]
        public static implicit operator Money(UInt16 value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="UInt32"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        [CLSCompliant(false)]
        public static implicit operator Money(UInt32 value)
        {
            return new Money(value);
        }

        /// <summary>
        /// Implicitly converts a <see cref="UInt64"/> value to <see cref="Money"/> with no <see cref="Currency"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// A <see cref="Money"/> value with no <see cref="Currency"/> specified.
        /// </returns>
        [CLSCompliant(false)]
        public static implicit operator Money(UInt64 value)
        {
            return new Money(value);
        }

        /// <summary>
        /// A negation operator for a <see cref="Money"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The additive inverse (negation) of the given <paramref name="value"/>.
        /// </returns>
        public static Money operator -(Money value)
        {
            return new Money(-value._units, -value._decimalFraction, value.Currency);
        }

        /// <summary>
        /// An identity operator for a <see cref="Money"/> value.
        /// </summary>
        /// <param name="value">
        /// The value.
        /// </param>
        /// <returns>
        /// The given <paramref name="value"/>.
        /// </returns>
        public static Money operator +(Money value)
        {
            return value;
        }

        /// <summary>
        /// An addition operator for two <see cref="Money"/> values.
        /// </summary>
        /// <param name="left">
        /// The left operand.
        /// </param>
        /// <param name="right">
        /// The right operand.
        /// </param>
        /// <returns>
        /// The sum of <paramref name="left"/> and <paramref name="right"/>.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the currencies of <paramref name="left"/> and <paramref name="right"/> are not equal.
        /// </exception>
        public static Money operator +(Money left, Money right)
        {
            if (left.Currency != right.Currency)
            {
                throw differentCurrencies();
            }

            var fractionSum = left._decimalFraction + right._decimalFraction;

            var overflow = 0L;
            var fractionSign = Math.Sign(fractionSum);
            var absFractionSum = Math.Abs(fractionSum);

            if (absFractionSum >= FractionScale)
            {
                overflow = fractionSign;
                absFractionSum -= (Int32)FractionScale;
                fractionSum = fractionSign * absFractionSum;
            }

            var newUnits = left._units + right._units + overflow;

            if (fractionSign < 0 && Math.Sign(newUnits) > 0)
            {
                newUnits -= 1;
                fractionSum = (Int32)FractionScale - absFractionSum;
            }

            return new Money(newUnits,
                             fractionSum,
                             left.Currency);
        }

        /// <summary>
        /// A subtraction operator for two <see cref="Money"/> values.
        /// </summary>
        /// <param name="left">
        /// The left operand.
        /// </param>
        /// <param name="right">
        /// The right operand.
        /// </param>
        /// <returns>
        /// The difference of <paramref name="left"/> and <paramref name="right"/>.
        /// </returns>
        /// <exception cref="InvalidOperationException">
        /// Thrown if the currencies of <paramref name="left"/> and <paramref name="right"/> are not equal.
        /// </exception>
        public static Money operator -(Money left, Money right)
        {
            if (left.Currency != right.Currency)
            {
                throw differentCurrencies();
            }

            return left + -right;
        }

        /// <summary>
        /// A product operator for a <see cref="Money"/> value and a <see cref="Decimal"/> value.
        /// </summary>
        /// <param name="left">
        /// The left operand.
        /// </param>
        /// <param name="right">
        /// The right operand.
        /// </param>
        /// <returns>
        /// The product of <paramref name="left"/> and <paramref name="right"/>.
        /// </returns>
        public static Money operator *(Money left, Decimal right)
        {
            return ((Decimal)left * right);
        }

        public static Money operator /(Money left, Decimal right)
        {
            return ((Decimal)left / right);
        }

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

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

        public static Boolean operator >(Money left, Money right)
        {
            return left.CompareTo(right) > 0;
        }

        public static Boolean operator <(Money left, Money right)
        {
            return left.CompareTo(right) < 0;
        }

        public static Boolean operator >=(Money left, Money right)
        {
            return left.CompareTo(right) >= 0;
        }

        public static Boolean operator <=(Money left, Money right)
        {
            return left.CompareTo(right) <= 0;
        }

        public static Boolean TryParse(String s, out Money money)
        {
            money = Zero;

            if (s == null)
            {
                return false;
            }

            s = s.Trim();

            if (s == String.Empty)
            {
                return false;
            }

            Currency? currency = null;
            Currency currencyValue;

            // Check for currency symbol (e.g. $, £)
            if (!Currency.TryParse(s.Substring(0, 1), out currencyValue))
            {
                // Check for currency ISO code (e.g. USD, GBP)
                if (s.Length > 2 && Currency.TryParse(s.Substring(0, 3), out currencyValue))
                {
                    s = s.Substring(3);
                    currency = currencyValue;
                }
            }
            else
            {
                s = s.Substring(1);
                currency = currencyValue;
            }

            Decimal value;

            if (!Decimal.TryParse(s, out value))
            {
                return false;
            }

            money = currency != null ? new Money(value, currency.Value) : new Money(value);

            return true;
        }

        public Money Round(RoundingPlaces places, MidpointRoundingRule rounding = MidpointRoundingRule.ToEven)
        {
            Money remainder;
            return Round(places, rounding, out remainder);
        }

        public Money Round(RoundingPlaces places, MidpointRoundingRule rounding, out Money remainder)
        {
            Int64 unit;

            var placesExponent = getExponentFromPlaces(places);
            var fraction = roundFraction(placesExponent, rounding, out unit);
            var units = _units + unit;
            remainder = new Money(0, _decimalFraction - fraction, Currency);

            return new Money(units, fraction, Currency);
        }

        private Int32 roundFraction(Int32 exponent, MidpointRoundingRule rounding, out Int64 unit)
        {
            var denominator = FractionScale / (Decimal)Math.Pow(10, exponent);
            var fraction = _decimalFraction / denominator;

            switch (rounding)
            {
                case MidpointRoundingRule.ToEven:
                    fraction = Math.Round(fraction, MidpointRounding.ToEven);
                    break;
                case MidpointRoundingRule.AwayFromZero:
                    {
                        var sign = Math.Sign(fraction);
                        fraction = Math.Abs(fraction);           // make positive
                        fraction = Math.Floor(fraction + 0.5M);  // round UP
                        fraction *= sign;                        // reapply sign
                        break;
                    }
                case MidpointRoundingRule.TowardZero:
                    {
                        var sign = Math.Sign(fraction);
                        fraction = Math.Abs(fraction);           // make positive
                        fraction = Math.Floor(fraction + 0.5M);  // round DOWN
                        fraction *= sign;                        // reapply sign
                        break;
                    }
                case MidpointRoundingRule.Up:
                    fraction = Math.Floor(fraction + 0.5M);
                    break;
                case MidpointRoundingRule.Down:
                    fraction = Math.Ceiling(fraction - 0.5M);
                    break;
                case MidpointRoundingRule.Stochastic:
                    if (_rng == null)
                    {
                        _rng = new MersenneTwister();
                    }

                    var coinFlip = _rng.NextDouble();

                    if (coinFlip >= 0.5)
                    {
                        goto case MidpointRoundingRule.Up;
                    }

                    goto case MidpointRoundingRule.Down;
                default:
                    throw new ArgumentOutOfRangeException("rounding");
            }

            fraction *= denominator;

            if (fraction >= FractionScale)
            {
                unit = 1;
                fraction = fraction - (Int32)FractionScale;
            }
            else
            {
                unit = 0;
            }

            return (Int32)fraction;
        }

        private static Int32 getExponentFromPlaces(RoundingPlaces places)
        {
            switch (places)
            {
                case RoundingPlaces.Zero:
                    return 0;
                case RoundingPlaces.One:
                    return 1;
                case RoundingPlaces.Two:
                    return 2;
                case RoundingPlaces.Three:
                    return 3;
                case RoundingPlaces.Four:
                    return 4;
                case RoundingPlaces.Five:
                    return 5;
                case RoundingPlaces.Six:
                    return 6;
                case RoundingPlaces.Seven:
                    return 7;
                case RoundingPlaces.Eight:
                    return 8;
                case RoundingPlaces.Nine:
                    return 9;
                default:
                    throw new ArgumentOutOfRangeException("places");
            }
        }

        public override Int32 GetHashCode()
        {
            unchecked
            {
                return (397 * _units.GetHashCode()) ^ _currency.GetHashCode();
            }
        }

        public override Boolean Equals(Object obj)
        {
            if (!(obj is Money))
            {
                return false;
            }

            var other = (Money)obj;
            return Equals(other);
        }

        public override String ToString()
        {
            return computeValue().ToString("C", (IFormatProvider)_currency ?? NumberFormatInfo.CurrentInfo);
        }

        public String ToString(String format)
        {
            return computeValue().ToString(format, (IFormatProvider)_currency ?? NumberFormatInfo.CurrentInfo);
        }

        #region Implementation of IEquatable<Money>

        public Boolean Equals(Money other)
        {
            checkCurrencies(other);

            return _units == other._units &&
                   _decimalFraction == other._decimalFraction;
        }

        #endregion

        #region Implementation of IComparable<Money>

        public Int32 CompareTo(Money other)
        {
            checkCurrencies(other);

            var unitCompare = _units.CompareTo(other._units);

            return unitCompare == 0
                       ? _decimalFraction.CompareTo(other._decimalFraction)
                       : unitCompare;
        }

        #endregion

        #region Implementation of IFormattable

        public String ToString(String format, IFormatProvider formatProvider)
        {
            return computeValue().ToString(format, formatProvider);
        }

        #endregion

        #region Implementation of IComparable

        int IComparable.CompareTo(object obj)
        {
            if (obj is Money)
            {
                return CompareTo((Money)obj);
            }

            throw new InvalidOperationException("Object is not a " + GetType() + " instance.");
        }

        #endregion

        #region Implementation of IConvertible

        public TypeCode GetTypeCode()
        {
            return TypeCode.Object;
        }

        public Boolean ToBoolean(IFormatProvider provider)
        {
            return _units == 0 && _decimalFraction == 0;
        }

        public Char ToChar(IFormatProvider provider)
        {
            throw new NotSupportedException();
        }

        [CLSCompliant(false)]
        public SByte ToSByte(IFormatProvider provider)
        {
            return (SByte)computeValue();
        }

        public Byte ToByte(IFormatProvider provider)
        {
            return (Byte)computeValue();
        }

        public Int16 ToInt16(IFormatProvider provider)
        {
            return (Int16)computeValue();
        }

        [CLSCompliant(false)]
        public UInt16 ToUInt16(IFormatProvider provider)
        {
            return (UInt16)computeValue();
        }

        public Int32 ToInt32(IFormatProvider provider)
        {
            return (Int32)computeValue();
        }

        [CLSCompliant(false)]
        public UInt32 ToUInt32(IFormatProvider provider)
        {
            return (UInt32)computeValue();
        }

        public Int64 ToInt64(IFormatProvider provider)
        {
            return (Int64)computeValue();
        }

        [CLSCompliant(false)]
        public UInt64 ToUInt64(IFormatProvider provider)
        {
            return (UInt64)computeValue();
        }

        public Single ToSingle(IFormatProvider provider)
        {
            return (Single)computeValue();
        }

        public Double ToDouble(IFormatProvider provider)
        {
            return (Double)computeValue();
        }

        public Decimal ToDecimal(IFormatProvider provider)
        {
            return computeValue();
        }

        public DateTime ToDateTime(IFormatProvider provider)
        {
            throw new NotSupportedException();
        }

        public String ToString(IFormatProvider provider)
        {
            return ((Decimal)this).ToString(provider);
        }

        public Object ToType(Type conversionType, IFormatProvider provider)
        {
            throw new NotSupportedException();
        }

        #endregion

        private Decimal computeValue()
        {
            return _units + _decimalFraction / FractionScale;
        }

        private static InvalidOperationException differentCurrencies()
        {
            return new InvalidOperationException("Money values are in different " +
                                                 "currencies. Convert to the same " +
                                                 "currency before performing " +
                                                 "operations on the values.");
        }

        private static void checkValue(Decimal value)
        {
            if (value < Int64.MinValue || value > Int64.MaxValue)
            {
                throw new ArgumentOutOfRangeException("value",
                                                      value,
                                                      "Money value must be between " +
                                                      Int64.MinValue + " and " +
                                                      Int64.MaxValue);
            }
        }

        private void checkCurrencies(Money other)
        {
            if (other.Currency != Currency)
            {
                throw differentCurrencies();
            }
        }

        private String getDebugView()
        {
            return ToString() +
                   String.Format(" ({0} {1})",
                                 ToDecimal(CultureInfo.CurrentCulture),
                                 Currency == Currency.None ? "<Unspecified Currency>" : Currency.Name);
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Architect
United States United States
I'm a software engineer with 25 years of experience in areas from game and simulation development, enterprise development, systems management, machine learning, real-time and embedded systems development and geospaitial systems development.

You can find more of my work at http://www.codeplex.com and my articles at http://vectordotnet.blogspot.com/ and http://dotnoted.spaces.live.com.

Comments and Discussions