Introduction
So my next .NET project started and sure enough, there was the requirement again to support international customers. Now, most of the systems I work on have quite a bit of financial functionality in them and therefore the handling of money is an important aspect. I have found that most developers like to take the simple approach to handling money, supporting only a single currency by solely using decimal type to store an amount and not explicitly naming the currency. Instead the currency is first made explicit when it is presented on a user interface, adding a currency symbol as it is stored in the “active culture info” (which generally means: the server’s culture settings).
I don’t know about you, but if some system administered some amount of my money, I would be very worried if it simply stored: 1,000,000. Especially if my money were in Euro and the system would be a web system running in the US. Chances are, the system’s implicit currency is USD and the current difference between USD 1,000,000 and EUR 1,000,000 is currently about USD 476,500, which is enough to give me quite big headache! ;)
I think we can all agree that when there is a hint that our system might be confronted with cross-border business, it is a very good architectural practice to store money not simply as a number, but also with the appropriate unit. Martin Fowler has already suggested this long ago (1996) in his book Analysis Patterns, where he discussed the Money analysis pattern to be a specialized version of the Quantity analysis pattern. In his Patterns of Enterprise Application Architecture he revisits the Money pattern with several code samples. So the most ground breaking work has been done long ago already.
What is there now?
There are different angles to look at money representations:
- From an administrative point of view (as discussed above), recording and computing monetary amounts along with the correct currency;
- From a presentation point of view, describing the way a monetary value is displayed;
- From a geographical point of view, listing what currencies are used in what countries.
In .NET, Microsoft has supplied us with facilities for the last two angles with the CultureInfo and NumberInfo classes. Amazingly, the first angle is completely ignored, even though that is what’s needed for solid financial business functionality.
Despite of the work of people like Martin Fowler, it seems there are not too many people who care about the administration aspect of money in the .NET space. At least, that’s the feeling I get when I set out to search for C# Money implementations. That yields one fairly decent implementation by Jason Hunt. Unfortunately, Jason relies on the presentation-related CultureInfo class to store currency information. I think that it is unnecessarily complicated, since it forces me to instantiate “1,000 of that currency they use in the US” instead of instantiating “1,000 USD”.
What do we need?
As far as I can see now, we need to be able to do the following with a money representation:
- Store monetary amounts as an amount together its currency code.
- Get a string representation of the monetary amount, dependent on how the user is accustomed to see currencies and numbers. Note that this means that a even though USD is an American currency, a Dutch user would not expect to see the American formatting rules applied to a USD amount, but instead the Dutch formatting rules.
The design
Basic properties
The basics of the Money class are straightforward: we need a property to hold the monetary amount and one to hold the currency of that amount.
public sealed class Money : IEquatable<Money>, IComparable, IComparable<Money>
{ private CurrencyCodeKind _currencyCode;private decimal _amount;
Here's a snippet of the CurrencyCodeKind enumeration:
public enum CurrencyCodeKind
{AED = 784,
AFN = 971,
ALL = 8,
AMD = 51,
ANG = 532,
...
}
The same argument holds for the use of the decimal type to store the actual amount: Fowler has shown implementation examples just using (big) integers, which means that in order to know what the right amount is, you’ll always need the application to position the decimal separator correctly. I find that a little too brittle for information that’s that important, hence I choose to go with a decimal datatype that also has an exactly matching representation on database level.
The inner workings
When working with a Money object, we’ll eventually need to know more about its currency than just its ISO code. More specifically we’ll need to know:
- The number of significant decimal digits, needed for correct calculations and rounding
- The currency symbol for display purposes
- The rounding type, needed for rounding of calculation results (discussed later)
- The name of the currency (maybe not necessary, but still convenient
internal class Currency
{public CurrencyCodeKind CurrencyCode { get; private set; }
public string EnglishName { get; private set; }
public string Symbol { get; private set; }
public int SignificantDecimalDigits { get; private set; }
public CurrencyRoundingKind RoundingType { get; private set; }
public sealed class Money : IEquatable<Money>, IComparable, IComparable<Money>
{ #region Properties public CurrencyCodeKind CurrencyCode { get; set; }public decimal Amount { get; set; }
private Currency _currency;private CurrencyRepository _currencyRepository = new CurrencyRepository();
private static int[] _cents = new int[] { 1, 10, 100, 1000 };
#endregion// Initialize a new money with an explicit currency codepublic Money(CurrencyCodeKind currencyCode, decimal amount)
{ if (_currencyRepository.Exists(currencyCode)) {CurrencyCode = currencyCode;
Amount = amount;
_currency = _currencyRepository.Get(CurrencyCode);
}
else {throw new InvalidOperationException("Currency code unknown.");
}
}
// Initialize a new money with the currency code// held in the passed in CultureInfo.public Money(CultureInfo cultureInfo, decimal amount)
{CurrencyCodeKind currencyCode;
try {currencyCode = (CurrencyCodeKind)Enum.Parse(typeof(CurrencyCodeKind), (new RegionInfo(cultureInfo.LCID)).ISOCurrencySymbol);
}
catch (Exception) {throw new InvalidOperationException("Currency code unknown.");
}
if (_currencyRepository.Exists(currencyCode)) {CurrencyCode = currencyCode;
Amount = amount;
_currency = _currencyRepository.Get(CurrencyCode);
}
else {throw new InvalidOperationException("Currency code unknown.");
}
}
// Initialize a new money with the currency code of the // runtime enviroment's current regional settings.public Money(decimal amount)
{CurrencyCodeKind currencyCode;
try { currencyCode = (CurrencyCodeKind)Enum.Parse(typeof(CurrencyCodeKind), RegionInfo.CurrentRegion.ISOCurrencySymbol);}
catch (Exception) {throw new InvalidOperationException("Currency code unknown.");
}
if (_currencyRepository.Exists(currencyCode)) {CurrencyCode = currencyCode;
Amount = amount;
_currency = _currencyRepository.Get(CurrencyCode);
}
else {throw new InvalidOperationException("Currency code unknown.");
}
}
internal class CurrencyRepository
{ // List of all currencies with their properties.static private Dictionary<CurrencyCodeKind, Currency> _currencyDictionary =
new Dictionary<CurrencyCodeKind, Currency>() { {CurrencyCodeKind.AED, new Currency(CurrencyCodeKind.AED, "United Arab Emirates dirham", "¤", 2)}, {CurrencyCodeKind.AFN, new Currency(CurrencyCodeKind.AFN, "Afghani", "¤", 2)}, {CurrencyCodeKind.ALL, new Currency(CurrencyCodeKind.ALL, "Lek", "¤", 2)}, {CurrencyCodeKind.AMD, new Currency(CurrencyCodeKind.AMD, "Armenian dram", "¤", 2)}, {CurrencyCodeKind.ANG, new Currency(CurrencyCodeKind.ANG, "Netherlands Antillean guilder", "ƒ", 2)},...
- the currency list hardly ever changes;
- when a new currency is introduced, it needs to be added to the CurrencyCodeKind enumeration anyway, thus requiring a new system build; it is only natural to then also update the hard coded currency object list;
- you don’t really want this core configuration of the way you handle money to be stored in an easy to corrupt/misuse configuration file.
One additional thing to note is that I quickly found out that the default character set does not hold all currency symbols. Therefore a lot of currency symbols are represented by the generic currency sign. A future enhancement will be to add the Unicode of the symbol to the properties of the Currency class.
Operators
The implementation of most arithmetic operators are straightforward; here’s the >, >=, ==, <=, <, !=, +, and - operators:
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.CurrencyCode, first.Amount + second.Amount);
}
public static Money operator -(Money first, Money second)
{AssertSameCurrency(first, second);
return new Money(first.CurrencyCode, first.Amount - second.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.CurrencyCode == second.CurrencyCode && 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(money.CurrencyCode, money.Amount * value).WithRoundedAmount();
}
public static Money operator /(Money money, decimal value)
{if (money == null) throw new ArgumentNullException("money");
return new Money(money.CurrencyCode, money.Amount / value).WithRoundedAmount();
}
internal Money WithRoundedAmount(){ switch (_currency.RoundingType) { case CurrencyRoundingKind.Argentinian:decimal insignificantDecimals1 = Amount * CentFactor() - (long)Math.Truncate(Amount * CentFactor());
if (insignificantDecimals1 < 0.3m) {return this.WithTruncatedAmount();
}
else { if (insignificantDecimals1 > 0.7m) { Amount = decimal.Round(Amount, _currency.SignificantDecimalDigits, MidpointRounding.AwayFromZero);return this;
}
else { Amount = ((long)Math.Truncate(Amount * CentFactor()) + 0.5m) / CentFactor();return this;
}
}
case CurrencyRoundingKind.Swiss:decimal insignificantDecimals2 = Amount * CentFactor() - (long)Math.Truncate(Amount * CentFactor());
if (insignificantDecimals2 < 0.26m) {return this.WithTruncatedAmount();
}
else { if (insignificantDecimals2 > 0.75m) { Amount = decimal.Round(Amount, _currency.SignificantDecimalDigits, MidpointRounding.AwayFromZero);return this;
}
else { Amount = ((long)Math.Truncate(Amount * CentFactor()) + 0.5m) / CentFactor();return this;
}
}
default: Amount = decimal.Round(Amount, _currency.SignificantDecimalDigits, MidpointRounding.AwayFromZero);return this;
}
}
- If the last digit is less than 5, drop it, otherwise,
- If the last digit is equal to or above 5, add one to the previous digit and drop the last digit.
internal enum CurrencyRoundingKind
{AwayFromZero,
Swiss,
Argentinian
}
Allocation
With rounding comes loss of information. If you use the division operator to calculate how to divide $0.05 amongst 3 people, you’ll have $0.01 to give to each person. $0.02 is lost due to rounding. Everyone knows about those exploit stories of programmers in the early days who took advantage of this loss due to rounding. We’ve wizened up since then, and Fowler introduced an Allocate algorithm to take care of situations like this. It’s a simple algorithm that actually plays the “rings on a sticks” game:
Say you have 5 rings and 3 sticks to divide them eenly over, you’ll get the following:
public Money[] Allocate(int n)
{ Money lowResult = new Money(CurrencyCode, Amount / n).WithTruncatedAmount();Money highResult = lowResult.Amount + new Money(this.CurrencyCode, 1.0m/CentFactor());
Money[] results = new Money[n];int remainder = (int)((Amount * CentFactor()) % n);
for (int i = 0; i < remainder; i++) results[i] = highResult;
for (int i = remainder; i < n; i++) results[i] = lowResult;
return results;}
- the remainder is evenly distributed over the parts
- the sum of the resulting amounts is equal to the divided amount.
public Money[] Allocate(int[] ratios)
{ decimal total = 0m;for (int i = 0; i < ratios.Length; i++) total += ratios[i];
Money remainder = this.Copy(); Money[] results = new Money[ratios.Length]; // First distribute the truncated amounts over all partsfor (int i = 0; i < results.Length; i++) {
results[i] = new Money(CurrencyCode, (this.Amount * ratios[i]) / total).WithTruncatedAmount();
remainder.Subtract(results[i]);
}
// Now determine how many cents are leftlong centsLeftToDivide = (long)(remainder.Amount / (1.0m / CentFactor()));
// Distribute those remaining cents over the different partsfor (int i = 0; i < centsLeftToDivide; i++) {
results[i].Add(new Money(this.CurrencyCode, 1.0m / CentFactor()));
}
return results;}
Formatting for displaying
Lastly, we'll need some formatting on a Money object for it to present itself neatly to a user. This formatting is included in the 3 variants of the ToString() operation:
- with no parameters (generally not recommended); the number formatting rules are taken from the OS' current culture info and the three letter currency code is used as the currency indicator;
- with a CultureInfo object as parameter (recommended); the number formatting rules are derived from the passed in CultureInfo object and the three letter currency code is used as the currency indicator;
- with a CultureInfo object as parameter and an indicator whether or not to use the currency symbol instead of the three letter currency code as currency indicator (equally recommended); the number formatting rules are derived from the passed in CultureInfo object.
public string ToString()
{return Amount.ToString("C", GetCurrencyFormatter(CultureInfo.CurrentCulture, false));
}
public string ToString(CultureInfo cultureInfo)
{return Amount.ToString("C", GetCurrencyFormatter(cultureInfo, false));
}
public string ToString(CultureInfo cultureInfo, bool useSymbol)
{return Amount.ToString("C", GetCurrencyFormatter(cultureInfo, useSymbol));
}
A central role is played by the GetCurrencyFormatter operation, which returns a NumberFormatInfo object that contains the desired currency indicator as well as an adapter currency formatting pattern that always makes sure there is a space between the currency indicator and the amount, if the currency indicator is a three letter currency code.
private NumberFormatInfo GetCurrencyFormatter(CultureInfo cultureInfo, bool useSymbol)
{ // Get all the basics from the passed in culture info (not before it is // guaranteed to use global settings) NumberFormatInfo LocalNumberFormatter = (NumberFormatInfo)this.GetGlobalCultureInfo(cultureInfo).NumberFormat.Clone(); // Overwrite the culture settings with the specifics of the current currencyCurrency currency = _currencyRepository.Get(CurrencyCode);
LocalNumberFormatter.CurrencyDecimalDigits = currency.SignificantDecimalDigits;
if (useSymbol) {LocalNumberFormatter.CurrencySymbol = currency.Symbol;
}
else { LocalNumberFormatter.CurrencySymbol = Enum.GetName(typeof(CurrencyCodeKind), CurrencyCode); // If we use the ISO code, then we need to make sure the patterns always include // a space between the ISO code and the amount. if (LocalNumberFormatter.CurrencyPositivePattern <= 1) {LocalNumberFormatter.CurrencyPositivePattern = LocalNumberFormatter.CurrencyPositivePattern + 2;
}
switch (LocalNumberFormatter.CurrencyNegativePattern) {case 0: // ($n)
LocalNumberFormatter.CurrencyNegativePattern = 14;
break;case 1: // -$n
LocalNumberFormatter.CurrencyNegativePattern = 9;
break;case 2: // $-n
LocalNumberFormatter.CurrencyNegativePattern = 12;
break;case 3: // $n-
LocalNumberFormatter.CurrencyNegativePattern = 11;
break;case 4: // (n$)
LocalNumberFormatter.CurrencyNegativePattern = 15;
break;case 5: // -n$
LocalNumberFormatter.CurrencyNegativePattern = 8;
break;case 6: // n-$
LocalNumberFormatter.CurrencyNegativePattern = 13;
break;case 7: // n$-
LocalNumberFormatter.CurrencyNegativePattern = 10;
break; default: break;}
}
return LocalNumberFormatter;}
private CultureInfo GetGlobalCultureInfo(CultureInfo cultureInfo){ if (!cultureInfo.UseUserOverride) { //The passed in culture info is already in "global mode" return cultureInfo;}
else { // The passed in culture info is set to use OS specific // culture settings; this should be changed.return new CultureInfo(cultureInfo.Name, false);
}
}
Wrapping it up
Jee wizz... that's a lot of explainin', Batman! Yeah, well... there's still a bit more in the actual code that I have not covered in detail, but this story should have you covered for regular use. If -after this whole epistle- you're still interested in the actual code, you can get your copy here:
Download the C# Money implementation
Oh yeah... do note that this all is built in Visual Studio 2008 Professional, so you might run into some problems if you try to use it directly in an older version of Visual Studio. Sorry for that!
5 comments:
I feel like one of those scientists in the 19th century who simultaneously discovered something that another scientist discovered. I just authored a money type using the same basic pattern. I published it on the same day, here: http://www.codeplex.com/MoneyType. There are a few differences which are notable - I've devolved the responsibility for money allocation to another class, made Currency a bit more functional to take advantage of built-in string formatting, and used scaled-integer storage (as did Fowler and Foemmel) to avoid the problems with using floating-point types such as System.Decimal.
Haha! What an insane coincidence this is! :D
I’m going to check out your solution very soon and get back to you! One remark: de System.Decimal is not a floating point type. The latter obviously suffers from imprecisions, but the Decimal type is precise and therefore is suited to be used for currency amounts.
I know, it is an astounding coincidence! I was thinking all along, "Wow, I can't believe no one has tackled this problem comprehensively yet." I'll bet you had just the same thoughts!
System.Decimal is a floating-point. It's a common misconception that it isn't due to the fact that it has so many bits of precision. From the MSDN docs (http://msdn.microsoft.com/en-us/library/system.decimal.aspx):
A decimal number is a floating-point value that consists of a sign, a numeric value where each digit in the value ranges from 0 to 9, and a scaling factor that indicates the position of a floating decimal point that separates the integral and fractional parts of the numeric value.
So, Decimal is not immune to base-10 fractional numeric representation issues. Also from the docs:
The Decimal type does not eliminate the need for rounding. Rather, it minimizes errors due to rounding. For example, the following code produces a result of 0.9999999999999999999999999999 rather than 1.
I indeed had the exact same thought. I have tackled it partially before, but it never materialized into a comprehensive solution, until recently. And yesterday the stars aligned when I finally wrote the last couple of lines on that whole story and managed to publish it onto my blog at the same time as you. Amazing!
Darn, I know I should have checked the docs on the floating-point thing! ;) Thanks for pointing that out. I’m going to look into that more closely. I still like the idea of having matching types on data storage level and code level. On the other hand, integer calculations are sure to be faster than the decimal calculations (as they are 10-based). But first and foremost the solution should be “safe” (whatever that means *exactly*). Interestingly enough, judging by the amount of articles on the theory on this, there still seems to be a bit of grey area around this.
Anyway, I downloaded your MoneyType and I hope to get around reviewing it this weekend. I’ll post my comments on your site.
Thanks for the referral. Just a note, the reason I left the CultureInfo reference in my implementation was that I didn't want to have to maintain a CurrencyCodeKind enumeration as countries were added/removed. I also wanted to leave the method of how to properly display the currency ("$100.00 CAD" english canadian, "100,00 $ CAD" french canadian - reference: http://www.sterlingdata.com/canada.htm) to the OS.
Post a Comment