diff --git a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs index 4b3b5322..986cfb17 100644 --- a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs +++ b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs @@ -1269,6 +1269,151 @@ public void Issue91ToFloatString() } } + [Test] + public void TestShortenMantissaBigDelta() + { + Assert.AreEqual(Decimal64.Parse("10000000000000000"), + Decimal64.Parse("9999000000000000").ShortenMantissa(DotNetImpl.MaxCoefficient / 10, 2)); + } + + [Test] + public void TestShortenMantissaCase006() + { + String testString = "0.006"; + var testValue = Double.Parse(testString); + var testX = 0.005999999999998265; // Math.nextDown(testValue); + + var d64 = Decimal64.FromDouble(testX).ShortenMantissa(1735, 1); + Assert.AreEqual(testString, d64.ToString()); + + Decimal64.FromDouble(9.060176071990028E-7).ShortenMantissa(2, 1); + } + + [Test] + public void TestShortenMantissaRandom() + { + var randomSeed = new Random().Next(); + var random = new Random(randomSeed); + + try + { + for (int iteration = 0; iteration < N; ++iteration) + { + var mantissa = GenerateMantissa(random, Decimal64.MaxSignificandDigits); + int error = random.Next(3) - 1; + mantissa = Math.Min(DotNetImpl.MaxCoefficient, Math.Max(0, (ulong)((long)mantissa + error))); + if (mantissa <= DotNetImpl.MaxCoefficient / 10) + mantissa = mantissa * 10; + + var delta = GenerateMantissa(random, 0); + if (delta > DotNetImpl.MaxCoefficient / 10) + delta = delta / 10; + + CheckShortenMantissaCase(mantissa, delta); + } + } + catch (Exception e) + { + throw new Exception("Random seed " + randomSeed + " exception: " + e.Message, e); + } + } + + [Test] + public void TestShortenMantissaCase() + { + CheckShortenMantissaCase(9999888877776001UL, 1000); + CheckShortenMantissaCase(1230000000000000UL, 80); + CheckShortenMantissaCase(1230000000000075UL, 80); + CheckShortenMantissaCase(1229999999999925UL, 80); + CheckShortenMantissaCase(4409286553495543UL, 900); + CheckShortenMantissaCase(4409286553495000UL, 1000); + CheckShortenMantissaCase(4409286550000000UL, 81117294); + CheckShortenMantissaCase(9010100000000001UL, 999999999999999L); + CheckShortenMantissaCase(8960196546869015UL, 1); + CheckShortenMantissaCase(4700900091799999UL, 947076117508L); + CheckShortenMantissaCase(5876471737721999UL, 91086); + CheckShortenMantissaCase(6336494570000000UL, 6092212816L); + CheckShortenMantissaCase(8960196546869011UL, 999999999999999L); + CheckShortenMantissaCase(1519453608576584UL, 3207L); + } + + private static void CheckShortenMantissaCase(ulong mantissa, ulong delta) + { + try + { + var bestSolution = ShortenMantissaDirect(mantissa, delta); + + var test64 = Decimal64.FromULong(mantissa).ShortenMantissa(delta, 0).ToULong(); + + if (test64 != bestSolution) + throw new Exception("The mantissa(=" + mantissa + ") and delta(=" + delta + ") produce test64(=" + test64 + ") != bestSolution(=" + bestSolution + ")."); + } + catch (Exception e) + { + throw new Exception("The mantissa(=" + mantissa + ") and delta(=" + delta + ") produce exception.", e); + } + } + + private static ulong ShortenMantissaDirect(ulong mantissaIn, ulong deltaIn) + { + var mantissa = (long)mantissaIn; + var delta = (long)deltaIn; + var rgUp = mantissa + delta; + var rgDown = mantissa - delta; + + if (mantissaIn <= DotNetImpl.MaxCoefficient / 10 || mantissaIn > DotNetImpl.MaxCoefficient) + throw new ArgumentException("The mantissa(=" + mantissa + ") must be in (" + DotNetImpl.MaxCoefficient / 10 + ".." + DotNetImpl.MaxCoefficient + "] range"); + + long bestSolution = long.MinValue; + if (rgDown > 0) + { + long mUp = (mantissa / 10) * 10; + long mFactor = 1; + + long bestDifference = long.MaxValue; + int bestPosition = -1; + + for (int replacePosition = 0; + replacePosition < Decimal64.MaxSignificandDigits + 1; + ++replacePosition, mUp = (mUp / 100) * 10, mFactor *= 10) + { + for (uint d = 0; d < 10; ++d) + { + long mTest = (mUp + d) * mFactor; + if (rgDown <= mTest && mTest <= rgUp) + { + var md = Math.Abs(mantissa - mTest); + if (bestPosition < replacePosition || + (bestPosition == replacePosition && bestDifference >= md)) + { + bestPosition = replacePosition; + bestDifference = md; + bestSolution = mTest; + } + } + } + } + } + else + { + bestSolution = 0; + } + + return (ulong)bestSolution; + } + + private static ulong GenerateMantissa(Random random, int minimalLength) + { + int mLen = (1 + random.Next(Decimal64.MaxSignificandDigits) /*[1..16]*/); + ulong m = 1 + (ulong)random.Next(9); + int i = 1; + for (; i < mLen; ++i) + m = m * 10 + (ulong)random.Next(10); + for (; i < minimalLength; ++i) + m = m * 10; + return m; + } + readonly int N = 5000000; static void Main() diff --git a/csharp/EPAM.Deltix.DFP/Decimal64.cs b/csharp/EPAM.Deltix.DFP/Decimal64.cs index 186d5f97..ac89c2c8 100644 --- a/csharp/EPAM.Deltix.DFP/Decimal64.cs +++ b/csharp/EPAM.Deltix.DFP/Decimal64.cs @@ -809,6 +809,45 @@ public Decimal64 RoundToNearestTiesToEven(Decimal64 multiple) return new Decimal64(NativeImpl.multiply2(ratio, multiple.Bits)); } + /// + /// This function is experimental. + /// Returns a DFP number in some neighborhood of the input value with a maximally + /// reduced number of digits. + /// Explanation: + /// Any finite DFP value can be represented as 16-digits integer number (mantissa) + /// multiplied by some power of ten (exponent): + /// 12.3456 = 1234_5600_0000_0000 * 10^-14 + /// 720491.5510000001 = 7204_9155_1000_0001 * 10^-10 + /// 0.009889899999999999 = 9889_8999_9999_9999 * 10^-18 + /// 9.060176071990028E-7 = 9060_1760_7199_0028 * 10^-22 + /// This function modify only the mantissa and leave the exponent unchanged. + /// This function attempts to find the number with the maximum count of trailing zeros + /// within the neighborhood range [mantissa-delta ... mantissa+delta]. + /// If the number of trailing zeros is less than minZerosCount, the original value is returned. + /// The delta argument determines how far new values can be from the input value. + /// It defines the region within which candidates are searched. + /// Once the best candidate within the search region is found, it is checked to determine if the candidate is good enough. + /// The good candidate must have at least minZerosCount trailing zeros. + /// If this is true, the new value with a shortened mantissa is returned; otherwise, the original input value is returned. + /// For the examples above the + /// Decimal64.FromDouble(12.3456).ShortenMantissa(4, 1) => 12.3456 + /// Decimal64.FromDouble(720491.5510000001).ShortenMantissa(4, 1) => 720491.551 + /// Decimal64.FromDouble(0.009889899999999999).ShortenMantissa(4, 1) => 0.0098899 + /// Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(4, 1) => 0.000000906017607199003 + /// Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(30, 4) => 0.000000906017607199 + /// Decimal64.fromDouble(9.060176071990048E-7).shortenMantissa(30, 4) + /// => 9.060176071990048E-7 - Note: this value is not rounded because the delta is too small + /// for ...0048 range is [...0018 - ...0078] and there is no value with 4 trailing zeros in this range. + /// + /// + /// + /// + /// + public Decimal64 ShortenMantissa(UInt64 delta, uint minZerosCount) + { + return new Decimal64(DotNetImpl.ShortenMantissa(Bits, delta, minZerosCount)); + } + public Decimal64 Round(int n, RoundingMode roundType) { return new Decimal64(DotNetImpl.Round(Bits, n, roundType)); diff --git a/csharp/EPAM.Deltix.DFP/DotNetImpl.cs b/csharp/EPAM.Deltix.DFP/DotNetImpl.cs index 2c01f623..31f51a2f 100644 --- a/csharp/EPAM.Deltix.DFP/DotNetImpl.cs +++ b/csharp/EPAM.Deltix.DFP/DotNetImpl.cs @@ -401,10 +401,225 @@ public static UInt64 Abs(UInt64 value) return value & ~SignMask; } + public static bool IsSpecial(BID_UINT64 value) + { + return (value & MaskSpecial) == MaskSpecial; + } + + public static bool IsNonFinite(BID_UINT64 value) + { + return (value & MaskInfinityAndNan) == MaskInfinityAndNan; + } + #endregion #region Rounding + public static UInt64 ShortenMantissa(UInt64 value, UInt64 delta, uint minZerosCount) + { + if (delta < 0 || delta > MaxCoefficient / 10) + throw new ArgumentException("The delta value must be in [0.." + MaxCoefficient / 10 + "] range."); + // Can't happen with uint + //if (minZerosCount < 0) + // throw new ArgumentException("The minZerosCount value must be non-negative."); + + // No need this check because of delta range restriction. + // if (delta >= MAX_COEFFICIENT) + // return Decimal64Utils.ZERO; + + if (IsNonFinite(value) || delta == 0 || minZerosCount >= MaxFormatDigits) + return value; + + // final Decimal64Parts parts = tlsDecimal64Parts.get(); + // JavaImpl.toParts(value, parts); + BID_UINT64 partsSignMask; + int partsExponent; + BID_UINT64 partsCoefficient; + { // Copy-paste the toParts method for speedup + partsSignMask = value & MaskSign; + + if (IsSpecial(value)) + { + // if (IsNonFinite(value)) { + // partsExponent = 0; + // + // partsCoefficient = value & 0xFE03_FFFF_FFFF_FFFFL; + // if ((value & 0x0003_FFFF_FFFF_FFFFL) > MAX_COEFFICIENT) + // partsCoefficient = value & ~MASK_COEFFICIENT; + // if (isInfinity(value)) + // partsCoefficient = value & MASK_SIGN_INFINITY_NAN; // TODO: Why this was done?? + // } else + { + // Check for non-canonical values. + BID_UINT64 coefficient = (value & DotNetReImpl.LARGE_COEFF_MASK64) | DotNetReImpl.LARGE_COEFF_HIGH_BIT64; + partsCoefficient = coefficient > MaxCoefficient ? 0 : coefficient; + + // Extract exponent. + BID_UINT64 tmp = value >> DotNetReImpl.EXPONENT_SHIFT_LARGE64; + partsExponent = (int)(tmp & DotNetReImpl.EXPONENT_MASK64); + } + } + else + { + + // Extract exponent. Maximum biased value for "small exponent" is 0x2FF(*2=0x5FE), signed: [] + // upper 1/4 of the mask range is "special", as checked in the code above + BID_UINT64 tmp = value >> DotNetReImpl.EXPONENT_SHIFT_SMALL64; + partsExponent = (int)(tmp & DotNetReImpl.EXPONENT_MASK64); + + // Extract coefficient. + partsCoefficient = (value & DotNetReImpl.SMALL_COEFF_MASK64); + } + } + + if (partsCoefficient == 0) // This is a zero with any exponent + return Zero; + + if (partsCoefficient <= MaxCoefficient / 10) + { // Denormalized value case - normalize mantissa and exponent + int pei = Array.BinarySearch(PowersOfTen, partsCoefficient); + int expDiff = pei < 0 + ? MaxFormatDigits - ~pei + : MaxFormatDigits - pei - 1; + partsCoefficient *= PowersOfTen[expDiff]; + partsExponent -= expDiff; + } + // assert (partsCoefficient <= MAX_COEFFICIENT); + // assert (partsCoefficient > MAX_COEFFICIENT / 10); + // assert (Decimal64Utils.equals(value, pack(partsSignMask, partsExponent, partsCoefficient, BID_ROUNDING_TO_NEAREST))); + + // No need this check because of delta range restriction. + // if (partsCoefficient <= delta) // Downside of the interval is close to zero, + // return Decimal64Utils.ZERO; // this is nearly impossible but still can happen + + int ei = Array.BinarySearch(PowersOfTen, delta); + var deltaFloorPowerTen = ei >= 0 ? PowersOfTen[ei] : PowersOfTen[~ei - 1]; + + var rangeUp = partsCoefficient + delta; + var rangeDown = partsCoefficient - delta; + + BID_UINT32 fpsf = DotNetReImpl.BID_EXACT_STATUS; + { // Check the optimistic case first + var deltaFloorPowerTenUp = deltaFloorPowerTen * 10; // Note: this is not the ceil: consider the case when epsilon = 10^n + if (deltaFloorPowerTenUp < MaxCoefficient) + { + var coefficientResultUp = TryShorten(partsCoefficient, delta, rangeUp, rangeDown, deltaFloorPowerTenUp); + + if (coefficientResultUp != BID_UINT64.MaxValue) + { + if (coefficientResultUp % PowersOfTen[minZerosCount] == 0) + return DotNetReImpl.get_BID64(partsSignMask, partsExponent, coefficientResultUp, DotNetReImpl.BID_ROUNDING_TO_NEAREST, ref fpsf); + else + return value; + } + } + } + + var coefficientResult = TryShortenNoRangeCheck(partsCoefficient, delta, deltaFloorPowerTen); + if (coefficientResult % PowersOfTen[minZerosCount] == 0) + return DotNetReImpl.get_BID64(partsSignMask, partsExponent, coefficientResult, DotNetReImpl.BID_ROUNDING_TO_NEAREST, ref fpsf); + else + return value; + } + + private static BID_UINT64 TryShorten(BID_UINT64 partsCoefficient, BID_UINT64 delta, + BID_UINT64 rangeUp, BID_UINT64 rangeDown, + BID_UINT64 deltaPowerTen) + { + var coefficientResult = BID_UINT64.MaxValue; + var coefficientResultZerosCount = int.MinValue; + var delatPowerTenUp = deltaPowerTen * 10; + + { // Check ceiling + var coefficientUp = (rangeUp / deltaPowerTen) * deltaPowerTen; + if (rangeDown <= coefficientUp && coefficientUp <= rangeUp) + { + coefficientResult = coefficientUp; + coefficientResultZerosCount = (coefficientResult % delatPowerTenUp == 0 ? 1 : 0); + } + } + + { // Check flooring + var coefficientDown = (partsCoefficient / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientDown && rangeDown <= coefficientDown && coefficientDown <= rangeUp) + { + var coefficientDownZerosCount = (coefficientDown % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientDownZerosCount > coefficientResultZerosCount) + { + coefficientResult = coefficientDown; + coefficientResultZerosCount = coefficientDownZerosCount; + } + } + } + + { // Check half-up + var coefficientHalf = ((partsCoefficient + deltaPowerTen / 2) / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientHalf && rangeDown <= coefficientHalf && coefficientHalf <= rangeUp) + { + // If the number of zeros in coefficientHalf is not less than the number of zeros in cr, + // then coefficientHalf should be chosen. + // Since both numbers are multiplied by epsilonFloor10Up, it can be ignored + // The only next digit after epsilonFloor10Up could be checked, + // since cr and coefficientHalf differs in (delta - (epsilonFloor10Up / 2 - 1)). + var coefficientHalfZerosCount = (coefficientHalf % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientHalfZerosCount >= coefficientResultZerosCount) + { + coefficientResult = coefficientHalf; + coefficientResultZerosCount = coefficientHalfZerosCount; + } + } + } + + return coefficientResult; + } + + private static BID_UINT64 TryShortenNoRangeCheck(BID_UINT64 partsCoefficient, BID_UINT64 delta, + BID_UINT64 deltaPowerTen) + { + var coefficientResult = BID_UINT64.MaxValue; + var coefficientResultZerosCount = int.MinValue; + var delatPowerTenUp = deltaPowerTen * 10; + + { // Check ceiling + var coefficientUp = ((partsCoefficient + delta) / deltaPowerTen) * deltaPowerTen; + coefficientResult = coefficientUp; + coefficientResultZerosCount = (coefficientResult % delatPowerTenUp == 0 ? 1 : 0); + } + + { // Check flooring + var coefficientDown = (partsCoefficient / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientDown) + { + var coefficientDownZerosCount = (coefficientDown % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientDownZerosCount > coefficientResultZerosCount) + { + coefficientResult = coefficientDown; + coefficientResultZerosCount = coefficientDownZerosCount; + } + } + } + + { // Check half-up + var coefficientHalf = ((partsCoefficient + deltaPowerTen / 2) / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientHalf) + { + // If the number of zeros in coefficientHalf is not less than the number of zeros in cr, + // then coefficientHalf should be chosen. + // Since both numbers are multiplied by epsilonFloor10Up, it can be ignored + // The only next digit after epsilonFloor10Up could be checked, + // since cr and coefficientHalf differs in (delta - (epsilonFloor10Up / 2 - 1)). + var coefficientHalfZerosCount = (coefficientHalf % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientHalfZerosCount >= coefficientResultZerosCount) + { + coefficientResult = coefficientHalf; + coefficientResultZerosCount = coefficientHalfZerosCount; + } + } + } + + return coefficientResult; + } + public static UInt64 Round(UInt64 value, int n, RoundingMode roundType) { if (!IsFinite(value)) @@ -1681,6 +1896,10 @@ internal static String ToDebugString(UInt64 value) public const UInt64 InfinityMask = 0x7800000000000000UL; public const UInt64 SignedInfinityMask = 0xF800000000000000UL; + public const BID_UINT64 MaskSign = 0x8000000000000000UL; + public const BID_UINT64 MaskSpecial = 0x6000000000000000UL; + public const BID_UINT64 MaskInfinityAndNan = 0x7800000000000000UL; + public const UInt64 NaNMask = 0x7C00000000000000UL; public const UInt64 SignalingNaNMask = 0xFC00000000000000UL; diff --git a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java index 537380a6..d272632b 100644 --- a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java +++ b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java @@ -715,6 +715,44 @@ public Decimal64 average(final Decimal64 other) { /// region Rounding + /** + * This function is experimental. + * Returns a {@code DFP} number in some neighborhood of the input value with a maximally + * reduced number of digits. + * Explanation: + * Any finite {@code DFP} value can be represented as 16-digits integer number (mantissa) + * multiplied by some power of ten (exponent): + * 12.3456 = 1234_5600_0000_0000 * 10^-14 + * 720491.5510000001 = 7204_9155_1000_0001 * 10^-10 + * 0.009889899999999999 = 9889_8999_9999_9999 * 10^-18 + * 9.060176071990028E-7 = 9060_1760_7199_0028 * 10^-22 + * This function modify only the mantissa and leave the exponent unchanged. + * This function attempts to find the number with the maximum count of trailing zeros + * within the neighborhood range [mantissa-delta ... mantissa+delta]. + * If the number of trailing zeros is less than minZerosCount, the original value is returned. + * The delta argument determines how far new values can be from the input value. + * It defines the region within which candidates are searched. + * Once the best candidate within the search region is found, it is checked to determine if the candidate is good enough. + * The good candidate must have at least minZerosCount trailing zeros. + * If this is true, the new value with a shortened mantissa is returned; otherwise, the original input value is returned. + * For the examples above the + * Decimal64.fromDouble(12.3456).shortenMantissa(4, 1) => 12.3456 + * Decimal64.fromDouble(720491.5510000001).shortenMantissa(4, 1) => 720491.551 + * Decimal64.fromDouble(0.009889899999999999).shortenMantissa(4, 1) => 0.0098899 + * Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(4, 1) => 0.000000906017607199003 + * Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(30, 4) => 0.000000906017607199 + * Decimal64.fromDouble(9.060176071990048E-7).shortenMantissa(30, 4) + * => 9.060176071990048E-7 - Note: this value is not rounded because the delta is too small + * for ...0048 range is [...0018 - ...0078] and there is no value with 4 trailing zeros in this range. + * + * @param delta the maximal mantissa difference in [0..999999999999999] range. + * @param minZerosCount the minimal number of trailing zeros (must be non-negative). + * @return the {@code DFP} value + */ + public Decimal64 shortenMantissa(final long delta, final int minZerosCount) { + return new Decimal64(Decimal64Utils.shortenMantissa(value, delta, minZerosCount)); + } + public Decimal64 roundToReciprocal(final int r, final RoundingMode roundType) { return new Decimal64(Decimal64Utils.roundToReciprocal(value, r, roundType)); } diff --git a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java index ecdcb623..3ac2cd11 100644 --- a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java +++ b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java @@ -850,6 +850,46 @@ public static long mean(@Decimal final long a, @Decimal final long b) { /// region Rounding + /** + * This function is experimental. + * Returns a {@code DFP} number in some neighborhood of the input value with a maximally + * reduced number of digits. + * Explanation: + * Any finite {@code DFP} value can be represented as 16-digits integer number (mantissa) + * multiplied by some power of ten (exponent): + * 12.3456 = 1234_5600_0000_0000 * 10^-14 + * 720491.5510000001 = 7204_9155_1000_0001 * 10^-10 + * 0.009889899999999999 = 9889_8999_9999_9999 * 10^-18 + * 9.060176071990028E-7 = 9060_1760_7199_0028 * 10^-22 + * This function modify only the mantissa and leave the exponent unchanged. + * This function attempts to find the number with the maximum count of trailing zeros + * within the neighborhood range [mantissa-delta ... mantissa+delta]. + * If the number of trailing zeros is less than minZerosCount, the original value is returned. + * The delta argument determines how far new values can be from the input value. + * It defines the region within which candidates are searched. + * Once the best candidate within the search region is found, it is checked to determine if the candidate is good enough. + * The good candidate must have at least minZerosCount trailing zeros. + * If this is true, the new value with a shortened mantissa is returned; otherwise, the original input value is returned. + * For the examples above the + * Decimal64.fromDouble(12.3456).shortenMantissa(4, 1) => 12.3456 + * Decimal64.fromDouble(720491.5510000001).shortenMantissa(4, 1) => 720491.551 + * Decimal64.fromDouble(0.009889899999999999).shortenMantissa(4, 1) => 0.0098899 + * Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(4, 1) => 0.000000906017607199003 + * Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(30, 4) => 0.000000906017607199 + * Decimal64.fromDouble(9.060176071990048E-7).shortenMantissa(30, 4) + * => 9.060176071990048E-7 - Note: this value is not rounded because the delta is too small + * for ...0048 range is [...0018 - ...0078] and there is no value with 4 trailing zeros in this range. + * + * @param value {@code DFP} argument for mantissa shorting + * @param delta the maximal mantissa difference in [0..999999999999999] range. + * @param minZerosCount the minimal number of trailing zeros (must be non-negative). + * @return the {@code DFP} value + */ + @Decimal + public static long shortenMantissa(@Decimal final long value, final long delta, final int minZerosCount) { + return JavaImpl.shortenMantissa(value, delta, minZerosCount); + } + /** * Returns the {@code DFP} value that is rounded to the value, reciprocal to r, according the selected rounding type. * @@ -2464,6 +2504,21 @@ public static long minChecked(@Decimal final long a, @Decimal final long b) { return min(a, b); } + /** + * Implements {@link Decimal64#shortenMantissa(long, int)}, adds null check; do not use directly. + * + * @param value {@code DFP} argument for mantissa shorting + * @param delta the maximal mantissa difference in [0..999999999999999] range. + * @param minZerosCount the minimal number of trailing zeros (must be non-negative). + * @return the {@code DFP} value + */ + @Decimal + @Deprecated + public static long shortenMantissaChecked(@Decimal final long value, final long delta, final int minZerosCount) { + checkNull(value); + return shortenMantissa(value, delta, minZerosCount); + } + /** * Implements {@link Decimal64#roundToReciprocal(int, RoundingMode)}, adds null check; do not use directly. * diff --git a/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java b/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java index 4a1dee48..9889d36e 100644 --- a/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java +++ b/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java @@ -3364,4 +3364,189 @@ static boolean isRoundedToReciprocalImpl(final int addExponent, return true; } + + public static long shortenMantissa(final long value, final long delta, final int minZerosCount) { + if (delta < 0 || delta > MAX_COEFFICIENT / 10) + throw new IllegalArgumentException("The delta value must be in [0.." + MAX_COEFFICIENT / 10 + "] range."); + if (minZerosCount < 0) + throw new IllegalArgumentException("The minZerosCount value must be non-negative."); + + // No need this check because of delta range restriction. +// if (delta >= MAX_COEFFICIENT) +// return Decimal64Utils.ZERO; + + if (isNonFinite(value) || delta == 0 || minZerosCount >= MAX_FORMAT_DIGITS) + return value; + +// final Decimal64Parts parts = tlsDecimal64Parts.get(); +// JavaImpl.toParts(value, parts); + long partsCoefficient; + long partsSignMask; + int partsExponent; + { // Copy-paste the toParts method for speedup + partsSignMask = value & MASK_SIGN; + + if (isSpecial(value)) { +// if (isNonFinite(value)) { +// partsExponent = 0; +// +// partsCoefficient = value & 0xFE03_FFFF_FFFF_FFFFL; +// if ((value & 0x0003_FFFF_FFFF_FFFFL) > MAX_COEFFICIENT) +// partsCoefficient = value & ~MASK_COEFFICIENT; +// if (isInfinity(value)) +// partsCoefficient = value & MASK_SIGN_INFINITY_NAN; // TODO: Why this was done?? +// } else + { + // Check for non-canonical values. + final long coefficient = (value & LARGE_COEFFICIENT_MASK) | LARGE_COEFFICIENT_HIGH_BIT; + partsCoefficient = coefficient > MAX_COEFFICIENT ? 0 : coefficient; + + // Extract exponent. + final long tmp = value >> EXPONENT_SHIFT_LARGE; + partsExponent = (int) (tmp & EXPONENT_MASK); + } + } else { + + // Extract exponent. Maximum biased value for "small exponent" is 0x2FF(*2=0x5FE), signed: [] + // upper 1/4 of the mask range is "special", as checked in the code above + final long tmp = value >> EXPONENT_SHIFT_SMALL; + partsExponent = (int) (tmp & EXPONENT_MASK); + + // Extract coefficient. + partsCoefficient = (value & SMALL_COEFFICIENT_MASK); + } + } + + if (partsCoefficient == 0) // This is a zero with any exponent + return Decimal64Utils.ZERO; + + if (partsCoefficient <= MAX_COEFFICIENT / 10) { // Denormalized value case - normalize mantissa and exponent + int ei = Arrays.binarySearch(POWERS_OF_TEN, partsCoefficient); + final int expDiff = ei < 0 + ? MAX_FORMAT_DIGITS - ~ei + : MAX_FORMAT_DIGITS - ei - 1; + partsCoefficient *= POWERS_OF_TEN[expDiff]; + partsExponent -= expDiff; + } +// assert (partsCoefficient <= MAX_COEFFICIENT); +// assert (partsCoefficient > MAX_COEFFICIENT / 10); +// assert (Decimal64Utils.equals(value, pack(partsSignMask, partsExponent, partsCoefficient, BID_ROUNDING_TO_NEAREST))); + + // No need this check because of delta range restriction. +// if (partsCoefficient <= delta) // Downside of the interval is close to zero, +// return Decimal64Utils.ZERO; // this is nearly impossible but still can happen + + int ei = Arrays.binarySearch(POWERS_OF_TEN, delta); + final long deltaFloorPowerTen = ei >= 0 ? POWERS_OF_TEN[ei] : POWERS_OF_TEN[~ei - 1]; + + final long rangeUp = partsCoefficient + delta; + final long rangeDown = partsCoefficient - delta; + + { // Check the optimistic case first + final long deltaFloorPowerTenUp = deltaFloorPowerTen * 10; // Note: this is not the ceil: consider the case when epsilon = 10^n + if (deltaFloorPowerTenUp < MAX_COEFFICIENT) { + final long coefficientResult = tryShorten(partsCoefficient, delta, rangeUp, rangeDown, deltaFloorPowerTenUp); + + if (coefficientResult != Long.MIN_VALUE) { + if (coefficientResult % POWERS_OF_TEN[minZerosCount] == 0) + return pack(partsSignMask, partsExponent, coefficientResult, BID_ROUNDING_TO_NEAREST); + else + return value; + } + } + } + + final long coefficientResult = tryShortenNoRangeCheck(partsCoefficient, delta, deltaFloorPowerTen); + if (coefficientResult % POWERS_OF_TEN[minZerosCount] == 0) + return pack(partsSignMask, partsExponent, coefficientResult, BID_ROUNDING_TO_NEAREST); + else + return value; + } + + private static long tryShorten(final long partsCoefficient, final long delta, + final long rangeUp, final long rangeDown, + final long deltaPowerTen) { + long coefficientResult = Long.MIN_VALUE; + int coefficientResultZerosCount = Integer.MIN_VALUE; + final long delatPowerTenUp = deltaPowerTen * 10; + + { // Check ceiling + final long coefficientUp = (rangeUp / deltaPowerTen) * deltaPowerTen; + if (rangeDown <= coefficientUp && coefficientUp <= rangeUp) { + coefficientResult = coefficientUp; + coefficientResultZerosCount = (coefficientResult % delatPowerTenUp == 0 ? 1 : 0); + } + } + + { // Check flooring + final long coefficientDown = (partsCoefficient / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientDown && rangeDown <= coefficientDown && coefficientDown <= rangeUp) { + final int coefficientDownZerosCount = (coefficientDown % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientDownZerosCount > coefficientResultZerosCount) { + coefficientResult = coefficientDown; + coefficientResultZerosCount = coefficientDownZerosCount; + } + } + } + + { // Check half-up + final long coefficientHalf = ((partsCoefficient + deltaPowerTen / 2) / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientHalf && rangeDown <= coefficientHalf && coefficientHalf <= rangeUp) { + // If the number of zeros in coefficientHalf is not less than the number of zeros in cr, + // then coefficientHalf should be chosen. + // Since both numbers are multiplied by epsilonFloor10Up, it can be ignored + // The only next digit after epsilonFloor10Up could be checked, + // since cr and coefficientHalf differs in (delta - (epsilonFloor10Up / 2 - 1)). + final int coefficientHalfZerosCount = (coefficientHalf % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientHalfZerosCount >= coefficientResultZerosCount) { + coefficientResult = coefficientHalf; + coefficientResultZerosCount = coefficientHalfZerosCount; + } + } + } + + return coefficientResult; + } + + private static long tryShortenNoRangeCheck(final long partsCoefficient, final long delta, + final long deltaPowerTen) { + long coefficientResult = Long.MIN_VALUE; + int coefficientResultZerosCount = Integer.MIN_VALUE; + final long delatPowerTenUp = deltaPowerTen * 10; + + { // Check ceiling + final long coefficientUp = ((partsCoefficient + delta) / deltaPowerTen) * deltaPowerTen; + coefficientResult = coefficientUp; + coefficientResultZerosCount = (coefficientResult % delatPowerTenUp == 0 ? 1 : 0); + } + + { // Check flooring + final long coefficientDown = (partsCoefficient / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientDown) { + final int coefficientDownZerosCount = (coefficientDown % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientDownZerosCount > coefficientResultZerosCount) { + coefficientResult = coefficientDown; + coefficientResultZerosCount = coefficientDownZerosCount; + } + } + } + + { // Check half-up + final long coefficientHalf = ((partsCoefficient + deltaPowerTen / 2) / deltaPowerTen) * deltaPowerTen; + if (coefficientResult != coefficientHalf) { + // If the number of zeros in coefficientHalf is not less than the number of zeros in cr, + // then coefficientHalf should be chosen. + // Since both numbers are multiplied by epsilonFloor10Up, it can be ignored + // The only next digit after epsilonFloor10Up could be checked, + // since cr and coefficientHalf differs in (delta - (epsilonFloor10Up / 2 - 1)). + final int coefficientHalfZerosCount = (coefficientHalf % delatPowerTenUp == 0 ? 1 : 0); + if (coefficientHalfZerosCount >= coefficientResultZerosCount) { + coefficientResult = coefficientHalf; + coefficientResultZerosCount = coefficientHalfZerosCount; + } + } + } + + return coefficientResult; + } } diff --git a/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java b/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java index d3465914..baad3e22 100644 --- a/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java +++ b/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java @@ -210,4 +210,164 @@ public void testFromDecimalDoublesCases() { } static final int N = 5000000; + + @Test + public void testShortenMantissaDenormalized() { + Assert.assertEquals(Decimal64.ZERO, Decimal64.ZERO.shortenMantissa(100, 1)); + + Assert.assertEquals(Decimal64.ZERO, + Decimal64.fromUnderlying( + JavaImpl.packBasic(0, JavaImpl.BIASED_EXPONENT_MAX_VALUE, 0)) + .shortenMantissa(100, 1)); + + { + final Decimal64 d64 = Decimal64.fromUnderlying( + JavaImpl.packBasic(0, JavaImpl.BIASED_EXPONENT_MAX_VALUE, 1)); + Assert.assertEquals(d64, d64.shortenMantissa(100, 1)); + } + + { + final Decimal64 d64 = Decimal64.fromUnderlying( + JavaImpl.packBasic(0, JavaImpl.BIASED_EXPONENT_MAX_VALUE, 22)); + Assert.assertEquals(d64, d64.shortenMantissa(100000, 1)); + } + + { + long m = 5999_9999_9999_8200L; + int e = -18 + JavaImpl.EXPONENT_BIAS; + + long delta = m % 10000; + delta = Math.min(delta, 10000 - delta); + int z = 3; + Decimal64 rNorm = Decimal64.fromUnderlying(JavaImpl.packBasic(0, e, m)) + .shortenMantissa(delta, z); + + Decimal64 rDenorm = Decimal64.fromUnderlying(JavaImpl.packBasic(0, e + 2, m / 100)) + .shortenMantissa(delta, z); + + Assert.assertEquals(rNorm, rDenorm); + } + } + + @Test + public void testShortenMantissaBigDelta() { + Assert.assertEquals(Decimal64.parse("10000000000000000"), + Decimal64.parse("9999000000000000").shortenMantissa(JavaImpl.MAX_COEFFICIENT / 10, 2)); + } + + @Test + public void testShortenMantissaCase006() { + String testString = "0.006"; + final double testValue = Double.parseDouble(testString); + final double testX = 0.005999999999998265; // Math.nextDown(testValue); + + Decimal64 d64 = Decimal64.fromDouble(testX).shortenMantissa(1735, 1); + Assert.assertEquals(testString, d64.toString()); + + Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(2, 1); + } + + @Test + public void testShortenMantissaRandom() { + final int randomSeed = new SecureRandom().nextInt(); + final Random random = new Random(randomSeed); + + try { + for (int iteration = 0; iteration < N; ++iteration) { + long mantissa = generateMantissa(random, Decimal64Utils.MAX_SIGNIFICAND_DIGITS); + int error = random.nextInt(3) - 1; + mantissa = Math.min(JavaImpl.MAX_COEFFICIENT, Math.max(0, mantissa + error)); + if (mantissa <= JavaImpl.MAX_COEFFICIENT / 10) + mantissa = mantissa * 10; + + long delta = generateMantissa(random, 0); + if (delta > JavaImpl.MAX_COEFFICIENT/10) + delta = delta / 10; + + checkShortenMantissaCase(mantissa, delta); + } + } catch (final Throwable e) { + throw new RuntimeException("Random seed " + randomSeed + " exception: " + e.getMessage(), e); + } + } + + @Test + public void testShortenMantissaCase() { + checkShortenMantissaCase(9999888877776001L, 1000); + checkShortenMantissaCase(1230000000000000L, 80); + checkShortenMantissaCase(1230000000000075L, 80); + checkShortenMantissaCase(1229999999999925L, 80); + checkShortenMantissaCase(4409286553495543L, 900); + checkShortenMantissaCase(4409286553495000L, 1000); + checkShortenMantissaCase(4409286550000000L, 81117294); + checkShortenMantissaCase(9010100000000001L, 999999999999999L); + checkShortenMantissaCase(8960196546869015L, 1); + checkShortenMantissaCase(4700900091799999L, 947076117508L); + checkShortenMantissaCase(5876471737721999L, 91086); + checkShortenMantissaCase(6336494570000000L, 6092212816L); + checkShortenMantissaCase(8960196546869011L, 999999999999999L); + checkShortenMantissaCase(1519453608576584L, 3207L); + } + + private static void checkShortenMantissaCase(final long mantissa, final long delta) { + try { + final long bestSolution = shortenMantissaDirect(mantissa, delta); + + final long test64 = Decimal64Utils.toLong(Decimal64Utils.shortenMantissa(Decimal64Utils.fromLong(mantissa), delta, 0)); + + if (test64 != bestSolution) + throw new RuntimeException("The mantissa(=" + mantissa + ") and delta(=" + delta + ") produce test64(=" + test64 + ") != bestSolution(=" + bestSolution + ")."); + } catch (Throwable e) { + throw new RuntimeException("The mantissa(=" + mantissa + ") and delta(=" + delta + ") produce exception.", e); + } + } + + private static long shortenMantissaDirect(final long mantissa, final long delta) { + long rgUp = mantissa + delta; + long rgDown = mantissa - delta; + + if (mantissa <= JavaImpl.MAX_COEFFICIENT / 10 || mantissa > JavaImpl.MAX_COEFFICIENT) + throw new IllegalArgumentException("The mantissa(=" + mantissa + ") must be in (" + JavaImpl.MAX_COEFFICIENT / 10 + ".." + JavaImpl.MAX_COEFFICIENT + "] range"); + + long bestSolution = Long.MIN_VALUE; + if (rgDown > 0) { + long mUp = (mantissa / 10) * 10; + long mFactor = 1; + + long bestDifference = Long.MAX_VALUE; + int bestPosition = -1; + + for (int replacePosition = 0; + replacePosition < Decimal64Utils.MAX_SIGNIFICAND_DIGITS + 1; + ++replacePosition, mUp = (mUp / 100) * 10, mFactor *= 10) { + for (int d = 0; d < 10; ++d) { + final long mTest = (mUp + d) * mFactor; + if (rgDown <= mTest && mTest <= rgUp) { + final long md = Math.abs(mantissa - mTest); + if (bestPosition < replacePosition || + (bestPosition == replacePosition && bestDifference >= md)) { + bestPosition = replacePosition; + bestDifference = md; + bestSolution = mTest; + } + } + } + } + } else { + bestSolution = 0; + } + + return bestSolution; + } + + private static long generateMantissa(final Random random, int minimalLength) { + int mLen = (1 + random.nextInt(Decimal64Utils.MAX_SIGNIFICAND_DIGITS) /*[1..16]*/); + long m = 1 + random.nextInt(9); + int i = 1; + for (; i < mLen; ++i) + m = m * 10 + random.nextInt(10); + for (; i < minimalLength; ++i) + m = m * 10; + return m; + } }