I recently came across a need to display very large decimal numbers and realized that I could use a BigDecimal class. After some tinkering I decided to leverage the BigInteger
class to do the heavy lifting.
using System;
using System.Numerics;
using System.Text;
namespace Numerics
{
public class BigDecimal : IComparable
{
//The number represents all the digits in the displayed value.
//The precision is the number of decimal places.
//maxPrecision is the maximum number of decimal places allowed.
//This is adjustable to any arbitrary number with the function provided.
BigInteger number = 0;
BigInteger precision = 0;
static BigInteger maxPrecision = 20;
public static BigInteger MAXPRECISION
{
get { return maxPrecision; }
}
#region Constructors
//Constructors that accept limited numeric types
public BigDecimal()
{
BigInteger test = new BigInteger();
BigInteger test2 = test + 1;
}
public BigDecimal(BigInteger num)
{
number = num;
}
public BigDecimal(int num)
{
number = num;
}
public BigDecimal(double num)
{
BigDecimal temp = new BigDecimal((decimal)num);
number = temp.number;
precision = temp.precision;
}
public BigDecimal(decimal num)
{
number = (BigInteger)num;
if (num == 0)
{
return;
}
if (number == 0)
{
string tempNum = num.ToString();
if (num < 0)
{
tempNum = "-" + tempNum.Substring(tempNum.IndexOf('.') + 1);
}
tempNum = tempNum.Substring(tempNum.IndexOf('.') + 1);
precision = tempNum.Length;
number = BigInteger.Parse(tempNum);
return;
}
string temp = decimal.Remainder(num, decimal.Truncate(num)).ToString();
string tempFraction = temp.Substring(temp.IndexOf('.') + 1);
precision = tempFraction.Length;
number = BigInteger.Parse(tempFraction) + (number * BIPow(10, precision));
}
public BigDecimal(float num)
{
BigDecimal temp = new BigDecimal((decimal)num);
number = temp.number;
precision = temp.precision;
}
public BigDecimal(long num)
{
number = num;
}
public BigDecimal(ulong num)
{
number = num;
}
public BigDecimal(uint num)
{
number = num;
}
BigDecimal(byte[] num)
{
number = new BigInteger(num);
}
#endregion
#region Operators
//Implicit operators for simple casting from limited numeric types
public static implicit operator BigDecimal(int v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(double v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(decimal v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(float v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(BigInteger v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(long v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(ulong v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(uint v)
{
return new BigDecimal(v);
}
public static implicit operator BigDecimal(byte[] v)
{
return new BigDecimal(v);
}
//Equals operator
public static bool operator ==(BigDecimal a, BigDecimal b)
{
return a.CompareTo(b) == 0;
}
//Not Equals operator
public static bool operator !=(BigDecimal a, BigDecimal b)
{
return a.CompareTo(b) != 0;
}
//Plus operator
public static BigDecimal operator +(BigDecimal a, BigDecimal b)
{
BigDecimal outVal = 0;
BigInteger maxPrecision = BigInteger.Max(a.precision, b.precision);
if (a.precision < maxPrecision)
{
outVal = a.number * BIPow(10, maxPrecision - a.precision) + b.number;
}
if (b.precision < maxPrecision)
{
outVal = b.number * BIPow(10, maxPrecision - b.precision) + a.number;
}
outVal.precision = maxPrecision;
return outVal;
}
//Minus operator
public static BigDecimal operator -(BigDecimal a, BigDecimal b)
{
return a + (b * -1);
}
//Multiplication operator
public static BigDecimal operator *(BigDecimal a, BigDecimal b)
{
BigDecimal outVal = a.number * b.number;
outVal.precision = a.precision + b.precision;
return outVal;
}
//Division operator
public static BigDecimal operator /(BigDecimal a, BigDecimal b)
{
BigDecimal outVal = 0;
BigInteger dividend = a.number;
BigInteger divisor = b.number;
BigInteger maxPrecision = BigInteger.Max(a.precision, b.precision);
if (a.precision < maxPrecision)
{
a.precision = maxPrecision;
a.number = a.number * BIPow(10, maxPrecision - a.precision);
}
if (b.precision < maxPrecision)
{
b.precision = maxPrecision;
b.number = b.number * BIPow(10, maxPrecision - b.precision);
}
BigInteger remainder = 0;
outVal.number = BigInteger.DivRem(a.number, b.number, out remainder);
while (remainder != 0 && outVal.precision < MAXPRECISION)
{
while(BigInteger.Abs(remainder) < BigInteger.Abs(b.number))
{
remainder *= 10;
outVal.number *= 10;
outVal.precision++;
}
outVal.number = outVal.number + BigInteger.DivRem(remainder, b.number, out remainder);
}
return outVal;
}
//Greater than operator
public static bool operator >(BigDecimal a, BigDecimal b)
{
BigInteger maxPrecision = BigInteger.Max(a.precision, b.precision);
if (a.precision < maxPrecision)
{
return (a.number * BIPow(10, maxPrecision - a.precision)) > b.number;
}
if (b.precision < maxPrecision)
{
return a.number > (b.number * BIPow(10, maxPrecision - b.precision));
}
return a.number > b.number;
}
//Less than operator
public static bool operator <(BigDecimal a, BigDecimal b)
{
BigInteger maxPrecision = BigInteger.Max(a.precision, b.precision);
if (a.precision < maxPrecision)
{
return (a.number * BIPow(10, maxPrecision - a.precision)) < b.number;
}
if (b.precision < maxPrecision)
{
return a.number < (b.number * BIPow(10, maxPrecision - b.precision));
}
return a.number < b.number;
}
#endregion
#region Public Functions
public override string ToString()
{
String outVal = number.ToString();
if (precision == 0)
{
return string.Format("{0}.0", outVal);
}
string startString = "0.";
if (outVal.TrimStart("-".ToCharArray()).Length < precision)
{
if(number < 0)
{
startString = "-0.";
outVal = outVal.TrimStart("-".ToCharArray());
}
StringBuilder sb = new StringBuilder(startString + NewString('0', precision - outVal.Length) + outVal);
return sb.ToString();
}
return outVal.Insert(outVal.Length - (int)precision, ".");
}
public int CompareTo(object a)
{
return ToString().CompareTo(a.ToString());
}
public override int GetHashCode()
{
return ToString().GetHashCode();
}
public override bool Equals(object obj)
{
return this.CompareTo(obj) == 0;
}
//Function to change the maximum precision the class will use
public static void ChangeMaxPrecision(BigInteger value)
{
maxPrecision = value;
}
#endregion
#region Private Functions
//A function to build a new string of repeating characters using BigInteger count.
static string NewString(char c, BigInteger count)
{
if(count <= int.MaxValue)
{
return new string(c, (int)count);
}
StringBuilder sb = new StringBuilder();
for (BigInteger i = 0; i < count; i++)
{
sb.Append(c);
}
return sb.ToString();
}
//A function to calculate a number raised to a power both numbers represented as BigIntegers
static BigInteger BIPow(BigInteger input, BigInteger exp)
{
if (exp == 0)
{
return 1;
}
if (exp == 1)
{
return input;
}
BigInteger outval = 1;
while (exp != 0)
{
if (exp % 2 == 1)
{
outval *= input;
}
exp >>= 1;
input *= input;
}
return outval;
}
#endregion
}
}
So far I've only implemented the 4 basic math operations. I thought I would get some feedback on what I have so far before I implement more complex operations.
Updated plus operator to fix bug:
//Plus operator
public static BigDecimal operator +(BigDecimal a, BigDecimal b)
{
BigDecimal outVal = 0;
if (a.precision == b.precision)
{
outVal = a.number + b.number;
outVal.precision = a.precision;
return outVal;
}
BigInteger maxPrecision = BigInteger.Max(a.precision, b.precision);
if (a.precision < maxPrecision)
{
outVal = a.number * BIPow(10, maxPrecision - a.precision) + b.number;
}
else
{
outVal = b.number * BIPow(10, maxPrecision - b.precision) + a.number;
}
outVal.precision = maxPrecision;
return outVal;
-
\$\begingroup\$ The very first thing that comes to my eyes is...performance. Converting to string (which BTW is broken because you're doing that using current locale) is terribly inefficient. \$\endgroup\$Adriano Repetti– Adriano Repetti2016年09月20日 06:03:22 +00:00Commented Sep 20, 2016 at 6:03
-
\$\begingroup\$ OK I'll look into that, which is fairly easily adaptable. I'm more worried about the operations. \$\endgroup\$user33306– user333062016年09月20日 06:15:32 +00:00Commented Sep 20, 2016 at 6:15
-
\$\begingroup\$ Can you describe briefly what the relationship is between the stored integer, the stored precision, and the number being represented? \$\endgroup\$Eric Lippert– Eric Lippert2016年09月20日 14:31:07 +00:00Commented Sep 20, 2016 at 14:31
-
\$\begingroup\$ I added more comments to help explain that \$\endgroup\$user33306– user333062016年09月20日 16:20:37 +00:00Commented Sep 20, 2016 at 16:20
-
\$\begingroup\$ I have rolled back the last edit. Please see what you may and may not do after receiving answers . \$\endgroup\$Mathieu Guindon– Mathieu Guindon2016年09月20日 17:07:39 +00:00Commented Sep 20, 2016 at 17:07
3 Answers 3
Couple of things caught my eye.
The constructor for double
and float
can throw an exception if a NaN
is passed in.
Just looking at the plus operator, there is a bug if a.precision
equals b.precision
in which case neither is less than maxPrecision
.
Also for subtraction, multiplication even by -1 seems heavy-handed. Perhaps:
//Minus operator
public static BigDecimal operator -(BigDecimal a, BigDecimal b)
{
return a + new BigDecimal(-b.number);
}
-
\$\begingroup\$ Your suggestion for the minus operator doesn't account for the precision in b. The constructor for BigInteger will return a BigDecimal with 0 precision. \$\endgroup\$user33306– user333062016年09月20日 16:37:32 +00:00Commented Sep 20, 2016 at 16:37
-
\$\begingroup\$ As for exceptions, for now I'm concentrating on the operations and making sure they're correct. I'll let the BigInteger class handle the exceptions for now. \$\endgroup\$user33306– user333062016年09月20日 16:40:14 +00:00Commented Sep 20, 2016 at 16:40
I don't understand the "max precision" logic at all.
- If 20 is the max, then why is the precision a big integer? Just make it an integer if it can't get bigger than 20. But...
- ...if 20 is the max, then how is it possible to get a decimal with precision of 40? (Multiply two precision-20 numbers together and that's what you get.) Or 29? (A Decimal can have 29 digits after the decimal.) Maximums should be maximums. We should have an iron clad guarantee that the precision is never observed to exceed the maximum.
- The type doesn't do what it says on the tin. You said you want an arbitrary precision decimal, and you then put a tiny little cap on the precision! The built in decimal type already has a billion times more precision than 20 places; what's the point of an "arbitrary precision" decimal type that is less precise than the built in one?
Scrap this whole idea. There is no maximum precision in an arbitrary precision numeric type; that's what "arbitrary" means.
Moreover: consider the merits of implementing not BigDecimal but rather BigFraction. A BigDecimal is simply a BigFraction where the denominator is a power of ten. The logic for adding, subtracting, multiplying and dividing arbitrary big fractions is actually easier than getting all this stuff right for the case restricted to decimal.
-
\$\begingroup\$ The maximum can be set to whatever the user desires with the function provided. The only limit is the users memory. This way if the user wants a 1,000 or even 1,000,000 digits the class will let them as long as the user doesn't face memory constraints. This way the user can stop the class from generating too many digits and getting memory overruns \$\endgroup\$user33306– user333062016年09月21日 02:42:06 +00:00Commented Sep 21, 2016 at 2:42
-
\$\begingroup\$ +1 for the points about
precision
, but I disagree with "a BigDecimal is simply a BigFraction where the denominator is a power of ten". A simple implementation ofBigDecimal
could be just a specialisedBigFraction
, but a good one will allow you to work to a given precision. I'm not sure whether Java'sMathContext
is the best solution, but it's certainly a reasonable one. And generalising that toBigFraction
is rather non-obvious. \$\endgroup\$Peter Taylor– Peter Taylor2016年09月21日 11:24:38 +00:00Commented Sep 21, 2016 at 11:24
//Equals operator public static bool operator ==(BigDecimal a, BigDecimal b) { return a.CompareTo(b) == 0; } //Not Equals operator public static bool operator !=(BigDecimal a, BigDecimal b) { return a.CompareTo(b) != 0; } //Greater than operator public static bool operator >(BigDecimal a, BigDecimal b) { BigInteger maxPrecision = BigInteger.Max(a.precision, b.precision); if (a.precision < maxPrecision) { return (a.number * BIPow(10, maxPrecision - a.precision)) > b.number; } if (b.precision < maxPrecision) { return a.number > (b.number * BIPow(10, maxPrecision - b.precision)); } return a.number > b.number; } //Less than operator public static bool operator <(BigDecimal a, BigDecimal b) { BigInteger maxPrecision = BigInteger.Max(a.precision, b.precision); if (a.precision < maxPrecision) { return (a.number * BIPow(10, maxPrecision - a.precision)) < b.number; } if (b.precision < maxPrecision) { return a.number < (b.number * BIPow(10, maxPrecision - b.precision)); } return a.number < b.number; }
Firstly, those comments are pointless. Secondly, two of the methods are written correctly and two are not. The latter two should be
public static bool operator >(BigDecimal a, BigDecimal b)
{
return a.CompareTo(b) > 0;
}
public static bool operator <(BigDecimal a, BigDecimal b)
{
return a.CompareTo(b) < 0;
}
Otherwise you'll end up duplicating code.
Well, I would expect you to end up duplicating code. Actually they were better implemented than
public int CompareTo(object a) { return ToString().CompareTo(a.ToString()); } public override bool Equals(object obj) { return this.CompareTo(obj) == 0; }
I would be strongly inclined to implement IComparable<BigDecimal>
rather than IComparable
. It's rather ugly to end up with asymmetric Equals
, which is what you have here. Consider
ISet<object> foo = new HashSet<object>();
foo.add("1");
foo.add(new BigDecimal(1));
ISet<object> bar = new HashSet<object>();
bar.add(new BigDecimal(1));
bar.add("1");
foo
and bar
do not contain the same number of elements, because "1".equals(new BigDecimal(1)) != new BigDecimal(1).equals("1")
.