| /* ==================================================================== |
| Licensed to the Apache Software Foundation (ASF) under one or more |
| contributor license agreements. See the NOTICE file distributed with |
| this work for additional information regarding copyright ownership. |
| The ASF licenses this file to You under the Apache License, Version 2.0 |
| (the "License"); you may not use this file except in compliance with |
| the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| ==================================================================== */ |
| |
| package org.apache.poi.ss.util; |
| |
| import static org.junit.Assert.assertEquals; |
| |
| import java.math.BigDecimal; |
| import java.math.BigInteger; |
| |
| import org.apache.poi.util.HexDump; |
| import org.junit.Test; |
| |
| import junit.framework.AssertionFailedError; |
| /** |
| * Tests for {@link ExpandedDouble} |
| * |
| * @author Josh Micich |
| */ |
| public final class TestExpandedDouble { |
| private static final BigInteger BIG_POW_10 = BigInteger.valueOf(1000000000); |
| |
| @Test |
| public void testNegative() { |
| ExpandedDouble hd = new ExpandedDouble(0xC010000000000000L); |
| |
| if (hd.getBinaryExponent() == -2046) { |
| throw new AssertionFailedError("identified bug - sign bit not masked out of exponent"); |
| } |
| assertEquals(2, hd.getBinaryExponent()); |
| BigInteger frac = hd.getSignificand(); |
| assertEquals(64, frac.bitLength()); |
| assertEquals(1, frac.bitCount()); |
| } |
| |
| @Test |
| public void testSubnormal() { |
| ExpandedDouble hd = new ExpandedDouble(0x0000000000000001L); |
| |
| if (hd.getBinaryExponent() == -1023) { |
| throw new AssertionFailedError("identified bug - subnormal numbers not decoded properly"); |
| } |
| assertEquals(-1086, hd.getBinaryExponent()); |
| BigInteger frac = hd.getSignificand(); |
| assertEquals(64, frac.bitLength()); |
| assertEquals(1, frac.bitCount()); |
| } |
| |
| /** |
| * Tests specific values for conversion from {@link ExpandedDouble} to {@link NormalisedDecimal} and back |
| */ |
| @Test |
| public void testRoundTripShifting() { |
| long[] rawValues = { |
| 0x4010000000000004L, |
| 0x7010000000000004L, |
| 0x1010000000000004L, |
| 0x0010000000000001L, // near lowest normal number |
| 0x0010000000000000L, // lowest normal number |
| 0x000FFFFFFFFFFFFFL, // highest subnormal number |
| 0x0008000000000000L, // subnormal number |
| |
| 0xC010000000000004L, |
| 0xE230100010001004L, |
| 0x403CE0FFFFFFFFF2L, |
| 0x0000000000000001L, // smallest non-zero number (subnormal) |
| 0x6230100010000FFEL, |
| 0x6230100010000FFFL, |
| 0x6230100010001000L, |
| 0x403CE0FFFFFFFFF0L, // has single digit round trip error |
| 0x2B2BFFFF10001079L, |
| }; |
| boolean success = true; |
| for (int i = 0; i < rawValues.length; i++) { |
| success &= confirmRoundTrip(i, rawValues[i]); |
| } |
| if (!success) { |
| throw new AssertionFailedError("One or more test examples failed. See stderr."); |
| } |
| } |
| |
| public static boolean confirmRoundTrip(int i, long rawBitsA) { |
| double a = Double.longBitsToDouble(rawBitsA); |
| if (a == 0.0) { |
| // Can't represent 0.0 or -0.0 with NormalisedDecimal |
| return true; |
| } |
| ExpandedDouble ed1; |
| NormalisedDecimal nd2; |
| ExpandedDouble ed3; |
| try { |
| ed1 = new ExpandedDouble(rawBitsA); |
| nd2 = ed1.normaliseBaseTen(); |
| checkNormaliseBaseTenResult(ed1, nd2); |
| |
| ed3 = nd2.normaliseBaseTwo(); |
| } catch (RuntimeException e) { |
| System.err.println("example[" + i + "] (" |
| + formatDoubleAsHex(a) + ") exception:"); |
| e.printStackTrace(); |
| return false; |
| } |
| if (ed3.getBinaryExponent() != ed1.getBinaryExponent()) { |
| System.err.println("example[" + i + "] (" |
| + formatDoubleAsHex(a) + ") bin exp mismatch"); |
| return false; |
| } |
| BigInteger diff = ed3.getSignificand().subtract(ed1.getSignificand()).abs(); |
| if (diff.signum() == 0) { |
| return true; |
| } |
| // original quantity only has 53 bits of precision |
| // these quantities may have errors in the 64th bit, which hopefully don't make any difference |
| |
| if (diff.bitLength() < 2) { |
| // errors in the 64th bit happen from time to time |
| // this is well below the 53 bits of precision required |
| return true; |
| } |
| |
| // but bigger errors are a concern |
| System.out.println("example[" + i + "] (" |
| + formatDoubleAsHex(a) + ") frac mismatch: " + diff.toString()); |
| |
| for (int j=-2; j<3; j++) { |
| System.out.println((j<0?"":"+") + j + ": " + getNearby(ed1, j)); |
| } |
| for (int j=-2; j<3; j++) { |
| System.out.println((j<0?"":"+") + j + ": " + getNearby(nd2, j)); |
| } |
| |
| |
| return false; |
| } |
| |
| public static String getBaseDecimal(ExpandedDouble hd) { |
| int gg = 64 - hd.getBinaryExponent() - 1; |
| BigDecimal bd = new BigDecimal(hd.getSignificand()).divide(new BigDecimal(BigInteger.ONE.shiftLeft(gg))); |
| int excessPrecision = bd.precision() - 23; |
| if (excessPrecision > 0) { |
| bd = bd.setScale(bd.scale() - excessPrecision, BigDecimal.ROUND_HALF_UP); |
| } |
| return bd.unscaledValue().toString(); |
| } |
| |
| public static BigInteger getNearby(NormalisedDecimal md, int offset) { |
| BigInteger frac = md.composeFrac(); |
| int be = frac.bitLength() - 24 - 1; |
| int sc = frac.bitLength() - 64; |
| return getNearby(frac.shiftRight(sc), be, offset); |
| } |
| |
| public static BigInteger getNearby(ExpandedDouble hd, int offset) { |
| return getNearby(hd.getSignificand(), hd.getBinaryExponent(), offset); |
| } |
| |
| private static BigInteger getNearby(BigInteger significand, int binExp, int offset) { |
| int nExtraBits = 1; |
| int nDec = (int) Math.round(3.0 + (64+nExtraBits) * Math.log10(2.0)); |
| BigInteger newFrac = significand.shiftLeft(nExtraBits).add(BigInteger.valueOf(offset)); |
| |
| int gg = 64 + nExtraBits - binExp - 1; |
| |
| BigDecimal bd = new BigDecimal(newFrac); |
| if (gg > 0) { |
| bd = bd.divide(new BigDecimal(BigInteger.ONE.shiftLeft(gg))); |
| } else { |
| BigInteger frac = newFrac; |
| while (frac.bitLength() + binExp < 180) { |
| frac = frac.multiply(BigInteger.TEN); |
| } |
| int binaryExp = binExp - newFrac.bitLength() + frac.bitLength(); |
| |
| bd = new BigDecimal( frac.shiftRight(frac.bitLength()-binaryExp-1)); |
| } |
| int excessPrecision = bd.precision() - nDec; |
| if (excessPrecision > 0) { |
| bd = bd.setScale(bd.scale() - excessPrecision, BigDecimal.ROUND_HALF_UP); |
| } |
| return bd.unscaledValue(); |
| } |
| |
| private static void checkNormaliseBaseTenResult(ExpandedDouble orig, NormalisedDecimal result) { |
| String sigDigs = result.getSignificantDecimalDigits(); |
| BigInteger frac = orig.getSignificand(); |
| while (frac.bitLength() + orig.getBinaryExponent() < 200) { |
| frac = frac.multiply(BIG_POW_10); |
| } |
| int binaryExp = orig.getBinaryExponent() - orig.getSignificand().bitLength(); |
| |
| String origDigs = frac.shiftLeft(binaryExp+1).toString(10); |
| |
| if (!origDigs.startsWith(sigDigs)) { |
| throw new AssertionFailedError("Expected '" + origDigs + "' but got '" + sigDigs + "'."); |
| } |
| |
| double dO = Double.parseDouble("0." + origDigs.substring(sigDigs.length())); |
| double d1 = Double.parseDouble(result.getFractionalPart().toPlainString()); |
| BigInteger subDigsO = BigInteger.valueOf((int) (dO * 32768 + 0.5)); |
| BigInteger subDigsB = BigInteger.valueOf((int) (d1 * 32768 + 0.5)); |
| |
| if (subDigsO.equals(subDigsB)) { |
| return; |
| } |
| BigInteger diff = subDigsB.subtract(subDigsO).abs(); |
| if (diff.intValue() > 100) { |
| // 100/32768 ~= 0.003 |
| throw new AssertionFailedError("minor mistake"); |
| } |
| } |
| |
| private static String formatDoubleAsHex(double d) { |
| long l = Double.doubleToLongBits(d); |
| return HexDump.longToHex(l)+'L'; |
| } |
| } |