using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+using System.Linq;
+using System.Numerics;
using Xunit;
namespace System.Tests
{
Assert.Equal(expected, --d);
}
+
+ public static class BigIntegerCompare
+ {
+ [Fact]
+ public static void Test()
+ {
+ decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals);
+ for (int i = 0; i < decimalValues.Length; i++)
+ {
+ decimal d1 = decimalValues[i];
+ BigDecimal b1 = bigDecimals[i];
+ for (int j = 0; j < decimalValues.Length; j++)
+ {
+ decimal d2 = decimalValues[j];
+ int expected = b1.CompareTo(bigDecimals[j]);
+ int actual = d1.CompareTo(d2);
+ if (expected != actual)
+ throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " CMP " + d2);
+ }
+ }
+ }
+ }
+
+ public static class BigIntegerAdd
+ {
+ [Fact]
+ public static void Test()
+ {
+ int overflowBudget = 1000;
+ decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals);
+ for (int i = 0; i < decimalValues.Length; i++)
+ {
+ decimal d1 = decimalValues[i];
+ BigDecimal b1 = bigDecimals[i];
+ for (int j = 0; j < decimalValues.Length; j++)
+ {
+ decimal d2 = decimalValues[j];
+ BigDecimal expected = b1.Add(bigDecimals[j], out bool expectedOverflow);
+ if (expectedOverflow)
+ {
+ if (--overflowBudget < 0)
+ continue;
+ try
+ {
+ decimal actual = d1 + d2;
+ throw new Xunit.Sdk.AssertActualExpectedException(typeof(OverflowException), actual, d1 + " + " + d2);
+ }
+ catch (OverflowException) { }
+ }
+ else
+ unsafe
+ {
+ decimal actual = d1 + d2;
+ if (expected.Scale != (byte)(*(uint*)&actual >> BigDecimal.ScaleShift) || expected.CompareTo(new BigDecimal(actual)) != 0)
+ throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " + " + d2);
+ }
+ }
+ }
+ }
+ }
+
+ public static class BigIntegerMul
+ {
+ [Fact]
+ public static void Test()
+ {
+ int overflowBudget = 1000;
+ decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals);
+ for (int i = 0; i < decimalValues.Length; i++)
+ {
+ decimal d1 = decimalValues[i];
+ BigDecimal b1 = bigDecimals[i];
+ for (int j = 0; j < decimalValues.Length; j++)
+ {
+ decimal d2 = decimalValues[j];
+ BigDecimal expected = b1.Mul(bigDecimals[j], out bool expectedOverflow);
+ if (expectedOverflow)
+ {
+ if (--overflowBudget < 0)
+ continue;
+ try
+ {
+ decimal actual = d1 * d2;
+ throw new Xunit.Sdk.AssertActualExpectedException(typeof(OverflowException), actual, d1 + " * " + d2);
+ }
+ catch (OverflowException) { }
+ }
+ else
+ unsafe
+ {
+ decimal actual = d1 * d2;
+ if (expected.Scale != (byte)(*(uint*)&actual >> BigDecimal.ScaleShift) || expected.CompareTo(new BigDecimal(actual)) != 0)
+ throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " * " + d2);
+ }
+ }
+ }
+ }
+ }
+
+ public static class BigIntegerDiv
+ {
+ [Fact]
+ public static void Test()
+ {
+ int overflowBudget = 1000;
+ decimal[] decimalValues = GetRandomData(out BigDecimal[] bigDecimals);
+ for (int i = 0; i < decimalValues.Length; i++)
+ {
+ decimal d1 = decimalValues[i];
+ BigDecimal b1 = bigDecimals[i];
+ for (int j = 0; j < decimalValues.Length; j++)
+ {
+ decimal d2 = decimalValues[j];
+ if (Math.Sign(d2) == 0)
+ continue;
+ BigDecimal expected = b1.Div(bigDecimals[j], out bool expectedOverflow);
+ if (expectedOverflow)
+ {
+ if (--overflowBudget < 0)
+ continue;
+ try
+ {
+ decimal actual = d1 / d2;
+ throw new Xunit.Sdk.AssertActualExpectedException(typeof(OverflowException), actual, d1 + " / " + d2);
+ }
+ catch (OverflowException) { }
+ }
+ else
+ unsafe
+ {
+ decimal actual = d1 / d2;
+ if (expected.Scale != (byte)(*(uint*)&actual >> BigDecimal.ScaleShift) || expected.CompareTo(new BigDecimal(actual)) != 0)
+ throw new Xunit.Sdk.AssertActualExpectedException(expected, actual, d1 + " / " + d2);
+ }
+ }
+ }
+ }
+ }
+
+ static decimal[] GetRandomData(out BigDecimal[] bigDecimals)
+ {
+ // some static data to test the limits
+ var list = new List<decimal> { new decimal(0, 0, 0, true, 0), decimal.Zero, decimal.MinusOne, decimal.One, decimal.MinValue, decimal.MaxValue,
+ new decimal(1, 0, 0, true, 28), new decimal(1, 0, 0, false, 28),
+ new decimal(123877878, -16789245, 1086421879, true, 16), new decimal(527635459, -80701438, 1767087216, true, 24), new decimal(253511426, -909347550, -753557281, false, 12) };
+
+ // ~1000 different random decimals covering every scale and sign with ~20 different bitpatterns each
+ var rnd = new Random(42);
+ var unique = new HashSet<string>();
+ for (byte scale = 0; scale <= 28; scale++)
+ for (int sign = 0; sign <= 1; sign++)
+ for (int high = 0; high <= 96; high = IncBitLimits(high))
+ for (int low = 0; low < high || (high | low) == 0; low = IncBitLimits(high))
+ {
+ var d = new decimal(GetDigits(low, high), GetDigits(low - 32, high - 32), GetDigits(low - 64, high - 64), sign != 0, scale);
+ if (!unique.Add(d.ToString(CultureInfo.InvariantCulture)))
+ continue; // skip duplicates
+ list.Add(d);
+ }
+ decimal[] decimalValues = list.ToArray();
+ bigDecimals = Array.ConvertAll(decimalValues, d => new BigDecimal(d));
+ return decimalValues;
+
+ // While the decimals are random in general,
+ // they are particularly focused on numbers starting or ending at bits 0-3, 30-34, 62-66, 94-96 to focus more on the corner cases around uint32 boundaries.
+ int IncBitLimits(int i)
+ {
+ switch (i)
+ {
+ case 3:
+ return 30;
+ case 34:
+ return 62;
+ case 66:
+ return 94;
+ default:
+ return i + 1;
+ }
+ }
+
+ // Generates a random number, only bits between low and high can be set.
+ int GetDigits(int low, int high)
+ {
+ if (high <= 0 || low >= 32)
+ return 0;
+ uint res = 0;
+ if (high <= 32)
+ res = 1u << (high - 1);
+ res |= (uint)Math.Ceiling((uint.MaxValue >> Math.Max(0, 32 - high)) * rnd.NextDouble());
+ if (low > 0)
+ res = (res >> low) << low;
+ return (int)res;
+ }
+ }
+
+ /// <summary>
+ /// Decimal implementation roughly based on the oleaut32 native decimal code (especially ScaleResult), but optimized for simplicity instead of speed
+ /// </summary>
+ struct BigDecimal
+ {
+ public readonly BigInteger Integer;
+ public readonly byte Scale;
+
+ public unsafe BigDecimal(decimal value)
+ {
+ Scale = (byte)(*(uint*)&value >> ScaleShift);
+ *(uint*)&value &= ~ScaleMask;
+ Integer = new BigInteger(value);
+ }
+
+ private const uint ScaleMask = 0x00FF0000;
+ public const int ScaleShift = 16;
+
+ public override string ToString()
+ {
+ if (Scale == 0)
+ return Integer.ToString();
+ var s = Integer.ToString("D" + (Scale + 1));
+ return s.Insert(s.Length - Scale, ".");
+ }
+
+ BigDecimal(BigInteger integer, byte scale)
+ {
+ Integer = integer;
+ Scale = scale;
+ }
+
+ static readonly BigInteger[] Pow10 = Enumerable.Range(0, 60).Select(i => BigInteger.Pow(10, i)).ToArray();
+
+ public int CompareTo(BigDecimal value)
+ {
+ int sd = Scale - value.Scale;
+ if (sd > 0)
+ return Integer.CompareTo(value.Integer * Pow10[sd]);
+ else if (sd < 0)
+ return (Integer * Pow10[-sd]).CompareTo(value.Integer);
+ else
+ return Integer.CompareTo(value.Integer);
+ }
+
+ public BigDecimal Add(BigDecimal value, out bool overflow)
+ {
+ int sd = Scale - value.Scale;
+ BigInteger a = Integer, b = value.Integer;
+ if (sd > 0)
+ b *= Pow10[sd];
+ else if (sd < 0)
+ a *= Pow10[-sd];
+
+ var res = a + b;
+ int scale = Math.Max(Scale, value.Scale);
+ overflow = ScaleResult(ref res, ref scale);
+ return new BigDecimal(res, (byte)scale);
+ }
+
+ public BigDecimal Mul(BigDecimal value, out bool overflow)
+ {
+ var res = Integer * value.Integer;
+ int scale = Scale + value.Scale;
+ if (res.IsZero)
+ {
+ overflow = false;
+ // VarDecMul quirk: multipling by zero results in a scaled zero (e.g., 0.000) only if the intermediate scale is <=47 and both inputs fit in 32 bits!
+ if (scale <= 47 && BigInteger.Abs(Integer) <= MaxInteger32 && BigInteger.Abs(value.Integer) <= MaxInteger32)
+ scale = Math.Min(scale, 28);
+ else
+ scale = 0;
+ }
+ else
+ {
+ overflow = ScaleResult(ref res, ref scale);
+ // VarDecMul quirk: rounding to zero results in a scaled zero (e.g., 0.000), except if the intermediate scale is >47 and both inputs fit in 32 bits!
+ if (res.IsZero && scale == 28 && Scale + value.Scale > 47 && BigInteger.Abs(Integer) <= MaxInteger32 && BigInteger.Abs(value.Integer) <= MaxInteger32)
+ scale = 0;
+ }
+ return new BigDecimal(res, (byte)scale);
+ }
+
+ public BigDecimal Div(BigDecimal value, out bool overflow)
+ {
+ int scale = Scale - value.Scale;
+ var dividend = Integer;
+ if (scale < 0)
+ {
+ dividend *= Pow10[-scale];
+ scale = 0;
+ }
+
+ var quo = BigInteger.DivRem(dividend, value.Integer, out var remainder);
+ if (remainder.IsZero)
+ overflow = BigInteger.Abs(quo) > MaxInteger;
+ else
+ {
+ // We have computed a quotient based on the natural scale ( <dividend scale> - <divisor scale> ).
+ // We have a non-zero remainder, so now we increase the scale to DEC_SCALE_MAX+1 to include more quotient bits.
+ var pow = Pow10[29 - scale];
+ quo *= pow;
+ quo += BigInteger.DivRem(remainder * pow, value.Integer, out remainder);
+ scale = 29;
+
+ overflow = ScaleResult(ref quo, ref scale, !remainder.IsZero);
+
+ // Unscale the result (removes extra zeroes).
+ while (scale > 0 && quo.IsEven)
+ {
+ var tmp = BigInteger.DivRem(quo, 10, out remainder);
+ if (!remainder.IsZero)
+ break;
+ quo = tmp;
+ scale--;
+ }
+ }
+ return new BigDecimal(quo, (byte)scale);
+ }
+
+ static readonly BigInteger MaxInteger = (new BigInteger(ulong.MaxValue) << 32) | uint.MaxValue;
+ static readonly BigInteger MaxInteger32 = uint.MaxValue;
+ static readonly double Log2To10 = Math.Log(2) / Math.Log(10);
+
+ /// <summary>
+ /// Returns Log10 for the given number, offset by 96bits.
+ /// </summary>
+ static int ScaleOverMaxInteger(BigInteger abs) => abs.IsZero ? -28 : (int)(((int)BigInteger.Log(abs, 2) - 95) * Log2To10);
+
+ /// <summary>
+ /// See if we need to scale the result to fit it in 96 bits.
+ /// Perform needed scaling. Adjust scale factor accordingly.
+ /// </summary>
+ static bool ScaleResult(ref BigInteger res, ref int scale, bool sticky = false)
+ {
+ int newScale = 0;
+ var abs = BigInteger.Abs(res);
+ if (abs > MaxInteger)
+ {
+ // Find the min scale factor to make the result to fit it in 96 bits, 0 - 29.
+ // This reduces the scale factor of the result. If it exceeds the current scale of the result, we'll overflow.
+ newScale = Math.Max(1, ScaleOverMaxInteger(abs));
+ if (newScale > scale)
+ return true;
+ }
+ // Make sure we scale by enough to bring the current scale factor into valid range.
+ newScale = Math.Max(newScale, scale - 28);
+
+ if (newScale != 0)
+ {
+ // Scale by the power of 10 given by newScale.
+ // This is not guaranteed to bring the number within 96 bits -- it could be 1 power of 10 short.
+ scale -= newScale;
+ var pow = Pow10[newScale];
+ while (true)
+ {
+ abs = BigInteger.DivRem(abs, pow, out var remainder);
+ // If we didn't scale enough, divide by 10 more.
+ if (abs > MaxInteger)
+ {
+ if (scale == 0)
+ return true;
+ pow = 10;
+ scale--;
+ sticky |= !remainder.IsZero;
+ continue;
+ }
+
+ // Round final result. See if remainder >= 1/2 of divisor.
+ // If remainder == 1/2 divisor, round up if odd or sticky bit set.
+ pow >>= 1;
+ if (remainder < pow || remainder == pow && !sticky && abs.IsEven)
+ break;
+ if (++abs <= MaxInteger)
+ break;
+
+ // The rounding caused us to carry beyond 96 bits. Scale by 10 more.
+ if (scale == 0)
+ return true;
+ pow = 10;
+ scale--;
+ sticky = false;
+ }
+ res = res.Sign < 0 ? -abs : abs;
+ }
+ return false;
+ }
+ }
}
}