Skip to content

Instantly share code, notes, and snippets.

@JcBernack
Last active May 13, 2024 05:33
Show Gist options
  • 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);
}
}
}
@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