"For money, always decimal"?
The rule to use decimal
for money is helpful because most currencies have decimal units. By using decimal arithmetic, you avoid introducing and accumulating round-off error.
Financial Class functions use floating-point for a few reasons:
- They don't internally accumulate -- they are based on a closed-form exponential/logarithmic computation, not iteration and summation over periods.
- They tend not to use or produce exact decimal values. For example, an exact decimal annual interest rate divided by 12 monthly payments becomes a repeating decimal.
- They are intended primarily for decision support, and in the end have little applicability to actual bookkeeping.
Pmt
and rounding may determine the nominal monthly payment, but once that amount is determined, balance accumulation -- payments made, interest charges applied, etc. -- happens in decimal
. Also, late or advance payments, payment holidays, and other such non-uniformities would invalidate the projected amortization provided by the financial functions.
Here's an interesting discussion regarding exactly this topic: http://www.vbforums.com/showthread.php?t=524101
About 1/3 of the way down someone explains that it uses Double because the VB.NET functions were implemented to work exactly the same as VB6. VB6 doesn't have a decimal type, which is why it uses double.
So, it seems that if accuracy is important, you should not use these functions.
The answers to this question have some promising alternatives - just ignore the accepted answer that suggests using the VB library.
The previously linked question has been deleted, so here are some of the suggestions I was referencing (note: I have not tried these, YMMV)
Implementations of excel financial functions in F# - can be used from any .NET language, does not require F# reference
QuantLib may be able to be exported/generated for C#. See this SO question/answer for more info
You can use this class:
public class Financial
{
#region Methods
public static decimal IPmt(decimal Rate, decimal Per, decimal NPer, decimal PV, decimal FV, FinancialEnumDueDate Due)
{
decimal num;
if (Due != FinancialEnumDueDate.EndOfPeriod)
{
num = 2;
}
else
{
num = 1;
}
if ((Per <= 0) || (Per >= (NPer + 1)))
{
//Argument_InvalidValue1=
throw new ArgumentException("Argument 'Per' is not a valid value.");
}
if ((Due != FinancialEnumDueDate.EndOfPeriod) && (Per == 1))
{
return 0;
}
decimal pmt = Pmt(Rate, NPer, PV, FV, Due);
if (Due != FinancialEnumDueDate.EndOfPeriod)
{
PV += pmt;
}
return (FV_Internal(Rate, Per - num, pmt, PV, FinancialEnumDueDate.EndOfPeriod) * Rate);
}
public static decimal PPmt(decimal Rate, decimal Per, decimal NPer, decimal PV, decimal FV, FinancialEnumDueDate Due)
{
if ((Per <= 0) || (Per >= (NPer + 1)))
{
throw new ArgumentException("Argument 'Per' is not valid.");
}
decimal num2 = Pmt(Rate, NPer, PV, FV, Due);
decimal num = IPmt(Rate, Per, NPer, PV, FV, Due);
return (num2 - num);
}
static decimal FV_Internal(decimal Rate, decimal NPer, decimal Pmt, decimal PV, FinancialEnumDueDate Due)
{
decimal num;
if (Rate == 0)
{
return (-PV - (Pmt * NPer));
}
if (Due != FinancialEnumDueDate.EndOfPeriod)
{
num = 1 + Rate;
}
else
{
num = 1;
}
decimal x = 1 + Rate;
decimal num2 = (decimal)Math.Pow((double)x, (double)NPer);
return ((-PV * num2) - (((Pmt / Rate) * num) * (num2 - 1)));
}
static decimal Pmt(decimal Rate, decimal NPer, decimal PV, decimal FV, FinancialEnumDueDate Due)
{
decimal num;
if (NPer == 0)
{
throw new ArgumentException("Argument NPer is not a valid value.");
}
if (Rate == 0)
{
return ((-FV - PV) / NPer);
}
if (Due != FinancialEnumDueDate.EndOfPeriod)
{
num = 1 + Rate;
}
else
{
num = 1;
}
decimal x = Rate + 1;
decimal num2 = (decimal)Math.Pow((double)x, (double)NPer);
return (((-FV - (PV * num2)) / (num * (num2 - 1))) * Rate);
}
#endregion Methods
}