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;
+ }
}