3
\$\begingroup\$

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;
\$\endgroup\$
6
  • \$\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 Sep 20 '16 at 6:03
  • \$\begingroup\$ OK I'll look into that, which is fairly easily adaptable. I'm more worried about the operations. \$\endgroup\$ – user33306 Sep 20 '16 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 Sep 20 '16 at 14:31
  • \$\begingroup\$ I added more comments to help explain that \$\endgroup\$ – user33306 Sep 20 '16 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 Sep 20 '16 at 17:07
2
\$\begingroup\$

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);
    }
\$\endgroup\$
2
  • \$\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 Sep 20 '16 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 Sep 20 '16 at 16:40
6
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\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 Sep 21 '16 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 of BigDecimal could be just a specialised BigFraction, but a good one will allow you to work to a given precision. I'm not sure whether Java's MathContext is the best solution, but it's certainly a reasonable one. And generalising that to BigFraction is rather non-obvious. \$\endgroup\$ – Peter Taylor Sep 21 '16 at 11:24
2
\$\begingroup\$
//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").

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy