Bugzilla 47598 - Improved formula evaluator number comparison

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@798771 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Josh Micich 2009-07-29 03:36:25 +00:00
parent 172db2ca58
commit 17af35f713
15 changed files with 1597 additions and 261 deletions

View File

@ -33,6 +33,7 @@
<changes> <changes>
<release version="3.5-beta7" date="2009-??-??"> <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">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">47548 - Fixed RecordFactoryInputStream to properly read continued DrawingRecords</action>
<action dev="POI-DEVELOPERS" type="fix">46419 - Fixed compatibility issue with OpenOffice 3.0</action> <action dev="POI-DEVELOPERS" type="fix">46419 - Fixed compatibility issue with OpenOffice 3.0</action>

View File

@ -17,6 +17,8 @@
package org.apache.poi.hssf.record.formula.eval; package org.apache.poi.hssf.record.formula.eval;
import org.apache.poi.ss.util.NumberComparer;
/** /**
* Base class for all comparison operator evaluators * Base class for all comparison operator evaluators
* *
@ -108,8 +110,7 @@ public abstract class RelationalOperationEval implements OperationEval {
if (vb instanceof NumberEval) { if (vb instanceof NumberEval) {
NumberEval nA = (NumberEval) va; NumberEval nA = (NumberEval) va;
NumberEval nB = (NumberEval) vb; NumberEval nB = (NumberEval) vb;
// Excel considers -0.0 < 0.0 which is the same as Double.compare() return NumberComparer.compare(nA.getNumberValue(), nB.getNumberValue());
return Double.compare(nA.getNumberValue(), nB.getNumberValue());
} }
} }
throw new IllegalArgumentException("Bad operand types (" + va.getClass().getName() + "), (" throw new IllegalArgumentException("Bad operand types (" + va.getClass().getName() + "), ("
@ -126,7 +127,7 @@ public abstract class RelationalOperationEval implements OperationEval {
} }
if (v instanceof NumberEval) { if (v instanceof NumberEval) {
NumberEval ne = (NumberEval) v; NumberEval ne = (NumberEval) v;
return Double.compare(0, ne.getNumberValue()); return NumberComparer.compare(0.0, ne.getNumberValue());
} }
if (v instanceof StringEval) { if (v instanceof StringEval) {
StringEval se = (StringEval) v; StringEval se = (StringEval) v;

View File

@ -0,0 +1,98 @@
/* ====================================================================
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.BigInteger;
import static org.apache.poi.ss.util.IEEEDouble.*;
/**
* Represents a 64 bit IEEE double quantity expressed with both decimal and binary exponents
* Does not handle negative numbers or zero
* <p/>
* The value of a {@link ExpandedDouble} is given by<br/>
* <tt> a &times; 2<sup>b</sup></tt>
* <br/>
* where:<br/>
*
* <tt>a</tt> = <i>significand</i><br/>
* <tt>b</tt> = <i>binaryExponent</i> - bitLength(significand) + 1<br/>
*
* @author Josh Micich
*/
final class ExpandedDouble {
private static final BigInteger BI_FRAC_MASK = BigInteger.valueOf(FRAC_MASK);
private static final BigInteger BI_IMPLIED_FRAC_MSB = BigInteger.valueOf(FRAC_ASSUMED_HIGH_BIT);
private static BigInteger getFrac(long rawBits) {
return BigInteger.valueOf(rawBits).and(BI_FRAC_MASK).or(BI_IMPLIED_FRAC_MSB).shiftLeft(11);
}
public static ExpandedDouble fromRawBitsAndExponent(long rawBits, int exp) {
return new ExpandedDouble(getFrac(rawBits), exp);
}
/**
* Always 64 bits long (MSB, bit-63 is '1')
*/
private final BigInteger _significand;
private final int _binaryExponent;
public ExpandedDouble(long rawBits) {
int biasedExp = (int) (rawBits >> 52);
if (biasedExp == 0) {
// sub-normal numbers
BigInteger frac = BigInteger.valueOf(rawBits).and(BI_FRAC_MASK);
int expAdj = 64 - frac.bitLength();
_significand = frac.shiftLeft(expAdj);
_binaryExponent = (biasedExp & 0x07FF) - 1023 - expAdj;
} else {
BigInteger frac = getFrac(rawBits);
_significand = frac;
_binaryExponent = (biasedExp & 0x07FF) - 1023;
}
}
ExpandedDouble(BigInteger frac, int binaryExp) {
if (frac.bitLength() != 64) {
throw new IllegalArgumentException("bad bit length");
}
_significand = frac;
_binaryExponent = binaryExp;
}
/**
* Convert to an equivalent {@link NormalisedDecimal} representation having 15 decimal digits of precision in the
* non-fractional bits of the significand.
*/
public NormalisedDecimal normaliseBaseTen() {
return NormalisedDecimal.create(_significand, _binaryExponent);
}
/**
* @return the number of non-fractional bits after the MSB of the significand
*/
public int getBinaryExponent() {
return _binaryExponent;
}
public BigInteger getSignificand() {
return _significand;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,209 @@
/* ====================================================================
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.BigInteger;
final class MutableFPNumber {
// TODO - what about values between (10<sup>14</sup>-0.5) and (10<sup>14</sup>-0.05) ?
/**
* The minimum value in 'Base-10 normalised form'.<br/>
* When {@link #_binaryExponent} == 46 this is the the minimum {@link #_frac} value
* (10<sup>14</sup>-0.05) * 2^17
* <br/>
* Values between (10<sup>14</sup>-0.05) and 10<sup>14</sup> will be represented as '1'
* followed by 14 zeros.
* Values less than (10<sup>14</sup>-0.05) will get shifted by one more power of 10
*
* This frac value rounds to '1' followed by fourteen zeros with an incremented decimal exponent
*/
private static final BigInteger BI_MIN_BASE = new BigInteger("0B5E620F47FFFE666", 16);
/**
* For 'Base-10 normalised form'<br/>
* The maximum {@link #_frac} value when {@link #_binaryExponent} == 49
* (10^15-0.5) * 2^14
*/
private static final BigInteger BI_MAX_BASE = new BigInteger("0E35FA9319FFFE000", 16);
/**
* Width of a long
*/
private static final int C_64 = 64;
/**
* Minimum precision after discarding whole 32-bit words from the significand
*/
private static final int MIN_PRECISION = 72;
private BigInteger _significand;
private int _binaryExponent;
public MutableFPNumber(BigInteger frac, int binaryExponent) {
_significand = frac;
_binaryExponent = binaryExponent;
}
public MutableFPNumber copy() {
return new MutableFPNumber(_significand, _binaryExponent);
}
public void normalise64bit() {
int oldBitLen = _significand.bitLength();
int sc = oldBitLen - C_64;
if (sc == 0) {
return;
}
if (sc < 0) {
throw new IllegalStateException("Not enough precision");
}
_binaryExponent += sc;
if (sc > 32) {
int highShift = (sc-1) & 0xFFFFE0;
_significand = _significand.shiftRight(highShift);
sc -= highShift;
oldBitLen -= highShift;
}
if (sc < 1) {
throw new IllegalStateException();
}
_significand = Rounder.round(_significand, sc);
if (_significand.bitLength() > oldBitLen) {
sc++;
_binaryExponent++;
}
_significand = _significand.shiftRight(sc);
}
public int get64BitNormalisedExponent() {
return _binaryExponent + _significand.bitLength() - C_64;
}
@Override
public boolean equals(Object obj) {
MutableFPNumber other = (MutableFPNumber) obj;
if (_binaryExponent != other._binaryExponent) {
return false;
}
return _significand.equals(other._significand);
}
public boolean isBelowMaxRep() {
int sc = _significand.bitLength() - C_64;
return _significand.compareTo(BI_MAX_BASE.shiftLeft(sc)) < 0;
}
public boolean isAboveMinRep() {
int sc = _significand.bitLength() - C_64;
return _significand.compareTo(BI_MIN_BASE.shiftLeft(sc)) > 0;
}
public NormalisedDecimal createNormalisedDecimal(int pow10) {
// missingUnderBits is (0..3)
int missingUnderBits = _binaryExponent-39;
int fracPart = (_significand.intValue() << missingUnderBits) & 0xFFFF80;
long wholePart = _significand.shiftRight(C_64-_binaryExponent-1).longValue();
return new NormalisedDecimal(wholePart, fracPart, pow10);
}
public void multiplyByPowerOfTen(int pow10) {
TenPower tp = TenPower.getInstance(Math.abs(pow10));
if (pow10 < 0) {
mulShift(tp._divisor, tp._divisorShift);
} else {
mulShift(tp._multiplicand, tp._multiplierShift);
}
}
private void mulShift(BigInteger multiplicand, int multiplierShift) {
_significand = _significand.multiply(multiplicand);
_binaryExponent += multiplierShift;
// check for too much precision
int sc = (_significand.bitLength() - MIN_PRECISION) & 0xFFFFFFE0;
// mask makes multiples of 32 which optimises BigInteger.shiftRight
if (sc > 0) {
// no need to round because we have at least 8 bits of extra precision
_significand = _significand.shiftRight(sc);
_binaryExponent += sc;
}
}
private static final class Rounder {
private static final BigInteger[] HALF_BITS;
static {
BigInteger[] bis = new BigInteger[33];
long acc=1;
for (int i = 1; i < bis.length; i++) {
bis[i] = BigInteger.valueOf(acc);
acc <<=1;
}
HALF_BITS = bis;
}
/**
* @param nBits number of bits to shift right
*/
public static BigInteger round(BigInteger bi, int nBits) {
if (nBits < 1) {
return bi;
}
return bi.add(HALF_BITS[nBits]);
}
}
/**
* Holds values for quick multiplication and division by 10
*/
private static final class TenPower {
private static final BigInteger FIVE = new BigInteger("5");
private static final TenPower[] _cache = new TenPower[350];
public final BigInteger _multiplicand;
public final BigInteger _divisor;
public final int _divisorShift;
public final int _multiplierShift;
private TenPower(int index) {
BigInteger fivePowIndex = FIVE.pow(index);
int bitsDueToFiveFactors = fivePowIndex.bitLength();
int px = 80 + bitsDueToFiveFactors;
BigInteger fx = BigInteger.ONE.shiftLeft(px).divide(fivePowIndex);
int adj = fx.bitLength() - 80;
_divisor = fx.shiftRight(adj);
bitsDueToFiveFactors -= adj;
_divisorShift = -(bitsDueToFiveFactors+index+80);
int sc = fivePowIndex.bitLength() - 68;
if (sc > 0) {
_multiplierShift = index + sc;
_multiplicand = fivePowIndex.shiftRight(sc);
} else {
_multiplierShift = index;
_multiplicand = fivePowIndex;
}
}
static TenPower getInstance(int index) {
TenPower result = _cache[index];
if (result == null) {
result = new TenPower(index);
_cache[index] = result;
}
return result;
}
}
public ExpandedDouble createExpandedDouble() {
return new ExpandedDouble(_significand, _binaryExponent);
}
}

View File

@ -0,0 +1,271 @@
/* ====================================================================
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;
/**
* Represents a transformation of a 64 bit IEEE double quantity having a decimal exponent and a
* fixed point (15 decimal digit) significand. Some quirks of Excel's calculation behaviour are
* simpler to reproduce with numeric quantities in this format. This class is currently used to
* help:
* <ol>
* <li>Comparison operations</li>
* <li>Conversions to text</li>
* </ol>
*
* <p/>
* This class does not handle negative numbers or zero.
* <p/>
* The value of a {@link NormalisedDecimal} is given by<br/>
* <tt> significand &times; 10<sup>decimalExponent</sup></tt>
* <br/>
* where:<br/>
*
* <tt>significand</tt> = wholePart + fractionalPart / 2<sup>24</sup><br/>
*
* @author Josh Micich
*/
final class NormalisedDecimal {
/**
* Number of powers of ten contained in the significand
*/
private static final int EXPONENT_OFFSET = 14;
private static final BigDecimal BD_2_POW_24 = new BigDecimal(BigInteger.ONE.shiftLeft(24));
/**
* log<sub>10</sub>(2)&times;2<sup>20</sup>
*/
private static final int LOG_BASE_10_OF_2_TIMES_2_POW_20 = 315653; // 315652.8287
/**
* 2<sup>19</sup>
*/
private static final int C_2_POW_19 = 1 << 19;
/**
* the value of {@link #_fractionalPart} that represents 0.5
*/
private static final int FRAC_HALF = 0x800000;
/**
* 10<sup>15</sup>
*/
private static final long MAX_REP_WHOLE_PART = 0x38D7EA4C68000L;
public static NormalisedDecimal create(BigInteger frac, int binaryExponent) {
// estimate pow2&pow10 first, perform optional mulShift, then normalize
int pow10;
if (binaryExponent > 49 || binaryExponent < 46) {
// working with ints (left shifted 20) instead of doubles
// x = 14.5 - binaryExponent * log10(2);
int x = (29 << 19) - binaryExponent * LOG_BASE_10_OF_2_TIMES_2_POW_20;
x += C_2_POW_19; // round
pow10 = -(x >> 20);
} else {
pow10 = 0;
}
MutableFPNumber cc = new MutableFPNumber(frac, binaryExponent);
if (pow10 != 0) {
cc.multiplyByPowerOfTen(-pow10);
}
switch (cc.get64BitNormalisedExponent()) {
case 46:
if (cc.isAboveMinRep()) {
break;
}
case 44:
case 45:
cc.multiplyByPowerOfTen(1);
pow10--;
break;
case 47:
case 48:
break;
case 49:
if (cc.isBelowMaxRep()) {
break;
}
case 50:
cc.multiplyByPowerOfTen(-1);
pow10++;
break;
default:
throw new IllegalStateException("Bad binary exp " + cc.get64BitNormalisedExponent() + ".");
}
cc.normalise64bit();
return cc.createNormalisedDecimal(pow10);
}
/**
* Rounds at the digit with value 10<sup>decimalExponent</sup>
*/
public NormalisedDecimal roundUnits() {
long wholePart = _wholePart;
if (_fractionalPart >= FRAC_HALF) {
wholePart++;
}
int de = _relativeDecimalExponent;
if (wholePart < MAX_REP_WHOLE_PART) {
return new NormalisedDecimal(wholePart, 0, de);
}
return new NormalisedDecimal(wholePart/10, 0, de+1);
}
/**
* The decimal exponent increased by one less than the digit count of {@link #_wholePart}
*/
private final int _relativeDecimalExponent;
/**
* The whole part of the significand (typically 15 digits).
*
* 47-50 bits long (MSB may be anywhere from bit 46 to 49)
* LSB is units bit.
*/
private final long _wholePart;
/**
* The fractional part of the significand.
* 24 bits (only top 14-17 bits significant): a value between 0x000000 and 0xFFFF80
*/
private final int _fractionalPart;
NormalisedDecimal(long wholePart, int fracPart, int decimalExponent) {
_wholePart = wholePart;
_fractionalPart = fracPart;
_relativeDecimalExponent = decimalExponent;
}
/**
* Convert to an equivalent {@link ExpandedDouble} representation (binary frac and exponent).
* The resulting transformed object is easily converted to a 64 bit IEEE double:
* <ul>
* <li>bits 2-53 of the {@link #getSignificand()} become the 52 bit 'fraction'.</li>
* <li>{@link #getBinaryExponent()} is biased by 1023 to give the 'exponent'.</li>
* </ul>
* The sign bit must be obtained from somewhere else.
* @return a new {@link NormalisedDecimal} normalised to base 2 representation.
*/
public ExpandedDouble normaliseBaseTwo() {
MutableFPNumber cc = new MutableFPNumber(composeFrac(), 39);
cc.multiplyByPowerOfTen(_relativeDecimalExponent);
cc.normalise64bit();
return cc.createExpandedDouble();
}
/**
* @return the significand as a fixed point number (with 24 fraction bits and 47-50 whole bits)
*/
BigInteger composeFrac() {
long wp = _wholePart;
int fp = _fractionalPart;
return new BigInteger(new byte[] {
(byte) (wp >> 56), // N.B. assuming sign bit is zero
(byte) (wp >> 48),
(byte) (wp >> 40),
(byte) (wp >> 32),
(byte) (wp >> 24),
(byte) (wp >> 16),
(byte) (wp >> 8),
(byte) (wp >> 0),
(byte) (fp >> 16),
(byte) (fp >> 8),
(byte) (fp >> 0),
});
}
public String getSignificantDecimalDigits() {
return Long.toString(_wholePart);
}
/**
* Rounds the first whole digit position (considers only units digit, not frational part).
* Caller should check total digit count of result to see whether the rounding operation caused
* a carry out of the most significant digit
*/
public String getSignificantDecimalDigitsLastDigitRounded() {
long wp = _wholePart + 5; // rounds last digit
StringBuilder sb = new StringBuilder(24);
sb.append(wp);
sb.setCharAt(sb.length()-1, '0');
return sb.toString();
}
/**
* @return the number of powers of 10 which have been extracted from the significand and binary exponent.
*/
public int getDecimalExponent() {
return _relativeDecimalExponent+EXPONENT_OFFSET;
}
/**
* assumes both this and other are normalised
*/
public int compareNormalised(NormalisedDecimal other) {
int cmp = _relativeDecimalExponent - other._relativeDecimalExponent;
if (cmp != 0) {
return cmp;
}
if (_wholePart > other._wholePart) {
return 1;
}
if (_wholePart < other._wholePart) {
return -1;
}
return _fractionalPart - other._fractionalPart;
}
public BigDecimal getFractionalPart() {
return new BigDecimal(_fractionalPart).divide(BD_2_POW_24);
}
private String getFractionalDigits() {
if (_fractionalPart == 0) {
return "0";
}
return getFractionalPart().toString().substring(2);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getName());
sb.append(" [");
String ws = String.valueOf(_wholePart);
sb.append(ws.charAt(0));
sb.append('.');
sb.append(ws.substring(1));
sb.append(' ');
sb.append(getFractionalDigits());
sb.append("E");
sb.append(getDecimalExponent());
sb.append("]");
return sb.toString();
}
}

View File

@ -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();
}
}

View File

@ -17,8 +17,6 @@
package org.apache.poi.ss.util; 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 * 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 { 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 long EXCEL_NAN_BITS = 0xFFFF0420003C0000L;
private static final int MAX_TEXT_LEN = 20; 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() { private NumberToTextConverter() {
// no instances of this class // no instances of this class
} }
@ -149,186 +135,110 @@ public final class NumberToTextConverter {
if (isNegative) { if (isNegative) {
rawBits &= 0x7FFFFFFFFFFFFFFFL; rawBits &= 0x7FFFFFFFFFFFFFFFL;
} }
if (rawBits == 0) {
int biasedExponent = (int) ((rawBits & expMask) >> EXPONENT_SHIFT); return isNegative ? "-0" : "0";
if (biasedExponent == 0) { }
ExpandedDouble ed = new ExpandedDouble(rawBits);
if (ed.getBinaryExponent() < -1022) {
// value is 'denormalised' which means it is less than 2^-1022 // value is 'denormalised' which means it is less than 2^-1022
// excel displays all these numbers as zero, even though calculations work OK // excel displays all these numbers as zero, even though calculations work OK
return isNegative ? "-0" : "0"; return isNegative ? "-0" : "0";
} }
if (ed.getBinaryExponent() == 1024) {
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) {
// Special number NaN /Infinity // 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) { if(rawBits == EXCEL_NAN_BITS) {
return "3.484840871308E+308"; return "3.484840871308E+308";
} }
// This is where excel really gets it wrong // 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. // the standard rules below.
isNegative = false; // except that the sign bit is ignored isNegative = false; // except that the sign bit is ignored
} }
bd = convertToBigDecimal(exponent, fracBits); NormalisedDecimal nd = ed.normaliseBaseTen();
StringBuilder sb = new StringBuilder(MAX_TEXT_LEN+1);
return formatBigInteger(isNegative, bd.unscaledValue(), bd.scale()); if (isNegative) {
} sb.append('-');
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);
}
BigInteger sl = bigInt.shiftLeft(lastSigBitIndex);
return new BigDecimal(sl);
}
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);
} else {
int decimalPointIndex = unscaledLength - scale;
formatGreaterThanOne(sb, decimalPointIndex);
}
if(isNegative) {
sb.insert(0, '-');
} }
convertToText(sb, nd);
return sb.toString(); return sb.toString();
} }
private static void convertToText(StringBuilder sb, NormalisedDecimal pnd) {
private static int getNumberOfSignificantFiguresDisplayed(int exponent) { NormalisedDecimal rnd = pnd.roundUnits();
int nLostDigits; // number of significand digits lost due big exponents int decExponent = rnd.getDecimalExponent();
if(exponent > 99) { String decimalDigits;
// any exponent greater than 99 has 3 digits instead of 2 if (Math.abs(decExponent)>98) {
nLostDigits = 1; decimalDigits = rnd.getSignificantDecimalDigitsLastDigitRounded();
} else if (exponent < -98) { if (decimalDigits.length() == 16) {
// For some weird reason on the negative side // rounding caused carry
// step is occurs from -98 to -99 (not from -99 to -100) decExponent++;
nLostDigits = 1; }
} else { } else {
nLostDigits = 0; decimalDigits = rnd.getSignificantDecimalDigits();
}
int countSigDigits = countSignifantDigits(decimalDigits);
if (decExponent < 0) {
formatLessThanOne(sb, decimalDigits, decExponent, countSigDigits);
} else {
formatGreaterThanOne(sb, decimalDigits, decExponent, countSigDigits);
}
}
private static void formatLessThanOne(StringBuilder sb, String decimalDigits, int decExponent,
int countSigDigits) {
int nLeadingZeros = -decExponent - 1;
int normalLength = 2 + nLeadingZeros + countSigDigits; // 2 == "0.".length()
if (needsScientificNotation(normalLength)) {
sb.append(decimalDigits.charAt(0));
if (countSigDigits > 1) {
sb.append('.');
sb.append(decimalDigits.subSequence(1, countSigDigits));
}
sb.append("E-");
appendExp(sb, -decExponent);
return;
}
sb.append("0.");
for (int i=nLeadingZeros; i>0; i--) {
sb.append('0');
}
sb.append(decimalDigits.subSequence(0, countSigDigits));
}
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;
}
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;
}
sb.append(decimalDigits.subSequence(0, countSigDigits));
for (int i=-nFractionalDigits; i>0; i--) {
sb.append('0');
} }
return DEFAULT_COUNT_SIGNIFICANT_DIGITS - nLostDigits;
} }
private static boolean needsScientificNotation(int nDigits) { private static boolean needsScientificNotation(int nDigits) {
return nDigits > MAX_TEXT_LEN; return nDigits > MAX_TEXT_LEN;
} }
private static void formatGreaterThanOne(StringBuffer sb, int nIntegerDigits) { private static int countSignifantDigits(String sb) {
int result=sb.length()-1;
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, '.');
}
sb.append("E+");
appendExp(sb, decimalPointIndex-1);
return;
}
if(isAllZeros(sb, decimalPointIndex, maxSigFigs)) {
sb.setLength(decimalPointIndex);
return;
}
// else some sig-digits after the decimal point
sb.setLength(nSigFigures);
sb.insert(decimalPointIndex, '.');
}
/**
* @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");
}
if (pAbsExponent < 1) {
throw new IllegalArgumentException("abs(exponent) must be positive");
}
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);
}
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 int countSignifantDigits(StringBuffer sb, int startIx) {
int result=startIx;
while(sb.charAt(result) == '0') { while(sb.charAt(result) == '0') {
result--; result--;
if(result < 0) { if(result < 0) {
@ -338,68 +248,12 @@ public final class NumberToTextConverter {
return result + 1; return result + 1;
} }
private static void appendExp(StringBuffer sb, int val) { private static void appendExp(StringBuilder sb, int val) {
if(val < 10) { if(val < 10) {
sb.append('0'); sb.append('0');
sb.append((char)('0' + val)); sb.append((char)('0' + val));
return; return;
} }
sb.append(val); 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;
} }
} }

View File

@ -114,4 +114,19 @@ public final class TestEqualEval extends TestCase {
throw new AssertionFailedError("Identified bug 47198: -0.0 != 0.0"); 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");
}
}
} }

View File

@ -28,6 +28,8 @@ public final class AllSSUtilTests {
public static Test suite() { public static Test suite() {
TestSuite result = new TestSuite(AllSSUtilTests.class.getName()); TestSuite result = new TestSuite(AllSSUtilTests.class.getName());
result.addTestSuite(TestCellReference.class); result.addTestSuite(TestCellReference.class);
result.addTestSuite(TestExpandedDouble.class);
result.addTestSuite(TestNumberComparer.class);
result.addTestSuite(TestNumberToTextConverter.class); result.addTestSuite(TestNumberToTextConverter.class);
result.addTestSuite(TestRegion.class); result.addTestSuite(TestRegion.class);
return result; return result;

View File

@ -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() + "'");
}
}

View File

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

View File

@ -95,17 +95,17 @@ final class NumberToTextConversionExamples {
ec(0x4087A00000000000L, "756.0", "756"), ec(0x4087A00000000000L, "756.0", "756"),
ec(0x401E3D70A3D70A3DL, "7.56", "7.56"), 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(0x405EDD3C07FB4C99L, "123.45678901234568", "123.456789012346"),
ec(0x405EDD3C07FB4CAEL, "123.45678901234598", "123.456789012346"), ec(0x405EDD3C07FB4CAEL, "123.45678901234598", "123.456789012346"),
ec(0x4132D687E3DF2180L, "1234567.8901234567", "1234567.89012346"), 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(0x3F543A272D9E0E4AL, "0.0012345678901234552", "0.00123456789012346"),
ec(0x3F543A272D9E0E55L, "0.0012345678901234576", "0.00123456789012346"), ec(0x3F543A272D9E0E55L, "0.0012345678901234576", "0.00123456789012346"),
ec(0x3F543A272D9E0E72L, "0.0012345678901234639", "0.00123456789012346"), ec(0x3F543A272D9E0E72L, "0.0012345678901234639", "0.00123456789012346"),
ec(0x3F543A272D9E0E76L, "0.0012345678901234647", "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"), ec(0x3F543A272D9E0E78L, "0.0012345678901234652", "0.00123456789012347"),
@ -121,11 +121,11 @@ final class NumberToTextConversionExamples {
ec(0x544CE6345CF32121L, "1.2345678901234751E98", "1.23456789012348E+98"), 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(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(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"), ec(0x54820FE0BA17F921L, "1.23456789012375E99", "1.2345678901238E+99"),
@ -137,52 +137,52 @@ final class NumberToTextConversionExamples {
ec(0x547D42AEA2879F2AL,"9.999999999999995E98", "9.99999999999999E+98"), ec(0x547D42AEA2879F2AL,"9.999999999999995E98", "9.99999999999999E+98"),
ec(0x547D42AEA2879F2BL,"9.999999999999996E98", "1E+99"), ec(0x547D42AEA2879F2BL,"9.999999999999996E98", "1E+99"),
ec(0x547D42AEA287A0A0L,"1.0000000000000449E99", "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(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(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(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(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(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(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(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(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(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(0x2B95DF5CA28EF46AL,"1.000000000000015E-98", "1.00000000000002E-98"),
ec(0x2B95DF5CA28EF469L,"1.0000000000000149E-98","1.00000000000001E-98"), ec(0x2B95DF5CA28EF469L,"1.0000000000000149E-98","1.00000000000001E-98"),
ec(0x2B95DF5CA28EF42DL,"1.0000000000000051E-98","1.00000000000001E-98"), ec(0x2B95DF5CA28EF42DL,"1.0000000000000051E-98","1.00000000000001E-98"),
// ec(0x2B95DF5CA28EF42CL,"1.000000000000005E-98", "1E-98"), ec(0x2B95DF5CA28EF42CL,"1.000000000000005E-98", "1E-98"),
// ec(0x2B95DF5CA28EF3ECL,"9.999999999999946E-99", "1E-98"), ec(0x2B95DF5CA28EF3ECL,"9.999999999999946E-99", "1E-98"),
ec(0x2B95DF5CA28EF3EBL,"9.999999999999944E-99", "9.9999999999999E-99"), 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(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(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(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(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(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(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(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"), ec(0x2B617F7D4ED8C2C0L,"9.999999999999744E-100","9.9999999999997E-100"),

View File

@ -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();
}
}

View File

@ -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();
}
}