From fa2e2dfdd1b2f643c052f237fe093f564068a15a Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Sat, 22 Feb 2025 00:41:35 +0300 Subject: [PATCH 1/9] Just save my work [skip ci] --- .../java/com/epam/deltix/dfp/Decimal64.java | 4 + .../com/epam/deltix/dfp/Decimal64Utils.java | 5 + .../java/com/epam/deltix/dfp/JavaImpl.java | 364 ++++++++++++++++++ .../com/epam/deltix/dfp/FromDoubleTest.java | 242 ++++++++++++ 4 files changed, 615 insertions(+) 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..eb05c47b 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,10 @@ public Decimal64 average(final Decimal64 other) { /// region Rounding + 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..83c22d2a 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,11 @@ public static long mean(@Decimal final long a, @Decimal final long b) { /// region Rounding + @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. * 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..23be39a7 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,368 @@ static boolean isRoundedToReciprocalImpl(final int addExponent, return true; } + + public static long shortenMantissa000(final long value, final long delta, final int minZerosCount) { + if (delta < 0) + throw new IllegalArgumentException("The delta value must be non-negative."); + if (minZerosCount < 0) + throw new IllegalArgumentException("The minZerosCount value must be non-negative."); + + 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))); + } + + 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 epsilonFloor10 = ei >= 0 ? POWERS_OF_TEN[ei] : POWERS_OF_TEN[~ei - 1]; + final long epsilonFloor10Up = epsilonFloor10 * 10; // Note: this is not the ceil: consider the case when epsilon = 10^n + + final long cUp = partsCoefficient + delta; + final long cDown = partsCoefficient - delta; + + if (epsilonFloor10Up < MAX_COEFFICIENT) { + final long cr = (cUp / epsilonFloor10Up) * epsilonFloor10Up; + if (cDown <= cr && cr <= cUp) { // Optimistic case + if (cr % POWERS_OF_TEN[minZerosCount] == 0) + return pack(partsSignMask, partsExponent, cr, BID_ROUNDING_TO_NEAREST); + else + return value; + } + } + + { + final long cr = (cUp / epsilonFloor10) * epsilonFloor10; + if (!(cDown <= cr && cr <= cUp)) { + throw new RuntimeException("@DEBUG: This could not be happen."); + } + + final long cCut = cUp / epsilonFloor10; + if (cCut % 10 == 0) + throw new RuntimeException("@DEBUG: How this can happens?"); + + return value; + } + } + + public static long shortenMantissa001(final long value, final long delta, final int minZerosCount) { + if (delta < 0) + throw new IllegalArgumentException("The delta value must be non-negative."); + if (minZerosCount < 0) + throw new IllegalArgumentException("The minZerosCount value must be non-negative."); + + 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))); + } + + 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 epsilonFloor10 = ei >= 0 ? POWERS_OF_TEN[ei] : POWERS_OF_TEN[~ei - 1]; + final long epsilonFloor10Up = epsilonFloor10 * 10; // Note: this is not the ceil: consider the case when epsilon = 10^n + + final long cUp = partsCoefficient + delta; + final long cDown = partsCoefficient - delta; + + if (epsilonFloor10Up < MAX_COEFFICIENT) { + long cr = (cUp / epsilonFloor10Up) * epsilonFloor10Up; + if (cDown <= cr && cr <= cUp) { // Optimistic case + // Than try to find the solution more close to input value + final long cr10 = ((partsCoefficient + epsilonFloor10Up / 2 - 1) / epsilonFloor10Up) * epsilonFloor10Up; + // If the number of zeros in cr10 is not less than the number of zeros in cr, + // then cr10 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 cr10 differs in (delta - (epsilonFloor10Up / 2 - 1)). + if (cr != cr10) { + final long epsilonFloor10UpUp = epsilonFloor10Up * 10; + final int crZerosCount = (cr % epsilonFloor10UpUp == 0 ? 1 : 0); //+ zerosCount(epsilonFloor10Up) -- ignore common part + final int cr10ZerosCount = (cr10 % epsilonFloor10UpUp == 0 ? 1 : 0); //+ zerosCount(epsilonFloor10Up) -- ignore common part + if (cr10ZerosCount >= crZerosCount) + cr = cr10; + } + + if (cr % POWERS_OF_TEN[minZerosCount] == 0) + return pack(partsSignMask, partsExponent, cr, BID_ROUNDING_TO_NEAREST); + else + return value; + } + + + } + + { + final long cr = ((partsCoefficient + epsilonFloor10 / 2 - 1) / epsilonFloor10) * epsilonFloor10; + if (!(cDown <= cr && cr <= cUp)) { + throw new RuntimeException("@DEBUG: This could not be happen."); + } + + if (cr % POWERS_OF_TEN[minZerosCount] == 0) + return pack(partsSignMask, partsExponent, cr, BID_ROUNDING_TO_NEAREST); + else + return value; + } + } + + 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."); + + 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))); + } + + 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 = tryShorten(partsCoefficient, delta, rangeUp, rangeDown, deltaFloorPowerTen); + if (coefficientResult != Long.MIN_VALUE) { + if (coefficientResult % POWERS_OF_TEN[minZerosCount] == 0) + return pack(partsSignMask, partsExponent, coefficientResult, BID_ROUNDING_TO_NEAREST); + else + return value; + } + + throw new RuntimeException("WTF?"); + } + + 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; + } } 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..28b88df1 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,246 @@ 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("9000000000000000"), + Decimal64.parse("9999000000000000").shortenMantissa(JavaImpl.MAX_COEFFICIENT / 2, 2)); + } + + @Test + public void testShortenMantissaDeltaPower10() { + checkShortenMantissaCase(9999888877776001L, 1000); + } + + @Test + public void testShortenMantissaCase006() { + + +// final long tmp = value >> JavaImpl.EXPONENT_SHIFT_SMALL; +// partsExponent = (int) (tmp & JavaImpl.EXPONENT_MASK); +// +// // Extract coefficient. +// partsCoefficient = (value & JavaImpl.SMALL_COEFFICIENT_MASK); + + + 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, 1); + Assert.assertEquals(testString, d64.toString()); + + Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(2, 1); + } + + @Test + public void testShortenMantissaCaseBigShift() { + Assert.assertEquals( + Decimal64.fromLong(9876_0000_0000_0000L), + Decimal64.fromLong(9876_0000_0000_0075L) + .shortenMantissa(110, 1)); + + Assert.assertEquals( + Decimal64.fromLong(9876_0000_0000_0000L), + Decimal64.fromLong(9875_9999_9999_9925L) + .shortenMantissa(110, 1)); + } + + @Test + public void testShortenMantissaRandom() { + final int randomSeed = new SecureRandom().nextInt(); + final Random random = new Random(randomSeed); + + try { + for (int iteration = 0; iteration < 1000_000; ++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(4409286550000000L, 81117294); + checkShortenMantissaCase(9010100000000001L, 999999999999999L); + checkShortenMantissaCase(8960196546869015L, 1); + checkShortenMantissaCase(4700900091799999L, 947076117508L); + checkShortenMantissaCase(5876471737721999L, 91086); + checkShortenMantissaCase(6336494570000000L, 6092212816L); + checkShortenMantissaCase(8960196546869011L, 999999999999999L); + checkShortenMantissaCase(1519453608576584L, 3207L); + } + + private void checkShortenMantissaCase(final long mantissa, final long delta) { + try { + final long bestSolution = shortenMantissaDirect(mantissa, delta); + + final Decimal64 test64 = Decimal64.fromLong(mantissa).shortenMantissa(delta, 0); + + if (test64.longValue() != 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; + } + + @Test + public void testShortenMantissaPrecision() { + final int randomSeed = new SecureRandom().nextInt(); + final Random random = new Random(randomSeed); + + int bugsCount = 0; + for (int iteration = 0; iteration < 1000; ++iteration) { + int mantissaLength = 2 + random.nextInt(Decimal64Utils.MAX_SIGNIFICAND_DIGITS - 4); + int exponent = random.nextInt(4 * Decimal64Utils.MAX_SIGNIFICAND_DIGITS) - 2 * Decimal64Utils.MAX_SIGNIFICAND_DIGITS; + + final StringBuilder sb = new StringBuilder(); + sb.append(1 + random.nextInt(9)).append('.'); + for (int i = 1; i < mantissaLength - 1; ++i) + sb.append(random.nextInt(10)); + sb.append(1 + random.nextInt(9)); + sb.append('E'); + sb.append(exponent); + + String testString = sb.toString(); + double testValue = Double.parseDouble(testString); + boolean goUp = random.nextInt(2) > 0; + double nextValue = goUp ? Math.nextUp(testValue) : Math.nextDown(testValue); + + final Decimal64 d64 = Decimal64.fromDouble(nextValue).shortenMantissa(3, 2); + final String d64s = d64.toScientificString(); + + int latestMantissaDigit = d64s.indexOf('e'); + while (latestMantissaDigit > 1 && d64s.charAt(latestMantissaDigit - 1) == '0') + latestMantissaDigit--; + + if (latestMantissaDigit != mantissaLength + 1) { + bugsCount++; + System.out.println("The Math." + (goUp ? "nextUp" : "nextDown") + "(Double.parseDouble(\"" + testString + "\")) with the value " + nextValue + " incorrectly converted to " + d64s); + } + } + + if (bugsCount != 0) { + throw new RuntimeException("There are a " + bugsCount + " bugs. See the report for random seed " + randomSeed); + } + } + + @Test + public void testShortenMantissa() { + final int randomSeed = -313667659;//new SecureRandom().nextInt(); + final Random random = new Random(randomSeed); + + final double testValue = 0.006; + final double ulp = Math.ulp(testValue); + + + Decimal64 d641 = Decimal64.fromDouble(Double.longBitsToDouble(4649175626595015063L)).shortenMantissa(3, 0); + + Decimal64 d642 = Decimal64.fromDouble(9.060176071990029E-7).shortenMantissa(3, 3); + + for (int d = -1000; d <= 1000; ++d) { + for (int e = 1; e <= 1000; ++e) { + Decimal64.fromDouble(testValue + d * ulp).shortenMantissa(e, 1); + } + } + } } From 8320f001f1b2da6fe96bd37636cced3aadfbe8fd Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Sat, 22 Feb 2025 15:50:08 +0300 Subject: [PATCH 2/9] Java: shortenMantissa: tiny speedup --- .../java/com/epam/deltix/dfp/JavaImpl.java | 56 ++++++++-- .../com/epam/deltix/dfp/FromDoubleTest.java | 102 ++---------------- 2 files changed, 57 insertions(+), 101 deletions(-) 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 23be39a7..60158e42 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 @@ -3673,15 +3673,11 @@ public static long shortenMantissa(final long value, final long delta, final int } } - final long coefficientResult = tryShorten(partsCoefficient, delta, rangeUp, rangeDown, deltaFloorPowerTen); - if (coefficientResult != Long.MIN_VALUE) { - if (coefficientResult % POWERS_OF_TEN[minZerosCount] == 0) - return pack(partsSignMask, partsExponent, coefficientResult, BID_ROUNDING_TO_NEAREST); - else - return value; - } - - throw new RuntimeException("WTF?"); + 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, @@ -3728,4 +3724,46 @@ private static long tryShorten(final long partsCoefficient, final long delta, 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 28b88df1..1b37e34a 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 @@ -255,52 +255,25 @@ public void testShortenMantissaBigDelta() { Decimal64.parse("9999000000000000").shortenMantissa(JavaImpl.MAX_COEFFICIENT / 2, 2)); } - @Test - public void testShortenMantissaDeltaPower10() { - checkShortenMantissaCase(9999888877776001L, 1000); - } - @Test public void testShortenMantissaCase006() { - - -// final long tmp = value >> JavaImpl.EXPONENT_SHIFT_SMALL; -// partsExponent = (int) (tmp & JavaImpl.EXPONENT_MASK); -// -// // Extract coefficient. -// partsCoefficient = (value & JavaImpl.SMALL_COEFFICIENT_MASK); - - 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, 1); + Decimal64 d64 = Decimal64.fromDouble(testX).shortenMantissa(1735, 1); Assert.assertEquals(testString, d64.toString()); Decimal64.fromDouble(9.060176071990028E-7).shortenMantissa(2, 1); } - @Test - public void testShortenMantissaCaseBigShift() { - Assert.assertEquals( - Decimal64.fromLong(9876_0000_0000_0000L), - Decimal64.fromLong(9876_0000_0000_0075L) - .shortenMantissa(110, 1)); - - Assert.assertEquals( - Decimal64.fromLong(9876_0000_0000_0000L), - Decimal64.fromLong(9875_9999_9999_9925L) - .shortenMantissa(110, 1)); - } - @Test public void testShortenMantissaRandom() { final int randomSeed = new SecureRandom().nextInt(); final Random random = new Random(randomSeed); try { - for (int iteration = 0; iteration < 1000_000; ++iteration) { + 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)); @@ -320,6 +293,12 @@ public void testShortenMantissaRandom() { @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); @@ -334,9 +313,9 @@ private void checkShortenMantissaCase(final long mantissa, final long delta) { try { final long bestSolution = shortenMantissaDirect(mantissa, delta); - final Decimal64 test64 = Decimal64.fromLong(mantissa).shortenMantissa(delta, 0); + final long test64 = Decimal64Utils.toLong(Decimal64Utils.shortenMantissa(Decimal64Utils.fromLong(mantissa), delta, 0)); - if (test64.longValue() != bestSolution) + 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); @@ -391,65 +370,4 @@ private static long generateMantissa(final Random random, int minimalLength) { m = m * 10; return m; } - - @Test - public void testShortenMantissaPrecision() { - final int randomSeed = new SecureRandom().nextInt(); - final Random random = new Random(randomSeed); - - int bugsCount = 0; - for (int iteration = 0; iteration < 1000; ++iteration) { - int mantissaLength = 2 + random.nextInt(Decimal64Utils.MAX_SIGNIFICAND_DIGITS - 4); - int exponent = random.nextInt(4 * Decimal64Utils.MAX_SIGNIFICAND_DIGITS) - 2 * Decimal64Utils.MAX_SIGNIFICAND_DIGITS; - - final StringBuilder sb = new StringBuilder(); - sb.append(1 + random.nextInt(9)).append('.'); - for (int i = 1; i < mantissaLength - 1; ++i) - sb.append(random.nextInt(10)); - sb.append(1 + random.nextInt(9)); - sb.append('E'); - sb.append(exponent); - - String testString = sb.toString(); - double testValue = Double.parseDouble(testString); - boolean goUp = random.nextInt(2) > 0; - double nextValue = goUp ? Math.nextUp(testValue) : Math.nextDown(testValue); - - final Decimal64 d64 = Decimal64.fromDouble(nextValue).shortenMantissa(3, 2); - final String d64s = d64.toScientificString(); - - int latestMantissaDigit = d64s.indexOf('e'); - while (latestMantissaDigit > 1 && d64s.charAt(latestMantissaDigit - 1) == '0') - latestMantissaDigit--; - - if (latestMantissaDigit != mantissaLength + 1) { - bugsCount++; - System.out.println("The Math." + (goUp ? "nextUp" : "nextDown") + "(Double.parseDouble(\"" + testString + "\")) with the value " + nextValue + " incorrectly converted to " + d64s); - } - } - - if (bugsCount != 0) { - throw new RuntimeException("There are a " + bugsCount + " bugs. See the report for random seed " + randomSeed); - } - } - - @Test - public void testShortenMantissa() { - final int randomSeed = -313667659;//new SecureRandom().nextInt(); - final Random random = new Random(randomSeed); - - final double testValue = 0.006; - final double ulp = Math.ulp(testValue); - - - Decimal64 d641 = Decimal64.fromDouble(Double.longBitsToDouble(4649175626595015063L)).shortenMantissa(3, 0); - - Decimal64 d642 = Decimal64.fromDouble(9.060176071990029E-7).shortenMantissa(3, 3); - - for (int d = -1000; d <= 1000; ++d) { - for (int e = 1; e <= 1000; ++e) { - Decimal64.fromDouble(testValue + d * ulp).shortenMantissa(e, 1); - } - } - } } From dcd4c799b797241f13d27d45b4e213dc7815212e Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Mon, 24 Feb 2025 11:37:50 +0300 Subject: [PATCH 3/9] Java: Add javaDoc for the shortenMantissa function --- .../java/com/epam/deltix/dfp/Decimal64.java | 24 +++++++++++ .../com/epam/deltix/dfp/Decimal64Utils.java | 40 +++++++++++++++++++ .../java/com/epam/deltix/dfp/JavaImpl.java | 17 ++++---- 3 files changed, 73 insertions(+), 8 deletions(-) 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 eb05c47b..4519bf18 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,30 @@ public Decimal64 average(final Decimal64 other) { /// region Rounding + /** + * 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. + * 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 + * + * @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)); } 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 83c22d2a..3022925c 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,31 @@ public static long mean(@Decimal final long a, @Decimal final long b) { /// region Rounding + /** + * 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. + * 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 + * + * @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); @@ -2469,6 +2494,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 60158e42..1f1b6fb0 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 @@ -3589,8 +3589,9 @@ public static long shortenMantissa(final long value, final long delta, final int if (minZerosCount < 0) throw new IllegalArgumentException("The minZerosCount value must be non-negative."); - if (delta >= MAX_COEFFICIENT) - return Decimal64Utils.ZERO; + // 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; @@ -3644,14 +3645,14 @@ public static long shortenMantissa(final long value, final long delta, final int : 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))); } +// assert (partsCoefficient <= MAX_COEFFICIENT); +// assert (partsCoefficient > MAX_COEFFICIENT / 10); +// assert (Decimal64Utils.equals(value, pack(partsSignMask, partsExponent, partsCoefficient, BID_ROUNDING_TO_NEAREST))); - if (partsCoefficient <= delta) // Downside of the interval is close to zero, - return Decimal64Utils.ZERO; // this is nearly impossible but still can happen + // 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]; From 4f11b3915972f285e6b57e57936c3ee4408390ca Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Mon, 24 Feb 2025 16:25:27 +0300 Subject: [PATCH 4/9] Java: cleanup code C#: Port ShortenMantissa function and unit tests form Java. --- csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs | 145 ++++++++++++ csharp/EPAM.Deltix.DFP/Decimal64.cs | 29 +++ csharp/EPAM.Deltix.DFP/DotNetImpl.cs | 219 ++++++++++++++++++ .../java/com/epam/deltix/dfp/JavaImpl.java | 218 ----------------- .../com/epam/deltix/dfp/FromDoubleTest.java | 2 +- 5 files changed, 394 insertions(+), 219 deletions(-) diff --git a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs index 4b3b5322..0c745fa6 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("9000000000000000"), + Decimal64.Parse("9999000000000000").ShortenMantissa(DotNetImpl.MaxCoefficient / 2, 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) + { + var mUp = (mantissa / 10) * 10; + uint 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) + { + var 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..4d6179df 100644 --- a/csharp/EPAM.Deltix.DFP/Decimal64.cs +++ b/csharp/EPAM.Deltix.DFP/Decimal64.cs @@ -809,6 +809,35 @@ public Decimal64 RoundToNearestTiesToEven(Decimal64 multiple) return new Decimal64(NativeImpl.multiply2(ratio, multiple.Bits)); } + /// + /// 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. + /// 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 + /// + /// + /// + /// + /// + 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/JavaImpl.java b/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java index 1f1b6fb0..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 @@ -3365,224 +3365,6 @@ static boolean isRoundedToReciprocalImpl(final int addExponent, return true; } - public static long shortenMantissa000(final long value, final long delta, final int minZerosCount) { - if (delta < 0) - throw new IllegalArgumentException("The delta value must be non-negative."); - if (minZerosCount < 0) - throw new IllegalArgumentException("The minZerosCount value must be non-negative."); - - 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))); - } - - 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 epsilonFloor10 = ei >= 0 ? POWERS_OF_TEN[ei] : POWERS_OF_TEN[~ei - 1]; - final long epsilonFloor10Up = epsilonFloor10 * 10; // Note: this is not the ceil: consider the case when epsilon = 10^n - - final long cUp = partsCoefficient + delta; - final long cDown = partsCoefficient - delta; - - if (epsilonFloor10Up < MAX_COEFFICIENT) { - final long cr = (cUp / epsilonFloor10Up) * epsilonFloor10Up; - if (cDown <= cr && cr <= cUp) { // Optimistic case - if (cr % POWERS_OF_TEN[minZerosCount] == 0) - return pack(partsSignMask, partsExponent, cr, BID_ROUNDING_TO_NEAREST); - else - return value; - } - } - - { - final long cr = (cUp / epsilonFloor10) * epsilonFloor10; - if (!(cDown <= cr && cr <= cUp)) { - throw new RuntimeException("@DEBUG: This could not be happen."); - } - - final long cCut = cUp / epsilonFloor10; - if (cCut % 10 == 0) - throw new RuntimeException("@DEBUG: How this can happens?"); - - return value; - } - } - - public static long shortenMantissa001(final long value, final long delta, final int minZerosCount) { - if (delta < 0) - throw new IllegalArgumentException("The delta value must be non-negative."); - if (minZerosCount < 0) - throw new IllegalArgumentException("The minZerosCount value must be non-negative."); - - 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))); - } - - 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 epsilonFloor10 = ei >= 0 ? POWERS_OF_TEN[ei] : POWERS_OF_TEN[~ei - 1]; - final long epsilonFloor10Up = epsilonFloor10 * 10; // Note: this is not the ceil: consider the case when epsilon = 10^n - - final long cUp = partsCoefficient + delta; - final long cDown = partsCoefficient - delta; - - if (epsilonFloor10Up < MAX_COEFFICIENT) { - long cr = (cUp / epsilonFloor10Up) * epsilonFloor10Up; - if (cDown <= cr && cr <= cUp) { // Optimistic case - // Than try to find the solution more close to input value - final long cr10 = ((partsCoefficient + epsilonFloor10Up / 2 - 1) / epsilonFloor10Up) * epsilonFloor10Up; - // If the number of zeros in cr10 is not less than the number of zeros in cr, - // then cr10 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 cr10 differs in (delta - (epsilonFloor10Up / 2 - 1)). - if (cr != cr10) { - final long epsilonFloor10UpUp = epsilonFloor10Up * 10; - final int crZerosCount = (cr % epsilonFloor10UpUp == 0 ? 1 : 0); //+ zerosCount(epsilonFloor10Up) -- ignore common part - final int cr10ZerosCount = (cr10 % epsilonFloor10UpUp == 0 ? 1 : 0); //+ zerosCount(epsilonFloor10Up) -- ignore common part - if (cr10ZerosCount >= crZerosCount) - cr = cr10; - } - - if (cr % POWERS_OF_TEN[minZerosCount] == 0) - return pack(partsSignMask, partsExponent, cr, BID_ROUNDING_TO_NEAREST); - else - return value; - } - - - } - - { - final long cr = ((partsCoefficient + epsilonFloor10 / 2 - 1) / epsilonFloor10) * epsilonFloor10; - if (!(cDown <= cr && cr <= cUp)) { - throw new RuntimeException("@DEBUG: This could not be happen."); - } - - if (cr % POWERS_OF_TEN[minZerosCount] == 0) - return pack(partsSignMask, partsExponent, cr, BID_ROUNDING_TO_NEAREST); - else - return value; - } - } - 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."); 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 1b37e34a..9307b5cd 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 @@ -309,7 +309,7 @@ public void testShortenMantissaCase() { checkShortenMantissaCase(1519453608576584L, 3207L); } - private void checkShortenMantissaCase(final long mantissa, final long delta) { + private static void checkShortenMantissaCase(final long mantissa, final long delta) { try { final long bestSolution = shortenMantissaDirect(mantissa, delta); From 49ba8041cc339caf361396c005727197411fc2ea Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Mon, 24 Feb 2025 17:02:33 +0300 Subject: [PATCH 5/9] Java, C#: Fix unit tests --- csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs | 4 ++-- .../dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs index 0c745fa6..fa7e6532 100644 --- a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs +++ b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs @@ -1272,8 +1272,8 @@ public void Issue91ToFloatString() [Test] public void TestShortenMantissaBigDelta() { - Assert.AreEqual(Decimal64.Parse("9000000000000000"), - Decimal64.Parse("9999000000000000").ShortenMantissa(DotNetImpl.MaxCoefficient / 2, 2)); + Assert.AreEqual(Decimal64.Parse("10000000000000000"), + Decimal64.Parse("9999000000000000").ShortenMantissa(DotNetImpl.MaxCoefficient / 10, 2)); } [Test] 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 9307b5cd..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 @@ -251,8 +251,8 @@ public void testShortenMantissaDenormalized() { @Test public void testShortenMantissaBigDelta() { - Assert.assertEquals(Decimal64.parse("9000000000000000"), - Decimal64.parse("9999000000000000").shortenMantissa(JavaImpl.MAX_COEFFICIENT / 2, 2)); + Assert.assertEquals(Decimal64.parse("10000000000000000"), + Decimal64.parse("9999000000000000").shortenMantissa(JavaImpl.MAX_COEFFICIENT / 10, 2)); } @Test From 53bb8147818f81a4c86d82d3a6f67b9374725061 Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Mon, 24 Feb 2025 18:43:59 +0300 Subject: [PATCH 6/9] C#: Fix unit test --- csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs index fa7e6532..986cfb17 100644 --- a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs +++ b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs @@ -1367,8 +1367,8 @@ private static ulong ShortenMantissaDirect(ulong mantissaIn, ulong deltaIn) long bestSolution = long.MinValue; if (rgDown > 0) { - var mUp = (mantissa / 10) * 10; - uint mFactor = 1; + long mUp = (mantissa / 10) * 10; + long mFactor = 1; long bestDifference = long.MaxValue; int bestPosition = -1; @@ -1379,7 +1379,7 @@ private static ulong ShortenMantissaDirect(ulong mantissaIn, ulong deltaIn) { for (uint d = 0; d < 10; ++d) { - var mTest = (mUp + d) * mFactor; + long mTest = (mUp + d) * mFactor; if (rgDown <= mTest && mTest <= rgUp) { var md = Math.Abs(mantissa - mTest); From 4ca9710ead663b9974809ac2ec99eb1686eff6ef Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Tue, 25 Feb 2025 19:30:36 +0300 Subject: [PATCH 7/9] Java, C#: Enhance shortenMantissa function description --- csharp/EPAM.Deltix.DFP/Decimal64.cs | 5 +++++ java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java | 5 +++++ .../src/main/java/com/epam/deltix/dfp/Decimal64Utils.java | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/csharp/EPAM.Deltix.DFP/Decimal64.cs b/csharp/EPAM.Deltix.DFP/Decimal64.cs index 4d6179df..4da5bf65 100644 --- a/csharp/EPAM.Deltix.DFP/Decimal64.cs +++ b/csharp/EPAM.Deltix.DFP/Decimal64.cs @@ -823,6 +823,11 @@ public Decimal64 RoundToNearestTiesToEven(Decimal64 multiple) /// 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 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 4519bf18..878ac149 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 @@ -729,6 +729,11 @@ public Decimal64 average(final Decimal64 other) { * 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 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 3022925c..e71094b5 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 @@ -864,6 +864,11 @@ public static long mean(@Decimal final long a, @Decimal final long b) { * 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 From fb3fe4904859abd129f339b5b5fa46f0932c3ade Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Wed, 26 Feb 2025 21:58:43 +0300 Subject: [PATCH 8/9] Java, C#: Enhance shortenMantissa function description. --- csharp/EPAM.Deltix.DFP/Decimal64.cs | 6 +++++- java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java | 6 +++++- .../src/main/java/com/epam/deltix/dfp/Decimal64Utils.java | 6 +++++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/csharp/EPAM.Deltix.DFP/Decimal64.cs b/csharp/EPAM.Deltix.DFP/Decimal64.cs index 4da5bf65..488b5ec5 100644 --- a/csharp/EPAM.Deltix.DFP/Decimal64.cs +++ b/csharp/EPAM.Deltix.DFP/Decimal64.cs @@ -832,7 +832,11 @@ public Decimal64 RoundToNearestTiesToEven(Decimal64 multiple) /// 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(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. /// /// /// 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 878ac149..3eafed7c 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 @@ -738,7 +738,11 @@ public Decimal64 average(final Decimal64 other) { * 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(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). 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 e71094b5..b6a96f7f 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 @@ -873,7 +873,11 @@ public static long mean(@Decimal final long a, @Decimal final long b) { * 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(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. From 83927dc2c9824dfb293eafd9e39e3420d928e993 Mon Sep 17 00:00:00 2001 From: Andrei Davydov Date: Fri, 28 Feb 2025 17:14:03 +0300 Subject: [PATCH 9/9] Java, C#: Add experimental notification to the shortenMantissa function --- csharp/EPAM.Deltix.DFP/Decimal64.cs | 1 + java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java | 1 + java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java | 1 + 3 files changed, 3 insertions(+) diff --git a/csharp/EPAM.Deltix.DFP/Decimal64.cs b/csharp/EPAM.Deltix.DFP/Decimal64.cs index 488b5ec5..ac89c2c8 100644 --- a/csharp/EPAM.Deltix.DFP/Decimal64.cs +++ b/csharp/EPAM.Deltix.DFP/Decimal64.cs @@ -810,6 +810,7 @@ public Decimal64 RoundToNearestTiesToEven(Decimal64 multiple) } /// + /// This function is experimental. /// Returns a DFP number in some neighborhood of the input value with a maximally /// reduced number of digits. /// Explanation: 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 3eafed7c..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 @@ -716,6 +716,7 @@ 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: 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 b6a96f7f..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 @@ -851,6 +851,7 @@ 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: