BigInteger based random testing of System.Decimal (dotnet/corefx#24053)
authorPent Ploompuu <kaalikas@gmail.com>
Mon, 25 Sep 2017 22:36:29 +0000 (01:36 +0300)
committerJose Perez Rodriguez <joperezr@microsoft.com>
Mon, 25 Sep 2017 22:36:29 +0000 (15:36 -0700)
* BigInteger based random testing of Decimal

* Comments for random testing of Decimal

* Parallelize random testing of Decimal

* Simplified BigDecimal.Div for Decimal tests

Commit migrated from https://github.com/dotnet/corefx/commit/d23561261a8c98a9b1f9c46838fae3ecfca31998

src/libraries/System.Runtime/tests/System/DecimalTests.cs

index 1c7738a..ce30b77 100644 (file)
@@ -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
@@ -1293,5 +1295,388 @@ 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;
+            }
+        }
     }
 }