Skip to content

Commit

Permalink
BigInteger based random testing of System.Decimal (dotnet#24053)
Browse files Browse the repository at this point in the history
* BigInteger based random testing of Decimal

* Comments for random testing of Decimal

* Parallelize random testing of Decimal

* Simplified BigDecimal.Div for Decimal tests
  • Loading branch information
pentp authored and joperezr committed Sep 25, 2017
1 parent af86115 commit d235612
Showing 1 changed file with 385 additions and 0 deletions.
385 changes: 385 additions & 0 deletions src/System.Runtime/tests/System/DecimalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Numerics;
using Xunit;

namespace System.Tests
Expand Down Expand Up @@ -1293,5 +1295,388 @@ public static void DecrementOperator(decimal d, decimal expected)
{
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;
}
}
}
}

0 comments on commit d235612

Please sign in to comment.