Skip to content

Instantly share code, notes, and snippets.

@JcBernack
Last active April 29, 2024 09:20
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save JcBernack/0b4eef59ca97ee931a2f45542b9ff06d to your computer and use it in GitHub Desktop.
Save JcBernack/0b4eef59ca97ee931a2f45542b9ff06d to your computer and use it in GitHub Desktop.
BigDecimal
using System;
using System.Numerics;
namespace Common
{
/// <summary>
/// Arbitrary precision decimal.
/// All operations are exact, except for division. Division never determines more digits than the given precision.
/// Source: https://gist.github.com/JcBernack/0b4eef59ca97ee931a2f45542b9ff06d
/// Based on https://stackoverflow.com/a/4524254
/// Author: Jan Christoph Bernack (contact: jc.bernack at gmail.com)
/// License: public domain
/// </summary>
public struct BigDecimal
: IComparable
, IComparable<BigDecimal>
{
/// <summary>
/// Specifies whether the significant digits should be truncated to the given precision after each operation.
/// </summary>
public static bool AlwaysTruncate = false;
/// <summary>
/// Sets the maximum precision of division operations.
/// If AlwaysTruncate is set to true all operations are affected.
/// </summary>
public static int Precision = 50;
public BigInteger Mantissa { get; set; }
public int Exponent { get; set; }
public BigDecimal(BigInteger mantissa, int exponent)
: this()
{
Mantissa = mantissa;
Exponent = exponent;
Normalize();
if (AlwaysTruncate)
{
Truncate();
}
}
/// <summary>
/// Removes trailing zeros on the mantissa
/// </summary>
public void Normalize()
{
if (Mantissa.IsZero)
{
Exponent = 0;
}
else
{
BigInteger remainder = 0;
while (remainder == 0)
{
var shortened = BigInteger.DivRem(Mantissa, 10, out remainder);
if (remainder == 0)
{
Mantissa = shortened;
Exponent++;
}
}
}
}
/// <summary>
/// Truncate the number to the given precision by removing the least significant digits.
/// </summary>
/// <returns>The truncated number</returns>
public BigDecimal Truncate(int precision)
{
// copy this instance (remember it's a struct)
var shortened = this;
// save some time because the number of digits is not needed to remove trailing zeros
shortened.Normalize();
// remove the least significant digits, as long as the number of digits is higher than the given Precision
while (NumberOfDigits(shortened.Mantissa) > precision)
{
shortened.Mantissa /= 10;
shortened.Exponent++;
}
// normalize again to make sure there are no trailing zeros left
shortened.Normalize();
return shortened;
}
public BigDecimal Truncate()
{
return Truncate(Precision);
}
public BigDecimal Floor()
{
return Truncate(BigDecimal.NumberOfDigits(Mantissa) + Exponent);
}
public static int NumberOfDigits(BigInteger value)
{
// do not count the sign
//return (value * value.Sign).ToString().Length;
// faster version
return (int)Math.Ceiling(BigInteger.Log10(value * value.Sign));
}
#region Conversions
public static implicit operator BigDecimal(int value)
{
return new BigDecimal(value, 0);
}
public static implicit operator BigDecimal(double value)
{
var mantissa = (BigInteger) value;
var exponent = 0;
double scaleFactor = 1;
while (Math.Abs(value * scaleFactor - (double)mantissa) > 0)
{
exponent -= 1;
scaleFactor *= 10;
mantissa = (BigInteger)(value * scaleFactor);
}
return new BigDecimal(mantissa, exponent);
}
public static implicit operator BigDecimal(decimal value)
{
var mantissa = (BigInteger)value;
var exponent = 0;
decimal scaleFactor = 1;
while ((decimal)mantissa != value * scaleFactor)
{
exponent -= 1;
scaleFactor *= 10;
mantissa = (BigInteger)(value * scaleFactor);
}
return new BigDecimal(mantissa, exponent);
}
public static explicit operator double(BigDecimal value)
{
return (double)value.Mantissa * Math.Pow(10, value.Exponent);
}
public static explicit operator float(BigDecimal value)
{
return Convert.ToSingle((double)value);
}
public static explicit operator decimal(BigDecimal value)
{
return (decimal)value.Mantissa * (decimal)Math.Pow(10, value.Exponent);
}
public static explicit operator int(BigDecimal value)
{
return (int)(value.Mantissa * BigInteger.Pow(10, value.Exponent));
}
public static explicit operator uint(BigDecimal value)
{
return (uint)(value.Mantissa * BigInteger.Pow(10, value.Exponent));
}
#endregion
#region Operators
public static BigDecimal operator +(BigDecimal value)
{
return value;
}
public static BigDecimal operator -(BigDecimal value)
{
value.Mantissa *= -1;
return value;
}
public static BigDecimal operator ++(BigDecimal value)
{
return value + 1;
}
public static BigDecimal operator --(BigDecimal value)
{
return value - 1;
}
public static BigDecimal operator +(BigDecimal left, BigDecimal right)
{
return Add(left, right);
}
public static BigDecimal operator -(BigDecimal left, BigDecimal right)
{
return Add(left, -right);
}
private static BigDecimal Add(BigDecimal left, BigDecimal right)
{
return left.Exponent > right.Exponent
? new BigDecimal(AlignExponent(left, right) + right.Mantissa, right.Exponent)
: new BigDecimal(AlignExponent(right, left) + left.Mantissa, left.Exponent);
}
public static BigDecimal operator *(BigDecimal left, BigDecimal right)
{
return new BigDecimal(left.Mantissa * right.Mantissa, left.Exponent + right.Exponent);
}
public static BigDecimal operator /(BigDecimal dividend, BigDecimal divisor)
{
var exponentChange = Precision - (NumberOfDigits(dividend.Mantissa) - NumberOfDigits(divisor.Mantissa));
if (exponentChange < 0)
{
exponentChange = 0;
}
dividend.Mantissa *= BigInteger.Pow(10, exponentChange);
return new BigDecimal(dividend.Mantissa / divisor.Mantissa, dividend.Exponent - divisor.Exponent - exponentChange);
}
public static BigDecimal operator %(BigDecimal left, BigDecimal right)
{
return left - right * (left / right).Floor();
}
public static bool operator ==(BigDecimal left, BigDecimal right)
{
return left.Exponent == right.Exponent && left.Mantissa == right.Mantissa;
}
public static bool operator !=(BigDecimal left, BigDecimal right)
{
return left.Exponent != right.Exponent || left.Mantissa != right.Mantissa;
}
public static bool operator <(BigDecimal left, BigDecimal right)
{
return left.Exponent > right.Exponent ? AlignExponent(left, right) < right.Mantissa : left.Mantissa < AlignExponent(right, left);
}
public static bool operator >(BigDecimal left, BigDecimal right)
{
return left.Exponent > right.Exponent ? AlignExponent(left, right) > right.Mantissa : left.Mantissa > AlignExponent(right, left);
}
public static bool operator <=(BigDecimal left, BigDecimal right)
{
return left.Exponent > right.Exponent ? AlignExponent(left, right) <= right.Mantissa : left.Mantissa <= AlignExponent(right, left);
}
public static bool operator >=(BigDecimal left, BigDecimal right)
{
return left.Exponent > right.Exponent ? AlignExponent(left, right) >= right.Mantissa : left.Mantissa >= AlignExponent(right, left);
}
/// <summary>
/// Returns the mantissa of value, aligned to the exponent of reference.
/// Assumes the exponent of value is larger than of reference.
/// </summary>
private static BigInteger AlignExponent(BigDecimal value, BigDecimal reference)
{
return value.Mantissa * BigInteger.Pow(10, value.Exponent - reference.Exponent);
}
#endregion
#region Additional mathematical functions
public static BigDecimal Exp(double exponent)
{
var tmp = (BigDecimal)1;
while (Math.Abs(exponent) > 100)
{
var diff = exponent > 0 ? 100 : -100;
tmp *= Math.Exp(diff);
exponent -= diff;
}
return tmp * Math.Exp(exponent);
}
public static BigDecimal Pow(double basis, double exponent)
{
var tmp = (BigDecimal)1;
while (Math.Abs(exponent) > 100)
{
var diff = exponent > 0 ? 100 : -100;
tmp *= Math.Pow(basis, diff);
exponent -= diff;
}
return tmp * Math.Pow(basis, exponent);
}
#endregion
public override string ToString()
{
return string.Concat(Mantissa.ToString(), "E", Exponent);
}
public bool Equals(BigDecimal other)
{
return other.Mantissa.Equals(Mantissa) && other.Exponent == Exponent;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
return obj is BigDecimal && Equals((BigDecimal) obj);
}
public override int GetHashCode()
{
unchecked
{
return (Mantissa.GetHashCode()*397) ^ Exponent;
}
}
public int CompareTo(object obj)
{
if (ReferenceEquals(obj, null) || !(obj is BigDecimal))
{
throw new ArgumentException();
}
return CompareTo((BigDecimal) obj);
}
public int CompareTo(BigDecimal other)
{
return this < other ? -1 : (this > other ? 1 : 0);
}
}
}
@JcBernack
Copy link
Author

Added the Floor() function and modulo operation.

@esskar
Copy link

esskar commented Aug 13, 2017

Is this public domain? MIT?

@JcBernack
Copy link
Author

@esskar, it's public domain, use it however you like.

@juanfranblanco
Copy link

Small correction, if the exponent is 0 we don't want to normalise.

        public void Normalize()
        {
            if (this.Exponent == 0) return;

            if (this.Mantissa.IsZero)
            {
                this.Exponent = 0;
            }
            else
            {
                BigInteger remainder = 0;
                while (remainder == 0)
                {
                    var shortened = BigInteger.DivRem(dividend: this.Mantissa, divisor: 10, remainder: out remainder);
                    if (remainder != 0)
                    {
                        continue;
                    }
                    this.Mantissa = shortened;
                    this.Exponent++;
                }
            }
        }

@danrayson
Copy link

danrayson commented Apr 22, 2018

Fantastic work! I really needed something like this in order to increase the resolution of the search space in a maths evolution problem I'm looking at. Thanks!

Now I need a BigComplex class :P I'm going to look into using this and the definition of the Complex class in System.Numerics namespace to try and implement a BigComplex that does what the .Net one does but using your class. We'll see how it goes :S

@uhDreamer
Copy link

I added this since i couldn't find another way to do it. if something like it is already in there then you can ignore this but it might be useful for anyone else looking to convert it to BigInteger

public static explicit operator BigInteger(BigDecimal value)
{
    BigDecimal floored = value.Floor();
    return floored.Mantissa * BigInteger.Pow(10, floored.Exponent);
}

@bhull242
Copy link

bhull242 commented Apr 17, 2019

For the record, the Floor() function doesn’t actually floor the BigDecimal; it actually truncates it (rounds towards 0) to an integer. To floor it, you’d have to round down. This doesn’t matter for nonnegative numbers, but it can make a difference for negative values.

Personally, I’d rename the Truncate methods to Round and the Floor function to Truncate.

@WaiYanMyintMo
Copy link

If your struct isn't readonly, your struct is not automatically immutable.

@ChronoDK
Copy link

Very cool. I would like to see a natural logarithm function for this - would be very useful.

@JcBernack
Copy link
Author

Very cool. I would like to see a natural logarithm function for this - would be very useful.

You could probably add an algorithm for a Taylor series like this one:
https://stackoverflow.com/questions/27179674/examples-of-log-algorithm-using-arbitrary-precision-maths/27180100#27180100

@MaximeTasset
Copy link

MaximeTasset commented Sep 3, 2021

The function NumberOfDigits returns wrong number for "0" (negative number), and all power of 10 (one digit missing).

public static int NumberOfDigits(BigInteger value)
        {
            // do not count the sign
            //return (value * value.Sign).ToString().Length;
            // faster version
            return (int)Math.Ceiling(BigInteger.Log10(value * value.Sign));
        }

should be:

public static int NumberOfDigits(BigInteger value)
{
    if (value.IsZero)
    {
        return 1;
    }
    double NumberOfDigits = BigInteger.Log10(value * value.Sign);
    if(NumberOfDigits % 1 == 0)
    {
        return (int)NumberOfDigits + 1;
    }
    return (int)Math.Ceiling(NumberOfDigits);
}

@MaximeTasset
Copy link

We can improve greatly the speed of the function Truncate:

        public BigDecimal Truncate(int precision)
        {
            // copy this instance (remember it's a struct)
            var shortened = this;
            // save some time because the number of digits is not needed to remove trailing zeros
            shortened.Normalize();
            
            // remove the least significant digits, as long as the number of digits is higher than the given Precision
            int Digits = NumberOfDigits(shortened.Mantissa);
            int DigitsToRemove = Math.Max(Digits - precision, 0);
            shortened.Mantissa /= BigInteger.Pow(10, DigitsToRemove);
            shortened.Exponent += DigitsToRemove;
            
            // normalize again to make sure there are no trailing zeros left
            shortened.Normalize();
            return shortened;
        }

@VivekDeshmukh21
Copy link

VivekDeshmukh21 commented Jul 23, 2022

This is much needed solution for many out there. Thank you for your efforts on this @JcBernack
I have two questions:

  1. How can we hold decimal(31,18)decimal number in this BigDecimal struct?
    Tried with this BigDecimal deBig = new BigDecimal(1234567890123, 123456789012345678); but it is giving error as exponent is integer but passing long value.
  2. How can we use this with ORM? Ex We have a Quantity column in SQL which holds (31,18) value into it. Now at the time of assigning this SQL columns value to BigDecimal variable, how can we assign it? Do we need to convert it into string ang then do a remapping? or any other best possible way?

Much appreciate your hints, references on this.

@JcBernack
Copy link
Author

Hi @VivekDeshmukh21

  1. the conversion for decimals is already implemented here, you can just assign a decimal to a BigDecimal or do a type cast:
    BigDecimal foo = 31.18m;
  2. I don't understand the question, if you're trying to store BigDecimal in a database there is probably no way around a string representation. ToString() already does a string serialization, you would just need to add a deserialization/constructor that parses a BigDecimal from string.

@VivekDeshmukh21
Copy link

Hi @JcBernack ,
Thanks for reaching out. I got the answer for second point and and good with that.

Regarding first point, let me elaborate more. Basically we wanted to apply many mathematical operations on this BigDecimal number. When we tried to do a multiplication its not returning correct number.

Ex we tried with
Step 1. 1234567890123.123456789012345678 * 1234567890123.123456789012345678 Expected value of this multiplication is 1524157875323060632530193.67918695395

Step 2. So to achieve this, we tried this by declaring two BigDecimal variables as

 BigDecimal bigDecimal1 = 1234567890123.123456789012345678;
 BigDecimal bigDecimal2 = 1234567890123.123456789012345678;
 Console.WriteLine("\n" + (bigDecimal1 * bigDecimal2).ToString());

It returned a value as 152415787532306098613756742087696E-8. This is not same as the plain multiplication performed in Step 1.
In watch window it shows as
image

So how can we match this multiplication with Step 1's result i.e. 1524157875323060632530193.67918695395 number as a string?

@JcBernack
Copy link
Author

Ok, in your example you are already losing precision before the BigDecimal ever gets the numbers.

BigDecimal bigDecimal1 = 1234567890123.123456789012345678;
The number literal is handled as a double by the compiler and then converted to a BigDecimal. The double will already have lost decimal digits here. You either have to start with a datatype that can also hold the information or find a way to construct the BigDecimal without precision loss. If you use a decimal (add the m suffix to the number) you get more digits, but your example will still lose a few digits of precision. If you want it accurate to an arbitrary level you need a string representation, just like for your ORM question.

You could write add a constructor that takes a string and basically reverts the ToString() serialization.

@JcBernack
Copy link
Author

JcBernack commented Jul 25, 2022

Just to show you that the BigDecimal would be accurate:

BigDecimal bigDecimal1 = new BigDecimal(BigInteger.Parse("1234567890123123456789012345678"), -18);
BigDecimal bigDecimal2 = new BigDecimal(BigInteger.Parse("1234567890123123456789012345678"), -18);
Console.WriteLine((bigDecimal1 * bigDecimal2).ToString());

Output

1524157875323060632530193679186953949115624527968299765279684E-36
or
1524157875323060632530193.679186953949115624527968299765279684

wolfram alpha result

1.524157875323060632530193679186953949115624527968299765279684×10^24
or
1524157875323060632530193.679186953949115624527968299765279684

@VivekDeshmukh21
Copy link

Many Thanks @JcBernack . We are clear about the usage now. When we performed the calculation, initially it was in SQL server i.e.
Select 1234567890123.123456789012345678 * 1234567890123.123456789012345678 it gave us 1524157875323060632530193.67918695395. Because SQL can hold only till 38 digits max into so its rounding and truncating trailing numbers. Hence there was a confusion.

Now, to be in sync with SQL, we need to modify existing BigDecimal implementation to generate total digits length only till max 38 digits so exploring on this.

If you can give any hint about how can we restrict BigDecimal to not go more than 38 digits then it will be bread an butter help :)

@JcBernack
Copy link
Author

JcBernack commented Aug 1, 2022

@VivekDeshmukh21 You can call .Truncate(38) to reduce the number of digits to 38, independent of where the decimal point is. This will not round the least significant digit properly though, it always just removes digits.

@VivekDeshmukh21
Copy link

Yes @JcBernack . Tried same. Once again many thanks for sharing this over a community. It helped us a lot :)

@KTSnowy
Copy link

KTSnowy commented Oct 17, 2022

@JcBernack Would it be possible to do a String.Insert() on the ToString() method?

That way you could return the decimal point in it's place instead of an "E-36" for example.

@KTSnowy
Copy link

KTSnowy commented Oct 17, 2022

Here, I made a quick version of this. I'm assuming that the Exponent will always be negative (please correct me if I'm wrong) and so I'm getting the length of the mantissa and subtracting the length by the exponent to get the position of the decimal point, and then I'm calling String.Insert() on the mantissa to insert a "." where the decimal point should be. Would this work?

public string ToString(string format)
{
    if (Exponent == 0) return Mantissa.ToString();
    int length = Mantissa.ToString().Length;
    return format == "decimal"
        ? Mantissa.ToString().Insert(length -(-Exponent), ".")
        : ToString();
}

Edit: Updated the ToString code, this one works as an overload on the original ToString() method

@KTSnowy
Copy link

KTSnowy commented Oct 17, 2022

Also, I wanted to ask if it would be alright to use this for my COBOL compiler project. We needed a decimal type with a larger range than C#'s System.Decimal and it looks like this one could work well.

@JcBernack
Copy link
Author

JcBernack commented Oct 18, 2022

Also, I wanted to ask if it would be alright to use this for my COBOL compiler project. We needed a decimal type with a larger range than C#'s System.Decimal and it looks like this one could work well.

@KTSnowy Absolutely, as stated in the header of the code, it is public domain. You're welcome to use it however you see fit. Just send me some champagne if it makes you rich.

@KTSnowy
Copy link

KTSnowy commented Oct 18, 2022

@JcBernack The compiler will be free and open source so I'm not sure if it will make me rich. But if it does, I'll make sure to send you the best champagne I can find.

@KTSnowy
Copy link

KTSnowy commented Oct 18, 2022

Also, as an extension to my previous code snippet, here's the String to BigDecimal conversion:
If someone sees this and needs these extensions, consider the ones I sent here as having the same license as the BigDecimal implementation, Public Domain.

public static implicit operator BigDecimal(string value)
{
    bool isDecimal = value.Contains(".");
    if (isDecimal)
    {
        int exponent = value.Length - (value.IndexOf(".") + 1);
        BigInteger mantissa = BigInteger.Parse(value.Replace(".", ""));
        return new BigDecimal(mantissa, -exponent);
    }
    return new BigDecimal(BigInteger.Parse(value), 0);
}

This should work fine for strings in this format for example: "12345.6789". This way you don't lose precision from the double to BigDecimal conversion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment