Improving Accuracy Of Decimal Calculations

J

Jeff Gaines

I am writing an app in C# to keep details of expenditure in relation to a
small block of flats.
There are 9 flats and costs are apportioned based on the lease terms, in
all about 12 different categories each with a different percentage cost
for each flat. I store the amounts and the proportions as decimal numbers
with 2 decimal places in an Access database.

I am using one central function to do the calculations:

public static Decimal CalculateProportion(Decimal amount, Decimal
proportion)
{
double famount = (double)amount;
double fproportion = (double)proportion;
double divisor = 100F;

double result = (famount * fproportion) / divisor;

double result1 = Math.Round(result, 2, MidpointRounding.AwayFromZero);

// For comparison
double result2 = Math.Round(result, 2, MidpointRounding.ToEven);

return Convert.ToDecimal(result1);
}

I appreciate the issues around computers representing numbers and I want
to get as close as I can to the result that would be produced by a
calculator or an Excel spreadsheet - at the moment although some results
are OK other are out by a penny (compared to Excel) which will raise
questions among the tenants.

Can I improve the accuracy of this function in any way? As I said all the
calculations are done in the one function so improving that function will
improve the accuracy of the whole app.

Many thanks.
 
M

Martin Honnen

Jeff said:
public static Decimal CalculateProportion(Decimal amount, Decimal
proportion)
{
double famount = (double)amount;
double fproportion = (double)proportion;
double divisor = 100F;

double result = (famount * fproportion) / divisor;

double result1 = Math.Round(result, 2, MidpointRounding.AwayFromZero);

// For comparison
double result2 = Math.Round(result, 2, MidpointRounding.ToEven);

return Convert.ToDecimal(result1);
}

Can I improve the accuracy of this function in any way? As I said all
the calculations are done in the one function so improving that function
will improve the accuracy of the whole app.

Well you pass in two values of type decimal and want a decimal out but
then in the function you convert value to doubles first which can lose
precision. Why don't you simply use the decimal values for those
computations? Math.Round is defined for decimals too I think.
 
J

Jeff Johnson

Well you pass in two values of type decimal and want a decimal out but
then in the function you convert value to doubles first which can lose
precision. Why don't you simply use the decimal values for those
computations? Math.Round is defined for decimals too I think.

Yup. Just about all the Math.xxx() methods that work with doubles also work
with decimals. (Min, Max, etc.)
 
J

Jeff Gaines

Well you pass in two values of type decimal and want a decimal out but
then in the function you convert value to doubles first which can lose
precision. Why don't you simply use the decimal values for those
computations? Math.Round is defined for decimals too I think.

Thanks Martin :)

I think I have been suffering from brain fade, I had convinced myself a
double had greater precision than a decimal. Your reply made me go and
look at the help again. I'll stick to decimals!
 
J

James Parker

I am writing an app in C# to keep details of expenditure in relation to a
small block of flats.
There are 9 flats and costs are apportioned based on the lease terms, in
all about 12 different categories each with a different percentage cost
for each flat. I store the amounts and the proportions as decimal numbers
with 2 decimal places in an Access database.

I am using one central function to do the calculations:

public static Decimal CalculateProportion(Decimal amount, Decimal
proportion)
{
double famount = (double)amount;
double fproportion = (double)proportion;
double divisor = 100F;

double result = (famount * fproportion) / divisor;

double result1 = Math.Round(result, 2, MidpointRounding.AwayFromZero);

// For comparison
double result2 = Math.Round(result, 2, MidpointRounding.ToEven);

return Convert.ToDecimal(result1);
}

I appreciate the issues around computers representing numbers and I want
to get as close as I can to the result that would be produced by a
calculator or an Excel spreadsheet - at the moment although some results
are OK other are out by a penny (compared to Excel) which will raise
questions among the tenants.

Can I improve the accuracy of this function in any way? As I said all the
calculations are done in the one function so improving that function will
improve the accuracy of the whole app.

Many thanks.


I wrote a class several years back when at Uni that might help.
I dont know if you require this amount of decimal accuracy.
The class below requires the use of
http://www.codeproject.com/KB/cs/biginteger.aspx

// BigDecimal.cs created with MonoDevelop
// User: j1mb0jay at 04:38 10/04/2009

using System;
using System.Text;

namespace JJMath.Big
{
public class BigDecimal
{
#region Finals
private static int SCALE =150;
#endregion

#region Instance Variables
private BigInteger[] decimal_as_fraction;
public BigInteger[] Fraction { get { return this.decimal_as_fraction; } }
#endregion

#region Constructors
public BigDecimal(String decimal_as_string)
{
this.decimal_as_fraction = this.ConvertDecimalToFraction(decimal_as_string).Fraction;
}

public BigDecimal(BigInteger[] fraction)
{
this.decimal_as_fraction = fraction;
}

public BigDecimal(int number)
{
this.decimal_as_fraction = this.ConvertDecimalToFraction(number.ToString()).Fraction;
}

public BigDecimal(long number)
{
this.decimal_as_fraction = this.ConvertDecimalToFraction(number.ToString()).Fraction;
}

public BigDecimal(BigInteger number)
{
this.decimal_as_fraction = this.ConvertDecimalToFraction(number.ToString()).Fraction;
}
#endregion

#region Conversions
private BigDecimal ConvertDecimalToFraction(String decimal_as_string)
{
if(decimal_as_string.IndexOf('.') != -1)
{
bool isNegative = false;
if(decimal_as_string.StartsWith("-"))
{
isNegative = true;
decimal_as_string = decimal_as_string.Replace("-","");
}

int digits_after_decimal_point = ((decimal_as_string.Length - decimal_as_string.IndexOf('.')) - 1);
BigInteger numerator = new BigInteger(decimal_as_string.Substring( (decimal_as_string.IndexOf(".") + 1)),10);
BigInteger denominator = BigInteger.Pow(10,digits_after_decimal_point);

BigInteger whole_num = new BigInteger(decimal_as_string.Substring(0,decimal_as_string.IndexOf(".")),10);
numerator+= (denominator * whole_num);

if(isNegative)
numerator*=-1;

return Simplify(new BigInteger[] { numerator , denominator });
//return new BigDecimal(new BigInteger[] { numerator , denominator });
}
else
{
return new BigDecimal(new BigInteger[] { new BigInteger(decimal_as_string,10) , 1 });
}
}
#endregion

#region Simplifications
private BigDecimal Simplify(BigInteger[] fraction)
{
BigInteger gcd = BigInteger.GCD(fraction[0],fraction[1]);
while(gcd != 1)
{
fraction[0] = (fraction[0] / gcd);
fraction[1] = (fraction[1] / gcd);
gcd = BigInteger.GCD(fraction[0],fraction[1]);
}
if(fraction[0] > 0 && fraction[1] < 0)
{
fraction[0]*= -1;
fraction[1]*= -1;
}
return new BigDecimal(fraction);
}
#endregion

#region Basic Maths Functions
public BigDecimal Multiply(BigDecimal multiply)
{
return Simplify( new BigInteger[] { (this.decimal_as_fraction[0] * multiply.Fraction[0]) ,
(this.decimal_as_fraction[1] * multiply.Fraction[1]) });

//return new BigDecimal(new BigInteger[] { (this.decimal_as_fraction[0] * multiply.Fraction[0]) ,
//(this.decimal_as_fraction[1] * multiply.Fraction[1]) });
}

public BigDecimal Divide(BigDecimal divide)
{
return Multiply(new BigDecimal(new BigInteger[] { divide.Fraction[1] , divide.Fraction[0] }));
}

public BigDecimal Add(BigDecimal adder)
{
return this.Simplify(new BigInteger[] { ((this.decimal_as_fraction[0] * adder.Fraction[1]) +
(this.decimal_as_fraction[1] * adder.Fraction[0])) ,
adder.Fraction[1] * this.decimal_as_fraction[1] });
}

public BigDecimal Subtract(BigDecimal adder)
{
return this.Simplify(new BigInteger[] { ((this.decimal_as_fraction[0] * adder.Fraction[1]) -
(this.decimal_as_fraction[1] * adder.Fraction[0])) ,
adder.Fraction[1] * this.decimal_as_fraction[1] });
}
#endregion

#region Complex Maths

private BigInteger[][] CalculateAlpha(int n)
{
StringBuilder test = new StringBuilder(this.ToDecimal());
if(!test.ToString().Contains("."))
{
test.Append(".");
for(int i = 0; i < ((BigDecimal.SCALE + 1) * n); i++)
{
test.Append("0");
}
}
else
{
int current_decimal_places = test.ToString().Split('.')[1].Length;
int required_decimal_places = (BigDecimal.SCALE + 1) * n;
if(current_decimal_places < required_decimal_places)
{
for(int i = 0; i < (required_decimal_places - current_decimal_places); i++)
{
test.Append("0");
}
}
}

String[] parts = test.ToString().Split('.');
StringBuilder whole_number = new StringBuilder(parts[0]);
while(whole_number.ToString().Length % n != 0)
{
whole_number.Insert(0,"0");
}
parts[0] = whole_number.ToString();
//Console.WriteLine(parts[0] + "." + parts[1]);

BigInteger[] whole_number_blocks = new BigInteger[(parts[0].Length / n)];
int string_index = 0;
int array_index = 0;
while(string_index < parts[0].Length)
{
whole_number_blocks[array_index] = new BigInteger(parts[0].Substring(string_index,n),10);
array_index++;
string_index+=n;
}

BigInteger[] decimal_number_blocks = new BigInteger[(parts[1].Length / n)];
string_index = 0;
array_index = 0;
while(string_index < parts[1].Length)
{
decimal_number_blocks[array_index] = new BigInteger(parts[1].Substring(string_index,n),10);
array_index++;
string_index+=n;
}

return new BigInteger[][] { whole_number_blocks, decimal_number_blocks};
}

public BigDecimal NthRoot(int n)
{
//Console.WriteLine(n + "th root of " + this.ToDecimal());


BigInteger ten = new BigInteger(10);
BigInteger gamma = new BigInteger(0);
BigInteger new_gamma = new BigInteger(0);
BigInteger beta = new BigInteger(0);
BigInteger[][] alpha_blocks = this.CalculateAlpha(n);
int alpha_index = 0;
int alpha_part = 0;
BigInteger alpha = new BigInteger(alpha_blocks[alpha_part][alpha_index]);
BigInteger r = new BigInteger(0);
BigInteger new_r = new BigInteger(0);

StringBuilder ans = new StringBuilder();
bool hasStarted = false;
while(true)
{
//Console.WriteLine("ALPHA : " + alpha);
while(true)
{

if((BigInteger.Pow(((ten * gamma) + beta),n)) <= (((BigInteger.Pow(ten,n)) * r) + alpha) + ((BigInteger.Pow(ten,n)) * (BigInteger.Pow(gamma,n))))
{
beta++;
}
else
{
beta--;
break;
}
}
//Console.WriteLine("BETA : " + beta);
ans.Append(beta);
new_gamma = ((ten * gamma) + beta);
r = (((BigInteger.Pow(ten,n)) * r) + alpha) - (BigInteger.Pow(((ten * gamma) + beta),n) - ((BigInteger.Pow(ten,n)) * (BigInteger.Pow(gamma,n))));
gamma = new_gamma;
//Console.WriteLine("GAMMA : " + gamma);
//Console.WriteLine("R : " + r);

if(r == 0 && hasStarted)
return new BigDecimal(ans.ToString());

alpha_index++;
if(alpha_part == 1 && alpha_index >= alpha_blocks[1].Length)
return new BigDecimal(ans.ToString());

if(alpha_part == 0 && alpha_index >= alpha_blocks[0].Length)
{
alpha_part = 1;
alpha_index = 0;
ans.Append(".");
}
alpha = alpha_blocks[alpha_part][alpha_index];
if(alpha > 0)
hasStarted = true;
beta = new BigInteger(0);
//Console.WriteLine("\r\n\r\n");
}
}

public BigDecimal Pow(BigInteger power)
{
if(power < 0)
{
power*=-1;
BigInteger tmp = this.Fraction[0];
this.Fraction[0] = this.Fraction[1];
this.Fraction[1] = tmp;
}

return new BigDecimal(new BigInteger[] { BigInteger.Pow(this.Fraction[0],power),
BigInteger.Pow(this.Fraction[1],power) });
}

public BigDecimal Pow(BigDecimal power)
{
//Console.WriteLine(power.ToString());
BigDecimal ans = this.NthRoot( power.Fraction[1].IntValue() ).Pow( power.Fraction[0].IntValue() );
return ans;
}

public BigDecimal Exponential()
{
BigInteger n = 0;
BigDecimal ans = new BigDecimal("0");
BigDecimal.SCALE+=1;
String last_decimal_value = string.Empty;

while(true)
{
ans = ans.Add( (this.Pow(n)).Divide(new BigDecimal(BigInteger.Factorial(n))) );
if(ans.ToDecimal().CompareTo(last_decimal_value) == 0)
break;
n++;
last_decimal_value = ans.ToDecimal();
//Console.WriteLine(last_decimal_value);
}
BigDecimal.SCALE-=1;
return ans;
}

public BigDecimal SIN ()
{
BigInteger n = 0;
BigDecimal ans = new BigDecimal("0");
String last_decimal_value = string.Empty;
int one = 1;
while(true)
{
ans = ans.Add(new BigDecimal( one ).Multiply( this.Pow( (n*2)+1) ).Divide(new BigDecimal(BigInteger.Factorial((2*n)+1))));
//Console.WriteLine(ans.ToDecimal());
if(ans.ToDecimal().CompareTo(last_decimal_value) == 0)
break;
n++;
last_decimal_value = ans.ToDecimal();
one *= -1;
}
return ans;
}

public BigDecimal COS ()
{
BigInteger n = 0;
BigDecimal ans = new BigDecimal("0");
String last_decimal_value = string.Empty;
int one = 1;
while(true)
{
ans = ans.Add(new BigDecimal( one ).Multiply( this.Pow((n*2)) ).Divide(new BigDecimal(BigInteger.Factorial((2*n)))));
//Console.WriteLine(ans.ToDecimal());
if(ans.ToDecimal().CompareTo(last_decimal_value) == 0)
break;
n++;
last_decimal_value = ans.ToDecimal();
one *= -1;
}
return ans;
}

public BigDecimal TAN()
{
return this.SIN().Divide(this.COS());
}

public BigDecimal ARCTAN()
{
BigInteger n = 0;
BigDecimal ans = new BigDecimal("0");
String last_decimal_value = string.Empty;
int one = 1;
while (true)
{
ans = ans.Add(new BigDecimal(one).Multiply(this.Pow((n * 2) + 1)).Divide(new BigDecimal((2 * n) + 1)));
//Console.WriteLine(ans.ToDecimal());

if (ans.ToDecimal().CompareTo(last_decimal_value) == 0)
break;
n++;

last_decimal_value = ans.ToDecimal();
//Console.WriteLine(Environment.TickCount - timer + "ms");
one *= -1;
}
return ans;
}

#endregion

#region Overrides

public override string ToString ()
{
return this.decimal_as_fraction[0] + "\r\n----\r\n" + this.decimal_as_fraction[1] + "\r\n";
}

#endregion

#region Helpers
public bool Equals(BigDecimal toCheck)
{
if (this.Fraction[0] == toCheck.Fraction[0] &&
this.Fraction[1] == toCheck.Fraction[1])
{
return true;
}
return false;
}

public bool IsNegative()
{
if( (this.decimal_as_fraction[0] < 0 && this.decimal_as_fraction[1] > 0) ||
(this.decimal_as_fraction[1] < 0 && this.decimal_as_fraction[0] > 0) )
{
return true;
}
return false;
}

public String ToDecimal()
{
//Console.WriteLine("------------------------");
//Console.WriteLine("Converting to Decimal");
//long start = System.Environment.TickCount;

BigInteger top = this.Fraction[0];
BigInteger bottom = this.Fraction[1];

if(top < 0)
{
top = (top * -1);
}
if(bottom < 0)
{
bottom = (bottom * -1);
}

BigInteger whole_number;
if(top >= bottom)
{
whole_number = top / bottom;

}
else
{
whole_number = 0;
}

BigInteger remainder = top - (whole_number * bottom);
int[] output = new int[BigDecimal.SCALE];
int index = 0;
int nextDigit = -1;

while(remainder != 0)
{
output[index] = ((remainder * 10) / bottom).IntValue();
remainder = (remainder * 10) % bottom;
index++;
if(index >= BigDecimal.SCALE)
{
nextDigit = ((remainder * 10) / bottom).IntValue();
break;
}
}

bool roundFixed = false;
if(nextDigit >= 5)
{
for(int i = (output.Length - 1); i >= 0; i --)
{
if(output != 9)
{
output++;
roundFixed = true;
break;
}
else
{
output = 0;
}
}
if(!roundFixed)
whole_number++;
}

StringBuilder decs = new StringBuilder();
foreach(int i in output)
{
decs.Append(i);
}
String retVal = (whole_number.ToString() + "." + decs.ToString()).Replace("-","").TrimEnd('0');
if(retVal.EndsWith("."))
{
retVal = retVal.Replace(".","");
}
//Console.WriteLine((System.Environment.TickCount - start) + "ms");
//Console.WriteLine("------------------------");

if(this.IsNegative())
return "-"+retVal;
else
return retVal;
}

public BigInteger WholeNumber()
{
return new BigInteger(this.ToDecimal().Split('.')[0],10);
}

public String ToHex()
{
String base10 = this.ToDecimal();
String[] parts = base10.Split('.');
return new BigInteger(parts[0],10).ToHexString() + "." + new BigInteger(parts[1],10).ToHexString();
}

public String ToHexFraction()
{
return this.Fraction[0].ToHexString() + "\r\n-------------\r\n" + this.Fraction[1].ToHexString();
}

public BigDecimal Invert()
{
return new BigDecimal(new BigInteger[] { this.Fraction[1], this.Fraction[0] });
}

#endregion

#region Pi
public static BigDecimal Pi(int loops)
{
BigInteger k = 0;
BigInteger big13 = 13591409;
BigInteger big54 = 545140134;
BigInteger big64 = 640320;
int switching_one = 1;

BigDecimal ans = new BigDecimal(0);
BigDecimal two_over_three = new BigDecimal(3).Divide(new BigDecimal(2));
while (loops > 0)
{
BigInteger three_times_k = (3 * k);

BigInteger top = ((switching_one * BigInteger.Factorial(6 * k)) * (big13 + (big54 * k)));

BigDecimal bottom = (new BigDecimal(BigInteger.Factorial(three_times_k) * BigInteger.Pow((BigInteger.Factorial(k)), 3))).Multiply(
(new BigDecimal(big64).Pow( new BigDecimal(three_times_k).Add(two_over_three))));


ans = ans.Add(new BigDecimal(top).Divide(bottom));

k++;
switching_one *= -1;
loops--;
}

ans = ans.Multiply(new BigDecimal(12));
ans = ans.Invert();
return ans;
}

#endregion
}
}

j1mb0jay
 
V

vanderghast

Doubles have more range (10 to the power 308) and based on IEEE standard
but decimals have more precision (28 digits) and based on (scaled) integer
arithmetic. As example, Intellisense don't present you a predefined square
root for decimal number.

Access (Jet) data type Currency is a decimal with a fixed scale of 4 ( 4
digits to the right of the digit for unit). You can also use a standard
database Decimal data type for a field in a Jet database, so you could
define a scale of 2 to fit your purpose (although it is unusual) at the
database level.


Vanderghast, Access MVP
 
Top