]> source.dussan.org Git - poi.git/commitdiff
Bugzilla 47598 - Improved formula evaluator number comparison
authorJosh Micich <josh@apache.org>
Wed, 29 Jul 2009 03:36:25 +0000 (03:36 +0000)
committerJosh Micich <josh@apache.org>
Wed, 29 Jul 2009 03:36:25 +0000 (03:36 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@798771 13f79535-47bb-0310-9956-ffa450edef68

15 files changed:
src/documentation/content/xdocs/status.xml
src/java/org/apache/poi/hssf/record/formula/eval/RelationalOperationEval.java
src/java/org/apache/poi/ss/util/ExpandedDouble.java [new file with mode: 0644]
src/java/org/apache/poi/ss/util/IEEEDouble.java [new file with mode: 0644]
src/java/org/apache/poi/ss/util/MutableFPNumber.java [new file with mode: 0644]
src/java/org/apache/poi/ss/util/NormalisedDecimal.java [new file with mode: 0644]
src/java/org/apache/poi/ss/util/NumberComparer.java [new file with mode: 0644]
src/java/org/apache/poi/ss/util/NumberToTextConverter.java
src/testcases/org/apache/poi/hssf/record/formula/eval/TestEqualEval.java
src/testcases/org/apache/poi/ss/util/AllSSUtilTests.java
src/testcases/org/apache/poi/ss/util/NumberComparingSpreadsheetGenerator.java [new file with mode: 0644]
src/testcases/org/apache/poi/ss/util/NumberComparisonExamples.java [new file with mode: 0644]
src/testcases/org/apache/poi/ss/util/NumberToTextConversionExamples.java
src/testcases/org/apache/poi/ss/util/TestExpandedDouble.java [new file with mode: 0644]
src/testcases/org/apache/poi/ss/util/TestNumberComparer.java [new file with mode: 0644]

index 91b9cdede07c5705a7aafd495564469bc155d6bb..a3ca8ccbfc4449cac2640ea4662b4087a27c1337 100644 (file)
@@ -33,6 +33,7 @@
 
     <changes>
         <release version="3.5-beta7" date="2009-??-??">
+           <action dev="POI-DEVELOPERS" type="fix">47598 - Improved formula evaluator number comparison</action>
            <action dev="POI-DEVELOPERS" type="fix">47571 - Fixed XWPFWordExtractor to extract inserted/deleted text</action>
            <action dev="POI-DEVELOPERS" type="fix">47548 - Fixed RecordFactoryInputStream to properly read continued DrawingRecords</action>
            <action dev="POI-DEVELOPERS" type="fix">46419 - Fixed compatibility issue with OpenOffice 3.0</action>
index c678507aab8a947d4f3d73cdeb06efc8ea584244..f54b7cd2bc8de235df21b3fd9ada18cbe290489d 100644 (file)
@@ -17,6 +17,8 @@
 
 package org.apache.poi.hssf.record.formula.eval;
 
+import org.apache.poi.ss.util.NumberComparer;
+
 /**
  * Base class for all comparison operator evaluators
  *
@@ -108,8 +110,7 @@ public abstract class RelationalOperationEval implements OperationEval {
                        if (vb instanceof NumberEval) {
                                NumberEval nA = (NumberEval) va;
                                NumberEval nB = (NumberEval) vb;
-                               // Excel considers -0.0 < 0.0 which is the same as Double.compare()
-                               return Double.compare(nA.getNumberValue(), nB.getNumberValue());
+                               return NumberComparer.compare(nA.getNumberValue(), nB.getNumberValue());
                        }
                }
                throw new IllegalArgumentException("Bad operand types (" + va.getClass().getName() + "), ("
@@ -126,7 +127,7 @@ public abstract class RelationalOperationEval implements OperationEval {
                }
                if (v instanceof NumberEval) {
                        NumberEval ne = (NumberEval) v;
-                       return Double.compare(0, ne.getNumberValue());
+                       return NumberComparer.compare(0.0, ne.getNumberValue());
                }
                if (v instanceof StringEval) {
                        StringEval se = (StringEval) v;
diff --git a/src/java/org/apache/poi/ss/util/ExpandedDouble.java b/src/java/org/apache/poi/ss/util/ExpandedDouble.java
new file mode 100644 (file)
index 0000000..41827df
--- /dev/null
@@ -0,0 +1,98 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.ss.util;\r
+\r
+import java.math.BigInteger;\r
+import static org.apache.poi.ss.util.IEEEDouble.*;\r
+\r
+/**\r
+ * Represents a 64 bit IEEE double quantity expressed with both decimal and binary exponents\r
+ * Does not handle negative numbers or zero\r
+ * <p/>\r
+ * The value of a {@link ExpandedDouble} is given by<br/>\r
+ * <tt> a &times; 2<sup>b</sup></tt>\r
+ * <br/>\r
+ * where:<br/>\r
+ *\r
+ * <tt>a</tt> = <i>significand</i><br/>\r
+ * <tt>b</tt> = <i>binaryExponent</i> - bitLength(significand) + 1<br/>\r
+ *\r
+ * @author Josh Micich\r
+ */\r
+final class ExpandedDouble {\r
+       private static final BigInteger BI_FRAC_MASK = BigInteger.valueOf(FRAC_MASK);\r
+       private static final BigInteger BI_IMPLIED_FRAC_MSB = BigInteger.valueOf(FRAC_ASSUMED_HIGH_BIT);\r
+\r
+       private static BigInteger getFrac(long rawBits) {\r
+               return BigInteger.valueOf(rawBits).and(BI_FRAC_MASK).or(BI_IMPLIED_FRAC_MSB).shiftLeft(11);\r
+       }\r
+\r
+\r
+       public static ExpandedDouble fromRawBitsAndExponent(long rawBits, int exp) {\r
+               return new ExpandedDouble(getFrac(rawBits), exp);\r
+       }\r
+\r
+       /**\r
+        * Always 64 bits long (MSB, bit-63 is '1')\r
+        */\r
+       private final BigInteger _significand;\r
+       private final int _binaryExponent;\r
+\r
+       public ExpandedDouble(long rawBits) {\r
+               int biasedExp = (int) (rawBits >> 52);\r
+               if (biasedExp == 0) {\r
+                       // sub-normal numbers\r
+                       BigInteger frac = BigInteger.valueOf(rawBits).and(BI_FRAC_MASK);\r
+                       int expAdj = 64 - frac.bitLength();\r
+                       _significand = frac.shiftLeft(expAdj);\r
+                       _binaryExponent = (biasedExp & 0x07FF) - 1023 - expAdj;\r
+               } else {\r
+                       BigInteger frac = getFrac(rawBits);\r
+                       _significand = frac;\r
+                       _binaryExponent = (biasedExp & 0x07FF) - 1023;\r
+               }\r
+       }\r
+\r
+       ExpandedDouble(BigInteger frac, int binaryExp) {\r
+               if (frac.bitLength() != 64) {\r
+                       throw new IllegalArgumentException("bad bit length");\r
+               }\r
+               _significand = frac;\r
+               _binaryExponent = binaryExp;\r
+       }\r
+\r
+\r
+       /**\r
+        * Convert to an equivalent {@link NormalisedDecimal} representation having 15 decimal digits of precision in the\r
+        * non-fractional bits of the significand.\r
+        */\r
+       public NormalisedDecimal normaliseBaseTen() {\r
+               return NormalisedDecimal.create(_significand, _binaryExponent);\r
+       }\r
+\r
+       /**\r
+        * @return the number of non-fractional bits after the MSB of the significand\r
+        */\r
+       public int getBinaryExponent() {\r
+               return _binaryExponent;\r
+       }\r
+\r
+       public BigInteger getSignificand() {\r
+               return _significand;\r
+       }\r
+}\r
diff --git a/src/java/org/apache/poi/ss/util/IEEEDouble.java b/src/java/org/apache/poi/ss/util/IEEEDouble.java
new file mode 100644 (file)
index 0000000..f5a42ed
--- /dev/null
@@ -0,0 +1,44 @@
+/* ====================================================================
+   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;
+
+
+/**
+ * For working with the internals of IEEE 754-2008 'binary64' (double precision) floating point numbers
+ *
+ * @author Josh Micich
+ */
+final class IEEEDouble {
+       private static final long EXPONENT_MASK = 0x7FF0000000000000L;
+       private static final int  EXPONENT_SHIFT = 52;
+       public static final long FRAC_MASK = 0x000FFFFFFFFFFFFFL;
+       public static final int  EXPONENT_BIAS  = 1023;
+       public static final long FRAC_ASSUMED_HIGH_BIT = ( 1L<<EXPONENT_SHIFT );
+       /**
+        * The value the exponent field gets for all <i>NaN</i> and <i>Infinity</i> values
+        */
+       public static final int BIASED_EXPONENT_SPECIAL_VALUE = 0x07FF;
+
+       /**
+        * @param rawBits the 64 bit binary representation of the double value
+        * @return the top 12 bits (sign and biased exponent value)
+        */
+       public static int getBiasedExponent(long rawBits) {
+               return (int) ((rawBits & EXPONENT_MASK) >> EXPONENT_SHIFT);
+       }
+}
diff --git a/src/java/org/apache/poi/ss/util/MutableFPNumber.java b/src/java/org/apache/poi/ss/util/MutableFPNumber.java
new file mode 100644 (file)
index 0000000..2ae93e6
--- /dev/null
@@ -0,0 +1,209 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.ss.util;\r
+\r
+import java.math.BigInteger;\r
+\r
+final class MutableFPNumber {\r
+\r
+\r
+       // TODO - what about values between (10<sup>14</sup>-0.5) and (10<sup>14</sup>-0.05) ?\r
+       /**\r
+        * The minimum value in 'Base-10 normalised form'.<br/>\r
+        * When {@link #_binaryExponent} == 46 this is the the minimum {@link #_frac} value\r
+        *  (10<sup>14</sup>-0.05) * 2^17\r
+        *  <br/>\r
+        *  Values between (10<sup>14</sup>-0.05) and 10<sup>14</sup> will be represented as '1'\r
+        *  followed by 14 zeros.\r
+        *  Values less than (10<sup>14</sup>-0.05) will get shifted by one more power of 10\r
+        *\r
+        *  This frac value rounds to '1' followed by fourteen zeros with an incremented decimal exponent\r
+        */\r
+       private static final BigInteger BI_MIN_BASE = new BigInteger("0B5E620F47FFFE666", 16);\r
+       /**\r
+        * For 'Base-10 normalised form'<br/>\r
+        * The maximum {@link #_frac} value when {@link #_binaryExponent} == 49\r
+        * (10^15-0.5) * 2^14\r
+        */\r
+       private static final BigInteger BI_MAX_BASE = new BigInteger("0E35FA9319FFFE000", 16);\r
+\r
+       /**\r
+        * Width of a long\r
+        */\r
+       private static final int C_64 = 64;\r
+\r
+       /**\r
+        * Minimum precision after discarding whole 32-bit words from the significand\r
+        */\r
+       private static final int MIN_PRECISION = 72;\r
+       private BigInteger _significand;\r
+       private int _binaryExponent;\r
+       public MutableFPNumber(BigInteger frac, int binaryExponent) {\r
+               _significand = frac;\r
+               _binaryExponent = binaryExponent;\r
+       }\r
+\r
+\r
+       public MutableFPNumber copy() {\r
+               return new MutableFPNumber(_significand, _binaryExponent);\r
+       }\r
+       public void normalise64bit() {\r
+               int oldBitLen = _significand.bitLength();\r
+               int sc = oldBitLen - C_64;\r
+               if (sc == 0) {\r
+                       return;\r
+               }\r
+               if (sc < 0) {\r
+                       throw new IllegalStateException("Not enough precision");\r
+               }\r
+               _binaryExponent += sc;\r
+               if (sc > 32) {\r
+                       int highShift = (sc-1) & 0xFFFFE0;\r
+                       _significand = _significand.shiftRight(highShift);\r
+                       sc -= highShift;\r
+                       oldBitLen -= highShift;\r
+               }\r
+               if (sc < 1) {\r
+                       throw new IllegalStateException();\r
+               }\r
+               _significand = Rounder.round(_significand, sc);\r
+               if (_significand.bitLength() > oldBitLen) {\r
+                       sc++;\r
+                       _binaryExponent++;\r
+               }\r
+               _significand = _significand.shiftRight(sc);\r
+       }\r
+       public int get64BitNormalisedExponent() {\r
+               return _binaryExponent + _significand.bitLength() - C_64;\r
+\r
+       }\r
+\r
+       @Override\r
+       public boolean equals(Object obj) {\r
+               MutableFPNumber other = (MutableFPNumber) obj;\r
+               if (_binaryExponent != other._binaryExponent) {\r
+                       return false;\r
+               }\r
+               return _significand.equals(other._significand);\r
+       }\r
+       public boolean isBelowMaxRep() {\r
+               int sc = _significand.bitLength() - C_64;\r
+               return _significand.compareTo(BI_MAX_BASE.shiftLeft(sc)) < 0;\r
+       }\r
+       public boolean isAboveMinRep() {\r
+               int sc = _significand.bitLength() - C_64;\r
+               return _significand.compareTo(BI_MIN_BASE.shiftLeft(sc)) > 0;\r
+       }\r
+       public NormalisedDecimal createNormalisedDecimal(int pow10) {\r
+               // missingUnderBits is (0..3)\r
+               int missingUnderBits = _binaryExponent-39;\r
+               int fracPart = (_significand.intValue() << missingUnderBits) & 0xFFFF80;\r
+               long wholePart = _significand.shiftRight(C_64-_binaryExponent-1).longValue();\r
+               return new NormalisedDecimal(wholePart, fracPart, pow10);\r
+       }\r
+       public void multiplyByPowerOfTen(int pow10) {\r
+               TenPower tp = TenPower.getInstance(Math.abs(pow10));\r
+               if (pow10 < 0) {\r
+                       mulShift(tp._divisor, tp._divisorShift);\r
+               } else {\r
+                       mulShift(tp._multiplicand, tp._multiplierShift);\r
+               }\r
+       }\r
+       private void mulShift(BigInteger multiplicand, int multiplierShift) {\r
+               _significand = _significand.multiply(multiplicand);\r
+               _binaryExponent += multiplierShift;\r
+               // check for too much precision\r
+               int sc = (_significand.bitLength() - MIN_PRECISION) & 0xFFFFFFE0;\r
+               // mask makes multiples of 32 which optimises BigInteger.shiftRight\r
+               if (sc > 0) {\r
+                       // no need to round because we have at least 8 bits of extra precision\r
+                       _significand = _significand.shiftRight(sc);\r
+                       _binaryExponent += sc;\r
+               }\r
+       }\r
+\r
+       private static final class Rounder {\r
+               private static final BigInteger[] HALF_BITS;\r
+\r
+               static {\r
+                       BigInteger[] bis = new BigInteger[33];\r
+                       long acc=1;\r
+                       for (int i = 1; i < bis.length; i++) {\r
+                               bis[i] = BigInteger.valueOf(acc);\r
+                               acc <<=1;\r
+                       }\r
+                       HALF_BITS = bis;\r
+               }\r
+               /**\r
+                * @param nBits number of bits to shift right\r
+                */\r
+               public static BigInteger round(BigInteger bi, int nBits) {\r
+                       if (nBits < 1) {\r
+                               return bi;\r
+                       }\r
+                       return bi.add(HALF_BITS[nBits]);\r
+               }\r
+       }\r
+\r
+       /**\r
+        * Holds values for quick multiplication and division by 10\r
+        */\r
+       private static final class TenPower {\r
+               private static final BigInteger FIVE = new BigInteger("5");\r
+               private static final TenPower[] _cache = new TenPower[350];\r
+\r
+               public final BigInteger _multiplicand;\r
+               public final BigInteger _divisor;\r
+               public final int _divisorShift;\r
+               public final int _multiplierShift;\r
+\r
+               private TenPower(int index) {\r
+                       BigInteger fivePowIndex = FIVE.pow(index);\r
+\r
+                       int bitsDueToFiveFactors = fivePowIndex.bitLength();\r
+                       int px = 80 + bitsDueToFiveFactors;\r
+                       BigInteger fx = BigInteger.ONE.shiftLeft(px).divide(fivePowIndex);\r
+                       int adj = fx.bitLength() - 80;\r
+                       _divisor = fx.shiftRight(adj);\r
+                       bitsDueToFiveFactors -= adj;\r
+\r
+                       _divisorShift = -(bitsDueToFiveFactors+index+80);\r
+                       int sc = fivePowIndex.bitLength() - 68;\r
+                       if (sc > 0) {\r
+                               _multiplierShift = index + sc;\r
+                               _multiplicand = fivePowIndex.shiftRight(sc);\r
+                       } else {\r
+                               _multiplierShift = index;\r
+                               _multiplicand = fivePowIndex;\r
+                       }\r
+               }\r
+\r
+               static TenPower getInstance(int index) {\r
+                       TenPower result = _cache[index];\r
+                       if (result == null) {\r
+                               result = new TenPower(index);\r
+                               _cache[index] = result;\r
+                       }\r
+                       return result;\r
+               }\r
+       }\r
+\r
+       public ExpandedDouble createExpandedDouble() {\r
+               return new ExpandedDouble(_significand, _binaryExponent);\r
+       }\r
+}\r
diff --git a/src/java/org/apache/poi/ss/util/NormalisedDecimal.java b/src/java/org/apache/poi/ss/util/NormalisedDecimal.java
new file mode 100644 (file)
index 0000000..84f4d72
--- /dev/null
@@ -0,0 +1,271 @@
+/* ====================================================================\r
+   Licensed to the Apache Software Foundation (ASF) under one or more\r
+   contributor license agreements.  See the NOTICE file distributed with\r
+   this work for additional information regarding copyright ownership.\r
+   The ASF licenses this file to You under the Apache License, Version 2.0\r
+   (the "License"); you may not use this file except in compliance with\r
+   the License.  You may obtain a copy of the License at\r
+\r
+       http://www.apache.org/licenses/LICENSE-2.0\r
+\r
+   Unless required by applicable law or agreed to in writing, software\r
+   distributed under the License is distributed on an "AS IS" BASIS,\r
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+   See the License for the specific language governing permissions and\r
+   limitations under the License.\r
+==================================================================== */\r
+\r
+package org.apache.poi.ss.util;\r
+\r
+import java.math.BigDecimal;\r
+import java.math.BigInteger;\r
+\r
+/**\r
+ * Represents a transformation of a 64 bit IEEE double quantity having a decimal exponent and a\r
+ * fixed point (15 decimal digit) significand.  Some quirks of Excel's calculation behaviour are\r
+ * simpler to reproduce with numeric quantities in this format.  This class is currently used to\r
+ * help:\r
+ * <ol>\r
+ * <li>Comparison operations</li>\r
+ * <li>Conversions to text</li>\r
+ * </ol>\r
+ *\r
+ * <p/>\r
+ * This class does not handle negative numbers or zero.\r
+ * <p/>\r
+ * The value of a {@link NormalisedDecimal} is given by<br/>\r
+ * <tt> significand &times; 10<sup>decimalExponent</sup></tt>\r
+ * <br/>\r
+ * where:<br/>\r
+ *\r
+ * <tt>significand</tt> = wholePart + fractionalPart / 2<sup>24</sup><br/>\r
+ *\r
+ * @author Josh Micich\r
+ */\r
+final class NormalisedDecimal {\r
+       /**\r
+        * Number of powers of ten contained in the significand\r
+        */\r
+       private static final int EXPONENT_OFFSET = 14;\r
+\r
+       private static final BigDecimal BD_2_POW_24 = new BigDecimal(BigInteger.ONE.shiftLeft(24));\r
+\r
+       /**\r
+        * log<sub>10</sub>(2)&times;2<sup>20</sup>\r
+        */\r
+       private static final int LOG_BASE_10_OF_2_TIMES_2_POW_20 = 315653; // 315652.8287\r
+\r
+       /**\r
+        * 2<sup>19</sup>\r
+        */\r
+       private static final int C_2_POW_19 = 1 << 19;\r
+\r
+\r
+       /**\r
+        * the value of {@link #_fractionalPart} that represents 0.5\r
+        */\r
+       private static final int FRAC_HALF = 0x800000;\r
+\r
+       /**\r
+        * 10<sup>15</sup>\r
+        */\r
+       private static final long MAX_REP_WHOLE_PART = 0x38D7EA4C68000L;\r
+\r
+\r
+\r
+       public static NormalisedDecimal create(BigInteger frac, int binaryExponent) {\r
+               // estimate pow2&pow10 first, perform optional mulShift, then normalize\r
+               int pow10;\r
+               if (binaryExponent > 49 || binaryExponent < 46) {\r
+\r
+                       // working with ints (left shifted 20) instead of doubles\r
+                       // x = 14.5 - binaryExponent * log10(2);\r
+                       int x = (29 << 19) - binaryExponent * LOG_BASE_10_OF_2_TIMES_2_POW_20;\r
+                       x += C_2_POW_19; // round\r
+                       pow10 = -(x >> 20);\r
+               } else {\r
+                       pow10 = 0;\r
+               }\r
+               MutableFPNumber cc = new MutableFPNumber(frac, binaryExponent);\r
+               if (pow10 != 0) {\r
+                       cc.multiplyByPowerOfTen(-pow10);\r
+               }\r
+\r
+               switch (cc.get64BitNormalisedExponent()) {\r
+                       case 46:\r
+                               if (cc.isAboveMinRep()) {\r
+                                       break;\r
+                               }\r
+                       case 44:\r
+                       case 45:\r
+                               cc.multiplyByPowerOfTen(1);\r
+                               pow10--;\r
+                               break;\r
+                       case 47:\r
+                       case 48:\r
+                               break;\r
+                       case 49:\r
+                               if (cc.isBelowMaxRep()) {\r
+                                       break;\r
+                               }\r
+                       case 50:\r
+                               cc.multiplyByPowerOfTen(-1);\r
+                               pow10++;\r
+                               break;\r
+\r
+                       default:\r
+                               throw new IllegalStateException("Bad binary exp " + cc.get64BitNormalisedExponent() + ".");\r
+               }\r
+               cc.normalise64bit();\r
+\r
+               return cc.createNormalisedDecimal(pow10);\r
+       }\r
+\r
+       /**\r
+        * Rounds at the digit with value 10<sup>decimalExponent</sup>\r
+        */\r
+       public NormalisedDecimal roundUnits() {\r
+               long wholePart = _wholePart;\r
+               if (_fractionalPart >= FRAC_HALF) {\r
+                       wholePart++;\r
+               }\r
+\r
+               int de = _relativeDecimalExponent;\r
+\r
+               if (wholePart < MAX_REP_WHOLE_PART) {\r
+                       return new NormalisedDecimal(wholePart, 0, de);\r
+               }\r
+               return new NormalisedDecimal(wholePart/10, 0, de+1);\r
+       }\r
+\r
+       /**\r
+        * The decimal exponent increased by one less than the digit count of {@link #_wholePart}\r
+        */\r
+       private final int _relativeDecimalExponent;\r
+       /**\r
+        * The whole part of the significand (typically 15 digits).\r
+        *\r
+        * 47-50 bits long (MSB may be anywhere from bit 46 to 49)\r
+        * LSB is units bit.\r
+        */\r
+       private final long _wholePart;\r
+       /**\r
+        * The fractional part of the significand.\r
+        * 24 bits (only top 14-17 bits significant): a value between 0x000000 and 0xFFFF80\r
+        */\r
+       private final int _fractionalPart;\r
+\r
+\r
+       NormalisedDecimal(long wholePart, int fracPart, int decimalExponent) {\r
+               _wholePart = wholePart;\r
+               _fractionalPart = fracPart;\r
+               _relativeDecimalExponent = decimalExponent;\r
+       }\r
+\r
+\r
+       /**\r
+        * Convert to an equivalent {@link ExpandedDouble} representation (binary frac and exponent).\r
+        * The resulting transformed object is easily converted to a 64 bit IEEE double:\r
+        * <ul>\r
+        * <li>bits 2-53 of the {@link #getSignificand()} become the 52 bit 'fraction'.</li>\r
+        * <li>{@link #getBinaryExponent()} is biased by 1023 to give the 'exponent'.</li>\r
+        * </ul>\r
+        * The sign bit must be obtained from somewhere else.\r
+        * @return a new {@link NormalisedDecimal} normalised to base 2 representation.\r
+        */\r
+       public ExpandedDouble normaliseBaseTwo() {\r
+               MutableFPNumber cc = new MutableFPNumber(composeFrac(), 39);\r
+               cc.multiplyByPowerOfTen(_relativeDecimalExponent);\r
+               cc.normalise64bit();\r
+               return cc.createExpandedDouble();\r
+       }\r
+\r
+       /**\r
+        * @return the significand as a fixed point number (with 24 fraction bits and 47-50 whole bits)\r
+        */\r
+       BigInteger composeFrac() {\r
+               long wp = _wholePart;\r
+               int fp = _fractionalPart;\r
+               return new BigInteger(new byte[] {\r
+                               (byte) (wp >> 56), // N.B. assuming sign bit is zero\r
+                               (byte) (wp >> 48),\r
+                               (byte) (wp >> 40),\r
+                               (byte) (wp >> 32),\r
+                               (byte) (wp >> 24),\r
+                               (byte) (wp >> 16),\r
+                               (byte) (wp >>  8),\r
+                               (byte) (wp >>  0),\r
+                               (byte) (fp >> 16),\r
+                               (byte) (fp >> 8),\r
+                               (byte) (fp >> 0),\r
+               });\r
+       }\r
+\r
+       public String getSignificantDecimalDigits() {\r
+               return Long.toString(_wholePart);\r
+       }\r
+       /**\r
+        * Rounds the first whole digit position (considers only units digit, not frational part).\r
+        * Caller should check total digit count of result to see whether the rounding operation caused\r
+        * a carry out of the most significant digit\r
+        */\r
+       public String getSignificantDecimalDigitsLastDigitRounded() {\r
+               long wp = _wholePart + 5; // rounds last digit\r
+               StringBuilder sb = new StringBuilder(24);\r
+               sb.append(wp);\r
+               sb.setCharAt(sb.length()-1, '0');\r
+               return sb.toString();\r
+       }\r
+\r
+       /**\r
+        * @return the number of powers of 10 which have been extracted from the significand and binary exponent.\r
+        */\r
+       public int getDecimalExponent() {\r
+               return _relativeDecimalExponent+EXPONENT_OFFSET;\r
+       }\r
+\r
+       /**\r
+        * assumes both this and other are normalised\r
+        */\r
+       public int compareNormalised(NormalisedDecimal other) {\r
+               int cmp = _relativeDecimalExponent - other._relativeDecimalExponent;\r
+               if (cmp != 0) {\r
+                       return cmp;\r
+               }\r
+               if (_wholePart > other._wholePart) {\r
+                       return 1;\r
+               }\r
+               if (_wholePart < other._wholePart) {\r
+                       return -1;\r
+               }\r
+               return _fractionalPart - other._fractionalPart;\r
+       }\r
+       public BigDecimal getFractionalPart() {\r
+               return new BigDecimal(_fractionalPart).divide(BD_2_POW_24);\r
+       }\r
+\r
+       private String getFractionalDigits() {\r
+               if (_fractionalPart == 0) {\r
+                       return "0";\r
+               }\r
+               return getFractionalPart().toString().substring(2);\r
+       }\r
+\r
+       @Override\r
+       public String toString() {\r
+\r
+               StringBuilder sb = new StringBuilder();\r
+               sb.append(getClass().getName());\r
+               sb.append(" [");\r
+               String ws = String.valueOf(_wholePart);\r
+               sb.append(ws.charAt(0));\r
+               sb.append('.');\r
+               sb.append(ws.substring(1));\r
+               sb.append(' ');\r
+               sb.append(getFractionalDigits());\r
+               sb.append("E");\r
+               sb.append(getDecimalExponent());\r
+               sb.append("]");\r
+               return sb.toString();\r
+       }\r
+}\r
diff --git a/src/java/org/apache/poi/ss/util/NumberComparer.java b/src/java/org/apache/poi/ss/util/NumberComparer.java
new file mode 100644 (file)
index 0000000..49a27d2
--- /dev/null
@@ -0,0 +1,173 @@
+/* ====================================================================
+   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.apache.poi.ss.util.IEEEDouble.*;
+
+/**
+ * Excel compares numbers using different rules to those of java, so
+ *  {@link Double#compare(double, double)} won't do.
+ *
+ *
+ * @author Josh Micich
+ */
+public final class NumberComparer {
+
+       /**
+        * This class attempts to reproduce Excel's behaviour for comparing numbers.  Results are
+        * mostly the same as those from {@link Double#compare(double, double)} but with some
+        * rounding.  For numbers that are very close, this code converts to a format having 15
+        * decimal digits of precision and a decimal exponent, before completing the comparison.
+        * <p/>
+        * In Excel formula evaluation, expressions like "(0.06-0.01)=0.05" evaluate to "TRUE" even
+        * though the equivalent java expression is <code>false</code>.  In examples like this,
+        * Excel achieves the effect by having additional logic for comparison operations.
+        * <p/>
+        * <p/>
+        * Note - Excel also gives special treatment to expressions like "0.06-0.01-0.05" which
+        * evaluates to "0" (in java, rounding anomalies give a result of 6.9E-18).  The special
+        * behaviour here is for different reasons to the example above:  If the last operator in a
+        * cell formula is '+' or '-' and the result is less than 2<sup>50</sup> times smaller than
+        * first operand, the result is rounded to zero.
+        * Needless to say, the two rules are not consistent and it is relatively easy to find
+        * examples that satisfy<br/>
+        * "A=B" is "TRUE" but "A-B" is not "0"<br/>
+        * and<br/>
+        * "A=B" is "FALSE" but "A-B" is "0"<br/>
+        * <br/>
+        * This rule (for rounding the result of a final addition or subtraction), has not been
+        * implemented in POI (as of Jul-2009).
+        *
+        * @return <code>negative, 0, or positive</code> according to the standard Excel comparison
+        * of values <tt>a</tt> and <tt>b</tt>.
+        */
+       public static int compare(double a, double b) {
+               long rawBitsA = Double.doubleToLongBits(a);
+               long rawBitsB = Double.doubleToLongBits(b);
+
+               int biasedExponentA = getBiasedExponent(rawBitsA);
+               int biasedExponentB = getBiasedExponent(rawBitsB);
+
+               if (biasedExponentA == BIASED_EXPONENT_SPECIAL_VALUE) {
+                       throw new IllegalArgumentException("Special double values are not allowed: " + toHex(a));
+               }
+               if (biasedExponentB == BIASED_EXPONENT_SPECIAL_VALUE) {
+                       throw new IllegalArgumentException("Special double values are not allowed: " + toHex(a));
+               }
+
+               int cmp;
+
+               // sign bit is in the same place for long and double:
+               boolean aIsNegative = rawBitsA < 0;
+               boolean bIsNegative = rawBitsB < 0;
+
+               // compare signs
+               if (aIsNegative != bIsNegative) {
+                       // Excel seems to have 'normal' comparison behaviour around zero (no rounding)
+                       // even -0.0 < +0.0 (which is not quite the initial conclusion of bug 47198)
+                       return aIsNegative ? -1 : +1;
+               }
+
+               // then compare magnitudes (IEEE 754 has exponent bias specifically to allow this)
+               cmp = biasedExponentA - biasedExponentB;
+               int absExpDiff = Math.abs(cmp);
+               if (absExpDiff > 1) {
+                       return aIsNegative ? -cmp : cmp;
+               }
+
+               if (absExpDiff == 1) {
+                       // special case exponent differs by 1.  There is still a chance that with rounding the two quantities could end up the same
+
+               } else {
+                       // else - sign and exponents equal
+                       if (rawBitsA == rawBitsB) {
+                               // fully equal - exit here
+                               return 0;
+                       }
+               }
+               if (biasedExponentA == 0) {
+                       if (biasedExponentB == 0) {
+                               return compareSubnormalNumbers(rawBitsA & FRAC_MASK, rawBitsB & FRAC_MASK, aIsNegative);
+                       }
+                       // else biasedExponentB is 1
+                       return -compareAcrossSubnormalThreshold(rawBitsB, rawBitsA, aIsNegative);
+               }
+               if (biasedExponentB == 0) {
+                       // else biasedExponentA is 1
+                       return +compareAcrossSubnormalThreshold(rawBitsA, rawBitsB, aIsNegative);
+               }
+
+               // sign and exponents same, but fractional bits are different
+
+               ExpandedDouble edA = ExpandedDouble.fromRawBitsAndExponent(rawBitsA, biasedExponentA - EXPONENT_BIAS);
+               ExpandedDouble edB = ExpandedDouble.fromRawBitsAndExponent(rawBitsB, biasedExponentB - EXPONENT_BIAS);
+               NormalisedDecimal ndA = edA.normaliseBaseTen().roundUnits();
+               NormalisedDecimal ndB = edB.normaliseBaseTen().roundUnits();
+               cmp = ndA.compareNormalised(ndB);
+               if (aIsNegative) {
+                       return -cmp;
+               }
+               return cmp;
+       }
+
+       /**
+        * If both numbers are subnormal, Excel seems to use standard comparison rules
+        */
+       private static int compareSubnormalNumbers(long fracA, long fracB, boolean isNegative) {
+               int cmp = fracA > fracB ? +1 : fracA < fracB ? -1 : 0;
+
+               return isNegative ? -cmp : cmp;
+       }
+
+
+
+       /**
+        * Usually any normal number is greater (in magnitude) than any subnormal number.
+        * However there are some anomalous cases around the threshold where Excel produces screwy results
+        * @param isNegative both values are either negative or positive. This parameter affects the sign of the comparison result
+        * @return usually <code>isNegative ? -1 : +1</code>
+        */
+       private static int compareAcrossSubnormalThreshold(long normalRawBitsA, long subnormalRawBitsB, boolean isNegative) {
+               long fracB = subnormalRawBitsB & FRAC_MASK;
+               if (fracB == 0) {
+                       // B is zero, so A is definitely greater than B
+                       return isNegative ? -1 : +1;
+               }
+               long fracA = normalRawBitsA & FRAC_MASK;
+               if (fracA <= 0x0000000000000007L && fracB >= 0x000FFFFFFFFFFFFAL) {
+                       // Both A and B close to threshold - weird results
+                       if (fracA == 0x0000000000000007L && fracB == 0x000FFFFFFFFFFFFAL) {
+                               // special case
+                               return 0;
+                       }
+                       // exactly the opposite
+                       return isNegative ? +1 : -1;
+               }
+               // else - typical case A and B is not close to threshold
+               return isNegative ? -1 : +1;
+       }
+
+
+
+       /**
+        * for formatting double values in error messages
+        */
+       private static String toHex(double a) {
+               return "0x" + Long.toHexString(Double.doubleToLongBits(a)).toUpperCase();
+       }
+}
index efcb012fcc0fa1a590de8d8183f192038222e8fb..c5b8d936b783942b76ed374fcd1d90451528688e 100644 (file)
@@ -17,8 +17,6 @@
 
 package org.apache.poi.ss.util;
 
-import java.math.BigDecimal;
-import java.math.BigInteger;
 
 /**
  * Excel converts numbers to text with different rules to those of java, so
@@ -113,21 +111,9 @@ import java.math.BigInteger;
  */
 public final class NumberToTextConverter {
 
-       private static final long expMask  = 0x7FF0000000000000L;
-       private static final long FRAC_MASK= 0x000FFFFFFFFFFFFFL;
-       private static final int  EXPONENT_SHIFT = 52;
-       private static final int  FRAC_BITS_WIDTH = EXPONENT_SHIFT;
-       private static final int  EXPONENT_BIAS  = 1023;
-       private static final long FRAC_ASSUMED_HIGH_BIT = ( 1L<<EXPONENT_SHIFT );
-
        private static final long EXCEL_NAN_BITS = 0xFFFF0420003C0000L;
        private static final int MAX_TEXT_LEN = 20;
 
-       private static final int DEFAULT_COUNT_SIGNIFICANT_DIGITS = 15;
-       private static final int MAX_EXTRA_ZEROS = MAX_TEXT_LEN - DEFAULT_COUNT_SIGNIFICANT_DIGITS;
-       private static final float LOG2_10 = 3.32F;
-
-
        private NumberToTextConverter() {
                // no instances of this class
        }
@@ -149,186 +135,110 @@ public final class NumberToTextConverter {
                if (isNegative) {
                        rawBits &= 0x7FFFFFFFFFFFFFFFL;
                }
-
-               int biasedExponent = (int) ((rawBits & expMask) >> EXPONENT_SHIFT);
-               if (biasedExponent == 0) {
+               if (rawBits == 0) {
+                       return isNegative ? "-0" : "0";
+               }
+               ExpandedDouble ed = new ExpandedDouble(rawBits);
+               if (ed.getBinaryExponent() < -1022) {
                        // value is 'denormalised' which means it is less than 2^-1022
                        // excel displays all these numbers as zero, even though calculations work OK
                        return isNegative ? "-0" : "0";
                }
-
-               int exponent = biasedExponent - EXPONENT_BIAS;
-
-               long fracBits = FRAC_ASSUMED_HIGH_BIT | rawBits & FRAC_MASK;
-
-
-               // Start by converting double value to BigDecimal
-               BigDecimal bd;
-               if (biasedExponent == 0x07FF) {
+               if (ed.getBinaryExponent() == 1024) {
                        // Special number NaN /Infinity
+                       // Normally one would not create HybridDecimal objects from these values
+                       // except in these cases Excel really tries to render them as if they were normal numbers
                        if(rawBits == EXCEL_NAN_BITS) {
                                return "3.484840871308E+308";
                        }
                        // This is where excel really gets it wrong
-                       // Special numbers like Infinity and Nan are interpreted according to
+                       // Special numbers like Infinity and NaN are interpreted according to
                        // the standard rules below.
                        isNegative = false; // except that the sign bit is ignored
                }
-               bd = convertToBigDecimal(exponent, fracBits);
-
-               return formatBigInteger(isNegative, bd.unscaledValue(), bd.scale());
-       }
-
-       private static BigDecimal convertToBigDecimal(int exponent, long fracBits) {
-               byte[] joob = {
-                               (byte) (fracBits >> 48),
-                               (byte) (fracBits >> 40),
-                               (byte) (fracBits >> 32),
-                               (byte) (fracBits >> 24),
-                               (byte) (fracBits >> 16),
-                               (byte) (fracBits >>  8),
-                               (byte) (fracBits >>  0),
-               };
-
-               BigInteger bigInt = new BigInteger(joob);
-               int lastSigBitIndex = exponent-FRAC_BITS_WIDTH;
-               if(lastSigBitIndex < 0) {
-                       BigInteger shifto = new BigInteger("1").shiftLeft(-lastSigBitIndex);
-                       int scale = 1 -(int) (lastSigBitIndex/LOG2_10);
-                       BigDecimal bd1 = new BigDecimal(bigInt);
-                       BigDecimal bdShifto = new BigDecimal(shifto);
-                       return bd1.divide(bdShifto, scale, BigDecimal.ROUND_HALF_UP);
+               NormalisedDecimal nd = ed.normaliseBaseTen();
+               StringBuilder sb = new StringBuilder(MAX_TEXT_LEN+1);
+               if (isNegative) {
+                       sb.append('-');
                }
-               BigInteger sl = bigInt.shiftLeft(lastSigBitIndex);
-               return new BigDecimal(sl);
+               convertToText(sb, nd);
+               return sb.toString();
        }
-
-       private static String formatBigInteger(boolean isNegative, BigInteger unscaledValue, int scale) {
-
-               if (scale < 0) {
-                       throw new RuntimeException("negative scale");
-               }
-
-               StringBuffer sb = new StringBuffer(unscaledValue.toString());
-               int numberOfLeadingZeros = -1;
-
-               int unscaledLength = sb.length();
-               if (scale > 0 && scale >= unscaledLength) {
-                       // less than one
-                       numberOfLeadingZeros = scale-unscaledLength;
-                       formatLessThanOne(sb, numberOfLeadingZeros+1);
+       private static void convertToText(StringBuilder sb, NormalisedDecimal pnd) {
+               NormalisedDecimal rnd = pnd.roundUnits();
+               int decExponent = rnd.getDecimalExponent();
+               String decimalDigits;
+               if (Math.abs(decExponent)>98) {
+                       decimalDigits = rnd.getSignificantDecimalDigitsLastDigitRounded();
+                       if (decimalDigits.length() == 16) {
+                               // rounding caused carry
+                               decExponent++;
+                       }
                } else {
-                       int decimalPointIndex = unscaledLength - scale;
-                       formatGreaterThanOne(sb, decimalPointIndex);
-               }
-               if(isNegative) {
-                       sb.insert(0, '-');
+                       decimalDigits = rnd.getSignificantDecimalDigits();
                }
-               return sb.toString();
-       }
-
-       private static int getNumberOfSignificantFiguresDisplayed(int exponent) {
-               int nLostDigits; // number of significand digits lost due big exponents
-               if(exponent > 99) {
-                       // any exponent greater than 99 has 3 digits instead of 2
-                       nLostDigits = 1;
-               } else if (exponent < -98) {
-                       // For some weird reason on the negative side
-                       // step is occurs from -98 to -99 (not from -99 to -100)
-                       nLostDigits = 1;
+               int countSigDigits = countSignifantDigits(decimalDigits);
+               if (decExponent < 0) {
+                       formatLessThanOne(sb, decimalDigits, decExponent, countSigDigits);
                } else {
-                       nLostDigits = 0;
+                       formatGreaterThanOne(sb, decimalDigits, decExponent, countSigDigits);
                }
-               return DEFAULT_COUNT_SIGNIFICANT_DIGITS - nLostDigits;
-       }
-
-       private static boolean needsScientificNotation(int nDigits) {
-               return nDigits > MAX_TEXT_LEN;
        }
 
-       private static void formatGreaterThanOne(StringBuffer sb, int nIntegerDigits) {
+       private static void formatLessThanOne(StringBuilder sb, String decimalDigits, int decExponent,
+                       int countSigDigits) {
+               int nLeadingZeros = -decExponent - 1;
+               int normalLength = 2 + nLeadingZeros + countSigDigits; // 2 == "0.".length()
 
-               int maxSigFigs = getNumberOfSignificantFiguresDisplayed(nIntegerDigits);
-               int decimalPointIndex = nIntegerDigits;
-               boolean roundCausedCarry = performRound(sb, 0, maxSigFigs);
-
-               int endIx = Math.min(maxSigFigs, sb.length()-1);
-
-               int nSigFigures;
-               if(roundCausedCarry) {
-                       sb.insert(0, '1');
-                       decimalPointIndex++;
-                       nSigFigures = 1;
-               } else {
-                       nSigFigures = countSignifantDigits(sb, endIx);
-               }
-
-               if(needsScientificNotation(decimalPointIndex)) {
-                       sb.setLength(nSigFigures);
-                       if (nSigFigures > 1) {
-                               sb.insert(1, '.');
+               if (needsScientificNotation(normalLength)) {
+                       sb.append(decimalDigits.charAt(0));
+                       if (countSigDigits > 1) {
+                       sb.append('.');
+                       sb.append(decimalDigits.subSequence(1, countSigDigits));
                        }
-                       sb.append("E+");
-                       appendExp(sb, decimalPointIndex-1);
+                       sb.append("E-");
+                       appendExp(sb, -decExponent);
                        return;
                }
-               if(isAllZeros(sb, decimalPointIndex, maxSigFigs)) {
-                       sb.setLength(decimalPointIndex);
-                       return;
+               sb.append("0.");
+               for (int i=nLeadingZeros; i>0; i--) {
+                       sb.append('0');
                }
-               // else some sig-digits after the decimal point
-               sb.setLength(nSigFigures);
-               sb.insert(decimalPointIndex, '.');
+               sb.append(decimalDigits.subSequence(0, countSigDigits));
        }
 
-       /**
-        * @param sb initially contains just the significant digits
-        * @param pAbsExponent to be inserted (after "0.") at the start of the number
-        */
-       private static void formatLessThanOne(StringBuffer sb, int pAbsExponent) {
-               if (sb.charAt(0) == 0) {
-                       throw new IllegalArgumentException("First digit of significand should be non-zero");
+       private static void formatGreaterThanOne(StringBuilder sb, String decimalDigits, int decExponent, int countSigDigits) {
+
+               if (decExponent > 19) {
+                       // scientific notation
+                       sb.append(decimalDigits.charAt(0));
+                       if (countSigDigits>1) {
+                               sb.append('.');
+                               sb.append(decimalDigits.subSequence(1, countSigDigits));
+                       }
+                       sb.append("E+");
+                       appendExp(sb, decExponent);
+                       return;
                }
-               if (pAbsExponent < 1) {
-                       throw new IllegalArgumentException("abs(exponent) must be positive");
+               int nFractionalDigits = countSigDigits - decExponent-1;
+               if (nFractionalDigits > 0) {
+                       sb.append(decimalDigits.subSequence(0, decExponent+1));
+                       sb.append('.');
+                       sb.append(decimalDigits.subSequence(decExponent+1, countSigDigits));
+                       return;
                }
-
-               int numberOfLeadingZeros = pAbsExponent-1;
-               int absExponent = pAbsExponent;
-               int maxSigFigs = getNumberOfSignificantFiguresDisplayed(-absExponent);
-
-               boolean roundCausedCarry = performRound(sb, 0, maxSigFigs);
-               int nRemainingSigFigs;
-               if(roundCausedCarry) {
-                       absExponent--;
-                       numberOfLeadingZeros--;
-                       nRemainingSigFigs = 1;
-                       sb.setLength(0);
-                       sb.append("1");
-               } else {
-                       nRemainingSigFigs = countSignifantDigits(sb, 0 + maxSigFigs);
-                       sb.setLength(nRemainingSigFigs);
+               sb.append(decimalDigits.subSequence(0, countSigDigits));
+               for (int i=-nFractionalDigits; i>0; i--) {
+                       sb.append('0');
                }
+       }
 
-               int normalLength = 2 + numberOfLeadingZeros + nRemainingSigFigs; // 2 == "0.".length()
-
-               if (needsScientificNotation(normalLength)) {
-                       if (sb.length()>1) {
-                               sb.insert(1, '.');
-                       }
-                       sb.append('E');
-                       sb.append('-');
-                       appendExp(sb, absExponent);
-               } else {
-                       sb.insert(0, "0.");
-                       for(int i=numberOfLeadingZeros; i>0; i--) {
-                               sb.insert(2, '0');
-                       }
-               }
+       private static boolean needsScientificNotation(int nDigits) {
+               return nDigits > MAX_TEXT_LEN;
        }
 
-       private static int countSignifantDigits(StringBuffer sb, int startIx) {
-               int result=startIx;
+       private static int countSignifantDigits(String sb) {
+               int result=sb.length()-1;
                while(sb.charAt(result) == '0') {
                        result--;
                        if(result < 0) {
@@ -338,68 +248,12 @@ public final class NumberToTextConverter {
                return result + 1;
        }
 
-       private static void appendExp(StringBuffer sb, int val) {
+       private static void appendExp(StringBuilder sb, int val) {
                if(val < 10) {
                        sb.append('0');
                        sb.append((char)('0' + val));
                        return;
                }
                sb.append(val);
-
-       }
-
-
-       private static boolean isAllZeros(StringBuffer sb, int startIx, int endIx) {
-               for(int i=startIx; i<=endIx && i<sb.length(); i++) {
-                       if(sb.charAt(i) != '0') {
-                               return false;
-                       }
-               }
-               return true;
-       }
-
-       /**
-        * @return <code>true</code> if carry (out of the MS digit) occurred
-        */
-       private static boolean performRound(StringBuffer sb, int firstSigFigIx, int nSigFigs) {
-               int nextDigitIx = firstSigFigIx + nSigFigs;
-               if(nextDigitIx == sb.length()) {
-                       return false; // nothing to do - digit to be rounded is at the end of the buffer
-               }
-               if(nextDigitIx > sb.length()) {
-                       throw new RuntimeException("Buffer too small to fit all significant digits");
-               }
-               boolean hadCarryOutOfFirstDigit;
-               if(sb.charAt(nextDigitIx) < '5') {
-                       // change to digit
-                       hadCarryOutOfFirstDigit = false;
-               } else {
-                       hadCarryOutOfFirstDigit = roundAndCarry(sb, nextDigitIx);
-               }
-               // clear out the rest of the digits after the rounded digit
-               // (at least the nearby digits)
-               int endIx = Math.min(nextDigitIx + MAX_EXTRA_ZEROS, sb.length());
-               for(int i = nextDigitIx; i<endIx; i++) {
-                       sb.setCharAt(i, '0');
-               }
-               return hadCarryOutOfFirstDigit;
-       }
-
-       private static boolean roundAndCarry(StringBuffer sb, int nextDigitIx) {
-
-               int changeDigitIx = nextDigitIx - 1;
-               while(sb.charAt(changeDigitIx) == '9') {
-                       sb.setCharAt(changeDigitIx, '0');
-                       changeDigitIx--;
-                       // All nines, rounded up.  Notify caller
-                       if(changeDigitIx < 0) {
-                               return true;
-                       }
-               }
-               // no more '9's to round up.
-               // Last digit to be changed is still inside sb
-               char prevDigit = sb.charAt(changeDigitIx);
-               sb.setCharAt(changeDigitIx, (char) (prevDigit + 1));
-               return false;
        }
 }
index 19d1fa1fbcaa035d68ecd81ebc52c24a43efedc9..3e71107b7697525fa1105f9dc7086136273ce7e0 100644 (file)
@@ -94,10 +94,10 @@ public final class TestEqualEval extends TestCase {
 
        /**
         * Bug 47198 involved a formula "-A1=0" where cell A1 was 0.0.
-        * Excel evaluates "-A1=0" to TRUE, not because it thinks -0.0==0.0 
+        * Excel evaluates "-A1=0" to TRUE, not because it thinks -0.0==0.0
         * but because "-A1" evaluated to +0.0
         * <p/>
-        * Note - the original diagnosis of bug 47198 was that 
+        * Note - the original diagnosis of bug 47198 was that
         * "Excel considers -0.0 to be equal to 0.0" which is NQR
         * See {@link TestMinusZeroResult} for more specific tests regarding -0.0.
         */
@@ -114,4 +114,19 @@ public final class TestEqualEval extends TestCase {
                        throw new AssertionFailedError("Identified bug 47198: -0.0 != 0.0");
                }
        }
+
+       public void testRounding_bug47598() {
+               double x = 1+1.0028-0.9973; // should be 1.0055, but has IEEE rounding
+               assertFalse(x == 1.0055);
+
+               NumberEval a = new NumberEval(x);
+               NumberEval b = new NumberEval(1.0055);
+               assertEquals("1.0055", b.getStringValue());
+
+               Eval[] args = { a, b, };
+               BoolEval result = (BoolEval) EqualEval.instance.evaluate(args, 0, (short) 0);
+               if (!result.getBooleanValue()) {
+                       throw new AssertionFailedError("Identified bug 47598: 1+1.0028-0.9973 != 1.0055");
+               }
+       }
 }
index 9da4c2ca3639400db0059c5facd2376426606f4a..49bdfadfa58e5b3b26545905fef58b1feadd442c 100644 (file)
@@ -21,13 +21,15 @@ import junit.framework.Test;
 import junit.framework.TestSuite;
 /**
  * Test suite for <tt>org.apache.poi.ss.util</tt>
- * 
+ *
  * @author Josh Micich
  */
 public final class AllSSUtilTests {
-    public static Test suite() {
+       public static Test suite() {
                TestSuite result = new TestSuite(AllSSUtilTests.class.getName());
                result.addTestSuite(TestCellReference.class);
+               result.addTestSuite(TestExpandedDouble.class);
+               result.addTestSuite(TestNumberComparer.class);
                result.addTestSuite(TestNumberToTextConverter.class);
                result.addTestSuite(TestRegion.class);
                return result;
diff --git a/src/testcases/org/apache/poi/ss/util/NumberComparingSpreadsheetGenerator.java b/src/testcases/org/apache/poi/ss/util/NumberComparingSpreadsheetGenerator.java
new file mode 100644 (file)
index 0000000..d04bf40
--- /dev/null
@@ -0,0 +1,155 @@
+/* ====================================================================
+   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 java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import org.apache.poi.hssf.usermodel.HSSFCell;
+import org.apache.poi.hssf.usermodel.HSSFCellStyle;
+import org.apache.poi.hssf.usermodel.HSSFFont;
+import org.apache.poi.hssf.usermodel.HSSFRichTextString;
+import org.apache.poi.hssf.usermodel.HSSFRow;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.util.NumberComparisonExamples.ComparisonExample;
+import org.apache.poi.util.HexDump;
+
+/**
+ * Creates a spreadsheet that checks Excel's comparison of various IEEE double values.
+ * The class {@link NumberComparisonExamples} contains specific comparison examples
+ * (along with their expected results) that get encoded into rows of the spreadsheet.
+ * Each example is checked with a formula (in column I) that displays either "OK" or
+ * "ERROR" depending on whether actual results match those expected.
+ *
+ * @author Josh Micich
+ */
+public class NumberComparingSpreadsheetGenerator {
+
+       private static final class SheetWriter {
+
+               private final HSSFSheet _sheet;
+               private int _rowIndex;
+
+               public SheetWriter(HSSFWorkbook wb) {
+                       HSSFSheet sheet = wb.createSheet("Sheet1");
+
+                       writeHeaderRow(wb, sheet);
+                       _sheet = sheet;
+                       _rowIndex = 1;
+               }
+
+               public void addTestRow(double a, double b, int expResult) {
+                       writeDataRow(_sheet, _rowIndex++, a, b, expResult);
+               }
+       }
+
+       private static void writeHeaderCell(HSSFRow row, int i, String text, HSSFCellStyle style) {
+               HSSFCell cell = row.createCell(i);
+               cell.setCellValue(new HSSFRichTextString(text));
+               cell.setCellStyle(style);
+       }
+       static void writeHeaderRow(HSSFWorkbook wb, HSSFSheet sheet) {
+               sheet.setColumnWidth(0, 6000);
+               sheet.setColumnWidth(1, 6000);
+               sheet.setColumnWidth(2, 3600);
+               sheet.setColumnWidth(3, 3600);
+               sheet.setColumnWidth(4, 2400);
+               sheet.setColumnWidth(5, 2400);
+               sheet.setColumnWidth(6, 2400);
+               sheet.setColumnWidth(7, 2400);
+               sheet.setColumnWidth(8, 2400);
+               HSSFRow row = sheet.createRow(0);
+               HSSFCellStyle style = wb.createCellStyle();
+               HSSFFont font = wb.createFont();
+               font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
+               style.setFont(font);
+               writeHeaderCell(row, 0, "Raw Long Bits A", style);
+               writeHeaderCell(row, 1, "Raw Long Bits B", style);
+               writeHeaderCell(row, 2, "Value A", style);
+               writeHeaderCell(row, 3, "Value B", style);
+               writeHeaderCell(row, 4, "Exp Cmp", style);
+               writeHeaderCell(row, 5, "LT", style);
+               writeHeaderCell(row, 6, "EQ", style);
+               writeHeaderCell(row, 7, "GT", style);
+               writeHeaderCell(row, 8, "Check", style);
+       }
+       /**
+        * Fills a spreadsheet row with one comparison example. The two numeric values are written to
+        * columns C and D. Columns (F, G and H) respectively get formulas ("v0<v1", "v0=v1", "v0>v1"),
+        * which will be evaluated by Excel. Column D gets the expected comparison result. Column I
+        * gets a formula to check that Excel's comparison results match that predicted in column D.
+        *
+        * @param v0 the first value to be compared
+        * @param v1 the second value to be compared
+        * @param expRes expected comparison result (-1, 0, or +1)
+        */
+       static void writeDataRow(HSSFSheet sheet, int rowIx, double v0, double v1, int expRes) {
+               HSSFRow row = sheet.createRow(rowIx);
+
+               int rowNum = rowIx + 1;
+
+
+               row.createCell(0).setCellValue(formatDoubleAsHex(v0));
+               row.createCell(1).setCellValue(formatDoubleAsHex(v1));
+               row.createCell(2).setCellValue(v0);
+               row.createCell(3).setCellValue(v1);
+               row.createCell(4).setCellValue(expRes < 0 ? "LT" : expRes > 0 ? "GT" : "EQ");
+               row.createCell(5).setCellFormula("C" + rowNum + "<" + "D" + rowNum);
+               row.createCell(6).setCellFormula("C" + rowNum + "=" + "D" + rowNum);
+               row.createCell(7).setCellFormula("C" + rowNum + ">" + "D" + rowNum);
+               // TODO - bug elsewhere in POI - something wrong with encoding of NOT() function
+               String frm = "if(or(" +
+                       "and(E#='LT', F#      , G#=FALSE, H#=FALSE)," +
+                       "and(E#='EQ', F#=FALSE, G#      , H#=FALSE)," +
+                       "and(E#='GT', F#=FALSE, G#=FALSE, H#      )" +
+                       "), 'OK', 'error')"     ;
+               row.createCell(8).setCellFormula(frm.replaceAll("#", String.valueOf(rowNum)).replace('\'', '"'));
+       }
+
+       private static String formatDoubleAsHex(double d) {
+               long l = Double.doubleToLongBits(d);
+               StringBuilder sb = new StringBuilder(20);
+               sb.append(HexDump.longToHex(l)).append('L');
+               return sb.toString();
+       }
+
+       public static void main(String[] args) {
+
+               HSSFWorkbook wb = new HSSFWorkbook();
+               SheetWriter sw = new SheetWriter(wb);
+               ComparisonExample[] ces = NumberComparisonExamples.getComparisonExamples();
+               for (int i = 0; i < ces.length; i++) {
+                       ComparisonExample ce = ces[i];
+                       sw.addTestRow(ce.getA(), ce.getB(), ce.getExpectedResult());
+               }
+
+
+               File outputFile = new File("ExcelNumberCompare.xls");
+
+               try {
+                       FileOutputStream os = new FileOutputStream(outputFile);
+                       wb.write(os);
+                       os.close();
+               } catch (IOException e) {
+                       throw new RuntimeException(e);
+               }
+               System.out.println("Finished writing '" + outputFile.getAbsolutePath() + "'");
+       }
+}
diff --git a/src/testcases/org/apache/poi/ss/util/NumberComparisonExamples.java b/src/testcases/org/apache/poi/ss/util/NumberComparisonExamples.java
new file mode 100644 (file)
index 0000000..265e40d
--- /dev/null
@@ -0,0 +1,182 @@
+/* ====================================================================
+   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 java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Contains specific examples of <tt>double</tt> value pairs and their comparison result according to Excel.
+ *
+ * @author Josh Micich
+ */
+final class NumberComparisonExamples {
+
+       private NumberComparisonExamples() {
+               // no instances of this class
+       }
+
+       /**
+        * represents one comparison test case
+        */
+       public static final class ComparisonExample {
+               private final long _rawBitsA;
+               private final long _rawBitsB;
+               private final int _expectedResult;
+
+               public ComparisonExample(long rawBitsA, long rawBitsB, int expectedResult) {
+                       _rawBitsA = rawBitsA;
+                       _rawBitsB = rawBitsB;
+                       _expectedResult = expectedResult;
+               }
+
+               public double getA() {
+                       return Double.longBitsToDouble(_rawBitsA);
+               }
+               public double getB() {
+                       return Double.longBitsToDouble(_rawBitsB);
+               }
+               public double getNegA() {
+                       return -Double.longBitsToDouble(_rawBitsA);
+               }
+               public double getNegB() {
+                       return -Double.longBitsToDouble(_rawBitsB);
+               }
+               public int getExpectedResult() {
+                       return _expectedResult;
+               }
+       }
+
+       private static final ComparisonExample[] examples = initExamples();
+
+       private static ComparisonExample[] initExamples() {
+
+               List<ComparisonExample> temp = new ArrayList<ComparisonExample>();
+
+               addStepTransition(temp, 0x4010000000000005L);
+               addStepTransition(temp, 0x4010000000000010L);
+               addStepTransition(temp, 0x401000000000001CL);
+
+               addStepTransition(temp, 0x403CE0FFFFFFFFF1L);
+
+               addStepTransition(temp, 0x5010000000000006L);
+               addStepTransition(temp, 0x5010000000000010L);
+               addStepTransition(temp, 0x501000000000001AL);
+
+               addStepTransition(temp, 0x544CE6345CF32018L);
+               addStepTransition(temp, 0x544CE6345CF3205AL);
+               addStepTransition(temp, 0x544CE6345CF3209CL);
+               addStepTransition(temp, 0x544CE6345CF320DEL);
+
+               addStepTransition(temp, 0x54B250001000101DL);
+               addStepTransition(temp, 0x54B2500010001050L);
+               addStepTransition(temp, 0x54B2500010001083L);
+
+               addStepTransition(temp, 0x6230100010001000L);
+               addStepTransition(temp, 0x6230100010001005L);
+               addStepTransition(temp, 0x623010001000100AL);
+
+               addStepTransition(temp, 0x7F50300020001011L);
+               addStepTransition(temp, 0x7F5030002000102BL);
+               addStepTransition(temp, 0x7F50300020001044L);
+
+
+               addStepTransition(temp, 0x2B2BFFFF1000102AL);
+               addStepTransition(temp, 0x2B2BFFFF10001079L);
+               addStepTransition(temp, 0x2B2BFFFF100010C8L);
+
+               addStepTransition(temp, 0x2B2BFF001000102DL);
+               addStepTransition(temp, 0x2B2BFF0010001035L);
+               addStepTransition(temp, 0x2B2BFF001000103DL);
+
+               addStepTransition(temp, 0x2B61800040002024L);
+               addStepTransition(temp, 0x2B61800040002055L);
+               addStepTransition(temp, 0x2B61800040002086L);
+
+
+               addStepTransition(temp, 0x008000000000000BL);
+               // just outside 'subnormal' range
+               addStepTransition(temp, 0x0010000000000007L);
+               addStepTransition(temp, 0x001000000000001BL);
+               addStepTransition(temp, 0x001000000000002FL);
+
+               for(ComparisonExample ce : new ComparisonExample[] {
+                               // negative, and exponents differ by more than 1
+                               ce(0xBF30000000000000L, 0xBE60000000000000L, -1),
+
+                               // negative zero *is* less than positive zero, but not easy to get out of calculations
+                               ce(0x0000000000000000L, 0x8000000000000000L, +1),
+                               // subnormal numbers compare without rounding for some reason
+                               ce(0x0000000000000000L, 0x0000000000000001L, -1),
+                               ce(0x0008000000000000L, 0x0008000000000001L, -1),
+                               ce(0x000FFFFFFFFFFFFFL, 0x000FFFFFFFFFFFFEL, +1),
+                               ce(0x000FFFFFFFFFFFFBL, 0x000FFFFFFFFFFFFCL, -1),
+                               ce(0x000FFFFFFFFFFFFBL, 0x000FFFFFFFFFFFFEL, -1),
+
+                               // across subnormal threshold (some mistakes when close)
+                               ce(0x000FFFFFFFFFFFFFL, 0x0010000000000000L, +1),
+                               ce(0x000FFFFFFFFFFFFBL, 0x0010000000000007L, +1),
+                               ce(0x000FFFFFFFFFFFFAL, 0x0010000000000007L, 0),
+
+                               // when a bit further apart - normal results
+                               ce(0x000FFFFFFFFFFFF9L, 0x0010000000000007L, -1),
+                               ce(0x000FFFFFFFFFFFFAL, 0x0010000000000008L, -1),
+                               ce(0x000FFFFFFFFFFFFBL, 0x0010000000000008L, -1),
+               }) {
+                       temp.add(ce);
+               }
+
+               ComparisonExample[] result = new ComparisonExample[temp.size()];
+               temp.toArray(result);
+               return result;
+       }
+
+       private static ComparisonExample ce(long rawBitsA, long rawBitsB, int expectedResult) {
+               return new ComparisonExample(rawBitsA, rawBitsB, expectedResult);
+       }
+
+       private static void addStepTransition(List<ComparisonExample> temp, long rawBits) {
+               for(ComparisonExample ce : new ComparisonExample[] {
+                               ce(rawBits-1, rawBits+0, 0),
+                               ce(rawBits+0, rawBits+1, -1),
+                               ce(rawBits+1, rawBits+2, 0),
+               }) {
+                       temp.add(ce);
+               }
+
+       }
+
+       public static ComparisonExample[] getComparisonExamples() {
+               return examples.clone();
+       }
+
+       public static ComparisonExample[] getComparisonExamples2() {
+               ComparisonExample[] result = examples.clone();
+
+               for (int i = 0; i < result.length; i++) {
+                       int ha = ("a"+i).hashCode();
+                       double a = ha * Math.pow(0.75, ha % 100);
+                       int hb = ("b"+i).hashCode();
+                       double b = hb * Math.pow(0.75, hb % 100);
+
+                       result[i] = new ComparisonExample(Double.doubleToLongBits(a), Double.doubleToLongBits(b), Double.compare(a, b));
+               }
+
+               return result;
+       }
+}
index fb80010c5a106d8c4473aeb844a3e86c390e12d8..91f9c4142956beb06f5eb04fbffc420acb9bec70 100644 (file)
@@ -95,17 +95,17 @@ final class NumberToTextConversionExamples {
                ec(0x4087A00000000000L, "756.0", "756"),
                ec(0x401E3D70A3D70A3DL, "7.56", "7.56"),
 
-//             ec(0x405EDD3C07FB4C8CL, "123.4567890123455",  "123.456789012345"),
+               ec(0x405EDD3C07FB4C8CL, "123.4567890123455",  "123.456789012345"),
                ec(0x405EDD3C07FB4C99L, "123.45678901234568", "123.456789012346"),
                ec(0x405EDD3C07FB4CAEL, "123.45678901234598", "123.456789012346"),
                ec(0x4132D687E3DF2180L, "1234567.8901234567", "1234567.89012346"),
 
-//             ec(0x3F543A272D9E0E49L, "0.001234567890123455",  "0.00123456789012345"),
+               ec(0x3F543A272D9E0E49L, "0.001234567890123455",  "0.00123456789012345"),
                ec(0x3F543A272D9E0E4AL, "0.0012345678901234552", "0.00123456789012346"),
                ec(0x3F543A272D9E0E55L, "0.0012345678901234576", "0.00123456789012346"),
                ec(0x3F543A272D9E0E72L, "0.0012345678901234639", "0.00123456789012346"),
                ec(0x3F543A272D9E0E76L, "0.0012345678901234647", "0.00123456789012346"),
-//             ec(0x3F543A272D9E0E77L, "0.001234567890123465",  "0.00123456789012346"),
+               ec(0x3F543A272D9E0E77L, "0.001234567890123465",  "0.00123456789012346"),
 
                ec(0x3F543A272D9E0E78L, "0.0012345678901234652", "0.00123456789012347"),
 
@@ -121,11 +121,11 @@ final class NumberToTextConversionExamples {
                ec(0x544CE6345CF32121L, "1.2345678901234751E98",  "1.23456789012348E+98"),
 
 
-//             ec(0x54820FE0BA17F5E9L, "1.23456789012355E99",    "1.2345678901236E+99"),
+               ec(0x54820FE0BA17F5E9L, "1.23456789012355E99",    "1.2345678901236E+99"),
                ec(0x54820FE0BA17F5EAL, "1.2345678901235502E99",  "1.2345678901236E+99"),
-//             ec(0x54820FE0BA17F784L, "1.2345678901236498E99",  "1.2345678901237E+99"),
+               ec(0x54820FE0BA17F784L, "1.2345678901236498E99",  "1.2345678901237E+99"),
                ec(0x54820FE0BA17F785L, "1.23456789012365E99",    "1.2345678901237E+99"),
-//             ec(0x54820FE0BA17F920L, "1.2345678901237498E99",  "1.2345678901238E+99"),
+               ec(0x54820FE0BA17F920L, "1.2345678901237498E99",  "1.2345678901238E+99"),
                ec(0x54820FE0BA17F921L, "1.23456789012375E99",    "1.2345678901238E+99"),
 
 
@@ -137,52 +137,52 @@ final class NumberToTextConversionExamples {
                ec(0x547D42AEA2879F2AL,"9.999999999999995E98",  "9.99999999999999E+98"),
                ec(0x547D42AEA2879F2BL,"9.999999999999996E98",  "1E+99"),
                ec(0x547D42AEA287A0A0L,"1.0000000000000449E99", "1E+99"),
-//             ec(0x547D42AEA287A0A1L,"1.000000000000045E99",  "1.0000000000001E+99"),
+               ec(0x547D42AEA287A0A1L,"1.000000000000045E99",  "1.0000000000001E+99"),
                ec(0x547D42AEA287A3D8L,"1.0000000000001449E99", "1.0000000000001E+99"),
-//             ec(0x547D42AEA287A3D9L,"1.000000000000145E99",  "1.0000000000002E+99"),
+               ec(0x547D42AEA287A3D9L,"1.000000000000145E99",  "1.0000000000002E+99"),
                ec(0x547D42AEA287A710L,"1.000000000000245E99",  "1.0000000000002E+99"),
-//             ec(0x547D42AEA287A711L,"1.0000000000002451E99", "1.0000000000003E+99"),
+               ec(0x547D42AEA287A711L,"1.0000000000002451E99", "1.0000000000003E+99"),
 
 
                ec(0x54B249AD2594C2F9L,"9.999999999999744E99",  "9.9999999999997E+99"),
-//             ec(0x54B249AD2594C2FAL,"9.999999999999746E99",  "9.9999999999998E+99"),
+               ec(0x54B249AD2594C2FAL,"9.999999999999746E99",  "9.9999999999998E+99"),
                ec(0x54B249AD2594C32DL,"9.999999999999845E99",  "9.9999999999998E+99"),
-//             ec(0x54B249AD2594C32EL,"9.999999999999847E99",  "9.9999999999999E+99"),
+               ec(0x54B249AD2594C32EL,"9.999999999999847E99",  "9.9999999999999E+99"),
                ec(0x54B249AD2594C360L,"9.999999999999944E99",  "9.9999999999999E+99"),
-//             ec(0x54B249AD2594C361L,"9.999999999999946E99",  "1E+100"),
+               ec(0x54B249AD2594C361L,"9.999999999999946E99",  "1E+100"),
                ec(0x54B249AD2594C464L,"1.0000000000000449E100","1E+100"),
-//             ec(0x54B249AD2594C465L,"1.000000000000045E100", "1.0000000000001E+100"),
+               ec(0x54B249AD2594C465L,"1.000000000000045E100", "1.0000000000001E+100"),
                ec(0x54B249AD2594C667L,"1.000000000000145E100", "1.0000000000001E+100"),
-//             ec(0x54B249AD2594C668L,"1.0000000000001451E100","1.0000000000002E+100"),
+               ec(0x54B249AD2594C668L,"1.0000000000001451E100","1.0000000000002E+100"),
                ec(0x54B249AD2594C86AL,"1.000000000000245E100", "1.0000000000002E+100"),
-//             ec(0x54B249AD2594C86BL,"1.0000000000002452E100","1.0000000000003E+100"),
+               ec(0x54B249AD2594C86BL,"1.0000000000002452E100","1.0000000000003E+100"),
 
 
                ec(0x2B95DF5CA28EF4A8L,"1.0000000000000251E-98","1.00000000000003E-98"),
-//             ec(0x2B95DF5CA28EF4A7L,"1.000000000000025E-98", "1.00000000000002E-98"),
+               ec(0x2B95DF5CA28EF4A7L,"1.000000000000025E-98", "1.00000000000002E-98"),
                ec(0x2B95DF5CA28EF46AL,"1.000000000000015E-98", "1.00000000000002E-98"),
                ec(0x2B95DF5CA28EF469L,"1.0000000000000149E-98","1.00000000000001E-98"),
                ec(0x2B95DF5CA28EF42DL,"1.0000000000000051E-98","1.00000000000001E-98"),
-//             ec(0x2B95DF5CA28EF42CL,"1.000000000000005E-98", "1E-98"),
-//             ec(0x2B95DF5CA28EF3ECL,"9.999999999999946E-99", "1E-98"),
+               ec(0x2B95DF5CA28EF42CL,"1.000000000000005E-98", "1E-98"),
+               ec(0x2B95DF5CA28EF3ECL,"9.999999999999946E-99", "1E-98"),
                ec(0x2B95DF5CA28EF3EBL,"9.999999999999944E-99", "9.9999999999999E-99"),
-//             ec(0x2B95DF5CA28EF3AEL,"9.999999999999845E-99", "9.9999999999999E-99"),
+               ec(0x2B95DF5CA28EF3AEL,"9.999999999999845E-99", "9.9999999999999E-99"),
                ec(0x2B95DF5CA28EF3ADL,"9.999999999999843E-99", "9.9999999999998E-99"),
-//             ec(0x2B95DF5CA28EF371L,"9.999999999999746E-99", "9.9999999999998E-99"),
+               ec(0x2B95DF5CA28EF371L,"9.999999999999746E-99", "9.9999999999998E-99"),
                ec(0x2B95DF5CA28EF370L,"9.999999999999744E-99", "9.9999999999997E-99"),
 
 
-//             ec(0x2B617F7D4ED8C7F5L,"1.000000000000245E-99", "1.0000000000003E-99"),
+               ec(0x2B617F7D4ED8C7F5L,"1.000000000000245E-99", "1.0000000000003E-99"),
                ec(0x2B617F7D4ED8C7F4L,"1.0000000000002449E-99","1.0000000000002E-99"),
-//             ec(0x2B617F7D4ED8C609L,"1.0000000000001452E-99","1.0000000000002E-99"),
+               ec(0x2B617F7D4ED8C609L,"1.0000000000001452E-99","1.0000000000002E-99"),
                ec(0x2B617F7D4ED8C608L,"1.000000000000145E-99", "1.0000000000001E-99"),
-//             ec(0x2B617F7D4ED8C41CL,"1.000000000000045E-99", "1.0000000000001E-99"),
+               ec(0x2B617F7D4ED8C41CL,"1.000000000000045E-99", "1.0000000000001E-99"),
                ec(0x2B617F7D4ED8C41BL,"1.0000000000000449E-99","1E-99"),
-//             ec(0x2B617F7D4ED8C323L,"9.999999999999945E-100","1E-99"),
+               ec(0x2B617F7D4ED8C323L,"9.999999999999945E-100","1E-99"),
                ec(0x2B617F7D4ED8C322L,"9.999999999999943E-100","9.9999999999999E-100"),
-//             ec(0x2B617F7D4ED8C2F2L,"9.999999999999846E-100","9.9999999999999E-100"),
+               ec(0x2B617F7D4ED8C2F2L,"9.999999999999846E-100","9.9999999999999E-100"),
                ec(0x2B617F7D4ED8C2F1L,"9.999999999999844E-100","9.9999999999998E-100"),
-//             ec(0x2B617F7D4ED8C2C1L,"9.999999999999746E-100","9.9999999999998E-100"),
+               ec(0x2B617F7D4ED8C2C1L,"9.999999999999746E-100","9.9999999999998E-100"),
                ec(0x2B617F7D4ED8C2C0L,"9.999999999999744E-100","9.9999999999997E-100"),
 
 
diff --git a/src/testcases/org/apache/poi/ss/util/TestExpandedDouble.java b/src/testcases/org/apache/poi/ss/util/TestExpandedDouble.java
new file mode 100644 (file)
index 0000000..dd524fd
--- /dev/null
@@ -0,0 +1,225 @@
+/* ====================================================================
+   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 java.math.BigDecimal;
+import java.math.BigInteger;
+
+import junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import org.apache.poi.util.HexDump;
+/**
+ * Tests for {@link ExpandedDouble}
+ *
+ * @author Josh Micich
+ */
+public final class TestExpandedDouble extends TestCase {
+       private static final BigInteger BIG_POW_10 = BigInteger.valueOf(1000000000);
+
+       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());
+       }
+
+       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
+        */
+       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);
+               StringBuilder sb = new StringBuilder(20);
+               sb.append(HexDump.longToHex(l)).append('L');
+               return sb.toString();
+       }
+}
diff --git a/src/testcases/org/apache/poi/ss/util/TestNumberComparer.java b/src/testcases/org/apache/poi/ss/util/TestNumberComparer.java
new file mode 100644 (file)
index 0000000..7c3d87e
--- /dev/null
@@ -0,0 +1,106 @@
+/* ====================================================================
+   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 junit.framework.AssertionFailedError;
+import junit.framework.TestCase;
+
+import org.apache.poi.ss.util.NumberComparisonExamples.ComparisonExample;
+import org.apache.poi.util.HexDump;
+/**
+ * Tests for {@link NumberComparer}
+ *
+ * @author Josh Micich
+ */
+public final class TestNumberComparer extends TestCase {
+
+       public void testAllComparisonExamples() {
+               ComparisonExample[] examples = NumberComparisonExamples.getComparisonExamples();
+               boolean success = true;
+
+               for(int i=0;i<examples.length; i++) {
+                       ComparisonExample ce = examples[i];
+                       success &= confirm(i, ce.getA(), ce.getB(), +ce.getExpectedResult());
+                       success &= confirm(i, ce.getB(), ce.getA(), -ce.getExpectedResult());
+                       success &= confirm(i, ce.getNegA(), ce.getNegB(), -ce.getExpectedResult());
+                       success &= confirm(i, ce.getNegB(), ce.getNegA(), +ce.getExpectedResult());
+               }
+               if (!success) {
+                       throw new AssertionFailedError("One or more cases failed.  See stderr");
+               }
+       }
+
+       public void testRoundTripOnComparisonExamples() {
+               ComparisonExample[] examples = NumberComparisonExamples.getComparisonExamples();
+               boolean success = true;
+               for(int i=0;i<examples.length; i++) {
+                       ComparisonExample ce = examples[i];
+                       success &= confirmRoundTrip(i, ce.getA());
+                       success &= confirmRoundTrip(i, ce.getNegA());
+                       success &= confirmRoundTrip(i, ce.getB());
+                       success &= confirmRoundTrip(i, ce.getNegB());
+               }
+               if (!success) {
+                       throw new AssertionFailedError("One or more cases failed.  See stderr");
+               }
+
+       }
+
+       private boolean confirmRoundTrip(int i, double a) {
+               return TestExpandedDouble.confirmRoundTrip(i, Double.doubleToLongBits(a));
+       }
+
+       /**
+        * The actual example from bug 47598
+        */
+       public void testSpecificExampleA() {
+               double a = 0.06-0.01;
+               double b = 0.05;
+               assertFalse(a == b);
+               assertEquals(0, NumberComparer.compare(a, b));
+       }
+
+       /**
+        * The example from the nabble posting
+        */
+       public void testSpecificExampleB() {
+               double a = 1+1.0028-0.9973;
+               double b = 1.0055;
+               assertFalse(a == b);
+               assertEquals(0, NumberComparer.compare(a, b));
+       }
+
+       private static boolean confirm(int i, double a, double b, int expRes) {
+               int actRes = NumberComparer.compare(a, b);
+
+               int sgnActRes = actRes < 0 ? -1 : actRes > 0 ? +1 : 0;
+               if (sgnActRes != expRes) {
+                       System.err.println("Mismatch example[" + i + "] ("
+                                       + formatDoubleAsHex(a) + ", " + formatDoubleAsHex(b) + ") expected "
+                                       + expRes + " but got " + sgnActRes);
+                       return false;
+               }
+               return true;
+       }
+       private static String formatDoubleAsHex(double d) {
+               long l = Double.doubleToLongBits(d);
+               StringBuilder sb = new StringBuilder(20);
+               sb.append(HexDump.longToHex(l)).append('L');
+               return sb.toString();
+       }
+}