Fix for bug 46156 - improved POI's number rendering to more closely match Excel's

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@722284 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Josh Micich 2008-12-01 23:24:23 +00:00
parent 8be4abf717
commit b2713da293
15 changed files with 1149 additions and 162 deletions

View File

@ -37,6 +37,7 @@
<!-- Don't forget to update status.xml too! --> <!-- Don't forget to update status.xml too! -->
<release version="3.5-beta5" date="2008-??-??"> <release version="3.5-beta5" date="2008-??-??">
<action dev="POI-DEVELOPERS" type="add">46156 - Improved number to text conversion to be closer to that of Excel</action>
<action dev="POI-DEVELOPERS" type="fix">46312 - Fixed ValueRecordsAggregate to handle removal of new empty row</action> <action dev="POI-DEVELOPERS" type="fix">46312 - Fixed ValueRecordsAggregate to handle removal of new empty row</action>
<action dev="POI-DEVELOPERS" type="add">46269 - Improved error message when attempting to read BIFF2 file</action> <action dev="POI-DEVELOPERS" type="add">46269 - Improved error message when attempting to read BIFF2 file</action>
<action dev="POI-DEVELOPERS" type="fix">46206 - Fixed Sheet to tolerate missing DIMENSION records</action> <action dev="POI-DEVELOPERS" type="fix">46206 - Fixed Sheet to tolerate missing DIMENSION records</action>

View File

@ -34,6 +34,7 @@
<!-- Don't forget to update changes.xml too! --> <!-- Don't forget to update changes.xml too! -->
<changes> <changes>
<release version="3.5-beta5" date="2008-??-??"> <release version="3.5-beta5" date="2008-??-??">
<action dev="POI-DEVELOPERS" type="add">46156 - Improved number to text conversion to be closer to that of Excel</action>
<action dev="POI-DEVELOPERS" type="fix">46312 - Fixed ValueRecordsAggregate to handle removal of new empty row</action> <action dev="POI-DEVELOPERS" type="fix">46312 - Fixed ValueRecordsAggregate to handle removal of new empty row</action>
<action dev="POI-DEVELOPERS" type="add">46269 - Improved error message when attempting to read BIFF2 file</action> <action dev="POI-DEVELOPERS" type="add">46269 - Improved error message when attempting to read BIFF2 file</action>
<action dev="POI-DEVELOPERS" type="fix">46206 - Fixed Sheet to tolerate missing DIMENSION records</action> <action dev="POI-DEVELOPERS" type="fix">46206 - Fixed Sheet to tolerate missing DIMENSION records</action>

View File

@ -17,6 +17,7 @@
package org.apache.poi.hssf.record.formula; package org.apache.poi.hssf.record.formula;
import org.apache.poi.ss.util.NumberToTextConverter;
import org.apache.poi.util.LittleEndianInput; import org.apache.poi.util.LittleEndianInput;
import org.apache.poi.util.LittleEndianOutput; import org.apache.poi.util.LittleEndianOutput;
@ -65,7 +66,6 @@ public final class NumberPtg extends ScalarConstantPtg {
} }
public String toFormulaString() { public String toFormulaString() {
// TODO - java's rendering of double values is not quite same as excel's return NumberToTextConverter.toText(field_1_value);
return String.valueOf(field_1_value);
} }
} }

View File

@ -23,51 +23,45 @@ package org.apache.poi.hssf.record.formula.eval;
import org.apache.poi.hssf.record.formula.IntPtg; import org.apache.poi.hssf.record.formula.IntPtg;
import org.apache.poi.hssf.record.formula.NumberPtg; import org.apache.poi.hssf.record.formula.NumberPtg;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
import org.apache.poi.ss.util.NumberToTextConverter;
/** /**
* @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt; * @author Amol S. Deshmukh &lt; amolweb at ya hoo dot com &gt;
* *
*/ */
public class NumberEval implements NumericValueEval, StringValueEval { public final class NumberEval implements NumericValueEval, StringValueEval {
public static final NumberEval ZERO = new NumberEval(0); public static final NumberEval ZERO = new NumberEval(0);
private double value; private final double _value;
private String stringValue; private String _stringValue;
public NumberEval(Ptg ptg) { public NumberEval(Ptg ptg) {
if (ptg instanceof IntPtg) { if (ptg == null) {
this.value = ((IntPtg) ptg).getValue(); throw new IllegalArgumentException("ptg must not be null");
} }
else if (ptg instanceof NumberPtg) { if (ptg instanceof IntPtg) {
this.value = ((NumberPtg) ptg).getValue(); _value = ((IntPtg) ptg).getValue();
} else if (ptg instanceof NumberPtg) {
_value = ((NumberPtg) ptg).getValue();
} else {
throw new IllegalArgumentException("bad argument type (" + ptg.getClass().getName() + ")");
} }
} }
public NumberEval(double value) { public NumberEval(double value) {
this.value = value; _value = value;
} }
public double getNumberValue() { public double getNumberValue() {
return value; return _value;
} }
public String getStringValue() { // TODO: limit to 15 decimal places public String getStringValue() {
if (stringValue == null) if (_stringValue == null) {
makeString(); _stringValue = NumberToTextConverter.toText(_value);
return stringValue;
}
protected void makeString() {
if (!Double.isNaN(value)) {
long lvalue = Math.round(value);
if (lvalue == value) {
stringValue = String.valueOf(lvalue);
}
else {
stringValue = String.valueOf(value);
}
} }
return _stringValue;
} }
public final String toString() { public final String toString() {
StringBuffer sb = new StringBuffer(64); StringBuffer sb = new StringBuffer(64);

View File

@ -0,0 +1,405 @@
/* ====================================================================
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;
/**
* Excel converts numbers to text with different rules to those of java, so
* <code>Double.toString(value)</tt> won't do.
* <ul>
* <li>No more than 15 significant figures are output (java does 18).</li>
* <li>The sign char for the exponent is included even if positive</li>
* <li>Special values (<tt>NaN</tt> and <tt>Infinity</tt>) get rendered like the ordinary
* number that the bit pattern represents.</li>
* <li>Denormalised values (between &plusmn;2<sup>-1074</sup> and &plusmn;2<sup>-1022</sup>
* are displayed as "0"</sup>
* </ul>
* IEEE 64-bit Double Rendering Comparison
*
* <table border="1" cellpadding="2" cellspacing="0" summary="IEEE 64-bit Double Rendering Comparison">
* <tr><th>Raw bits</th><th>Java</th><th>Excel</th></tr>
*
* <tr><td>0x0000000000000000L</td><td>0.0</td><td>0</td></tr>
* <tr><td>0x3FF0000000000000L</td><td>1.0</td><td>1</td></tr>
* <tr><td>0x3FF00068DB8BAC71L</td><td>1.0001</td><td>1.0001</td></tr>
* <tr><td>0x4087A00000000000L</td><td>756.0</td><td>756</td></tr>
* <tr><td>0x401E3D70A3D70A3DL</td><td>7.56</td><td>7.56</td></tr>
* <tr><td>0x405EDD3C07FB4C99L</td><td>123.45678901234568</td><td>123.456789012346</td></tr>
* <tr><td>0x4132D687E3DF2180L</td><td>1234567.8901234567</td><td>1234567.89012346</td></tr>
* <tr><td>0x3EE9E409302678BAL</td><td>1.2345678901234568E-5</td><td>1.23456789012346E-05</td></tr>
* <tr><td>0x3F202E85BE180B74L</td><td>1.2345678901234567E-4</td><td>0.000123456789012346</td></tr>
* <tr><td>0x3F543A272D9E0E51L</td><td>0.0012345678901234567</td><td>0.00123456789012346</td></tr>
* <tr><td>0x3F8948B0F90591E6L</td><td>0.012345678901234568</td><td>0.0123456789012346</td></tr>
* <tr><td>0x3EE9E409301B5A02L</td><td>1.23456789E-5</td><td>0.0000123456789</td></tr>
* <tr><td>0x3E6E7D05BDABDE50L</td><td>5.6789012345E-8</td><td>0.000000056789012345</td></tr>
* <tr><td>0x3E6E7D05BDAD407EL</td><td>5.67890123456E-8</td><td>5.67890123456E-08</td></tr>
* <tr><td>0x3E6E7D06029F18BEL</td><td>5.678902E-8</td><td>0.00000005678902</td></tr>
* <tr><td>0x2BCB5733CB32AE6EL</td><td>9.999999999999123E-98</td><td>9.99999999999912E-98</td></tr>
* <tr><td>0x2B617F7D4ED8C59EL</td><td>1.0000000000001235E-99</td><td>1.0000000000001E-99</td></tr>
* <tr><td>0x0036319916D67853L</td><td>1.2345678901234578E-307</td><td>1.2345678901235E-307</td></tr>
* <tr><td>0x359DEE7A4AD4B81FL</td><td>2.0E-50</td><td>2E-50</td></tr>
* <tr><td>0x41678C29DCD6E9E0L</td><td>1.2345678901234567E7</td><td>12345678.9012346</td></tr>
* <tr><td>0x42A674E79C5FE523L</td><td>1.2345678901234568E13</td><td>12345678901234.6</td></tr>
* <tr><td>0x42DC12218377DE6BL</td><td>1.2345678901234567E14</td><td>123456789012346</td></tr>
* <tr><td>0x43118B54F22AEB03L</td><td>1.2345678901234568E15</td><td>1234567890123460</td></tr>
* <tr><td>0x43E56A95319D63E1L</td><td>1.2345678901234567E19</td><td>12345678901234600000</td></tr>
* <tr><td>0x441AC53A7E04BCDAL</td><td>1.2345678901234568E20</td><td>1.23456789012346E+20</td></tr>
* <tr><td>0xC3E56A95319D63E1L</td><td>-1.2345678901234567E19</td><td>-12345678901234600000</td></tr>
* <tr><td>0xC41AC53A7E04BCDAL</td><td>-1.2345678901234568E20</td><td>-1.23456789012346E+20</td></tr>
* <tr><td>0x54820FE0BA17F46DL</td><td>1.2345678901234577E99</td><td>1.2345678901235E+99</td></tr>
* <tr><td>0x54B693D8E89DF188L</td><td>1.2345678901234576E100</td><td>1.2345678901235E+100</td></tr>
* <tr><td>0x4A611B0EC57E649AL</td><td>2.0E50</td><td>2E+50</td></tr>
* <tr><td>0x7FEFFFFFFFFFFFFFL</td><td>1.7976931348623157E308</td><td>1.7976931348623E+308</td></tr>
* <tr><td>0x0010000000000000L</td><td>2.2250738585072014E-308</td><td>2.2250738585072E-308</td></tr>
* <tr><td>0x000FFFFFFFFFFFFFL</td><td>2.225073858507201E-308</td><td>0</td></tr>
* <tr><td>0x0000000000000001L</td><td>4.9E-324</td><td>0</td></tr>
* <tr><td>0x7FF0000000000000L</td><td>Infinity</td><td>1.7976931348623E+308</td></tr>
* <tr><td>0xFFF0000000000000L</td><td>-Infinity</td><td>1.7976931348623E+308</td></tr>
* <tr><td>0x441AC7A08EAD02F2L</td><td>1.234999999999999E20</td><td>1.235E+20</td></tr>
* <tr><td>0x40FE26BFFFFFFFF9L</td><td>123499.9999999999</td><td>123500</td></tr>
* <tr><td>0x3E4A857BFB2F2809L</td><td>1.234999999999999E-8</td><td>0.00000001235</td></tr>
* <tr><td>0x3BCD291DEF868C89L</td><td>1.234999999999999E-20</td><td>1.235E-20</td></tr>
* <tr><td>0x444B1AE4D6E2EF4FL</td><td>9.999999999999999E20</td><td>1E+21</td></tr>
* <tr><td>0x412E847FFFFFFFFFL</td><td>999999.9999999999</td><td>1000000</td></tr>
* <tr><td>0x3E45798EE2308C39L</td><td>9.999999999999999E-9</td><td>0.00000001</td></tr>
* <tr><td>0x3C32725DD1D243ABL</td><td>9.999999999999999E-19</td><td>0.000000000000000001</td></tr>
* <tr><td>0x3BFD83C94FB6D2ABL</td><td>9.999999999999999E-20</td><td>1E-19</td></tr>
* <tr><td>0xC44B1AE4D6E2EF4FL</td><td>-9.999999999999999E20</td><td>-1E+21</td></tr>
* <tr><td>0xC12E847FFFFFFFFFL</td><td>-999999.9999999999</td><td>-1000000</td></tr>
* <tr><td>0xBE45798EE2308C39L</td><td>-9.999999999999999E-9</td><td>-0.00000001</td></tr>
* <tr><td>0xBC32725DD1D243ABL</td><td>-9.999999999999999E-19</td><td>-0.000000000000000001</td></tr>
* <tr><td>0xBBFD83C94FB6D2ABL</td><td>-9.999999999999999E-20</td><td>-1E-19</td></tr>
* <tr><td>0xFFFF0420003C0000L</td><td>NaN</td><td>3.484840871308E+308</td></tr>
* <tr><td>0x7FF8000000000000L</td><td>NaN</td><td>2.6965397022935E+308</td></tr>
* <tr><td>0x7FFF0420003C0000L</td><td>NaN</td><td>3.484840871308E+308</td></tr>
* <tr><td>0xFFF8000000000000L</td><td>NaN</td><td>2.6965397022935E+308</td></tr>
* <tr><td>0xFFFF0AAAAAAAAAAAL</td><td>NaN</td><td>3.4877119413344E+308</td></tr>
* <tr><td>0x7FF80AAAAAAAAAAAL</td><td>NaN</td><td>2.7012211948322E+308</td></tr>
* <tr><td>0xFFFFFFFFFFFFFFFFL</td><td>NaN</td><td>3.5953862697246E+308</td></tr>
* <tr><td>0x7FFFFFFFFFFFFFFFL</td><td>NaN</td><td>3.5953862697246E+308</td></tr>
* <tr><td>0xFFF7FFFFFFFFFFFFL</td><td>NaN</td><td>2.6965397022935E+308</td></tr>
* </table>
*
* <b>Note</b>:
* Excel has inconsistent rules for the following numeric operations:
* <ul>
* <li>Conversion to string (as handled here)</li>
* <li>Rendering numerical quantities in the cell grid.</li>
* <li>Conversion from text</li>
* <li>General arithmetic</li>
* </ul>
* Excel's text to number conversion is not a true <i>inverse</i> of this operation. The
* allowable ranges are different. Some numbers that don't correctly convert to text actually
* <b>do</b> get handled properly when used in arithmetic evaluations.
*
* @author Josh Micich
*/
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
}
/**
* Converts the supplied <tt>value</tt> to the text representation that Excel would give if
* the value were to appear in an unformatted cell, or as a literal number in a formula.<br/>
* Note - the results from this method differ slightly from those of <tt>Double.toString()</tt>
* In some special cases Excel behaves quite differently. This function attempts to reproduce
* those results.
*/
public static String toText(double value) {
return rawDoubleBitsToText(Double.doubleToLongBits(value));
}
/* package */ static String rawDoubleBitsToText(long pRawBits) {
long rawBits = pRawBits;
boolean isNegative = rawBits < 0; // sign bit is in the same place for long and double
if (isNegative) {
rawBits &= 0x7FFFFFFFFFFFFFFFL;
}
int biasedExponent = (int) ((rawBits & expMask) >> EXPONENT_SHIFT);
if (biasedExponent == 0) {
// value is 'denormalised' which means it is less than 2^-1022
// excel displays all these numbers as zero, even though calculations work OK
return "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) {
// Special number NaN /Infinity
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
// 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);
}
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, '-');
}
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;
} else {
nLostDigits = 0;
}
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) {
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') {
result--;
if(result < 0) {
throw new RuntimeException("No non-zero digits found");
}
}
return result + 1;
}
private static void appendExp(StringBuffer 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;
}
}

View File

@ -264,12 +264,7 @@ public class LittleEndian implements LittleEndianConsts {
*@param value the double (64-bit) value *@param value the double (64-bit) value
*/ */
public static void putDouble(byte[] data, int offset, double value) { public static void putDouble(byte[] data, int offset, double value) {
// Excel likes NaN to be a specific value. putLong(data, offset, Double.doubleToLongBits(value));
if (Double.isNaN(value)) {
putLong(data, offset, -276939487313920L);
} else {
putLong(data, offset, Double.doubleToLongBits(value));
}
} }

View File

@ -29,6 +29,7 @@ import org.apache.poi.hssf.record.AllRecordTests;
import org.apache.poi.hssf.usermodel.AllUserModelTests; import org.apache.poi.hssf.usermodel.AllUserModelTests;
import org.apache.poi.hssf.util.AllHSSFUtilTests; import org.apache.poi.hssf.util.AllHSSFUtilTests;
import org.apache.poi.ss.formula.AllSSFormulaTests; import org.apache.poi.ss.formula.AllSSFormulaTests;
import org.apache.poi.ss.util.AllSSUtilTests;
/** /**
* Test Suite for all sub-packages of org.apache.poi.hssf<br/> * Test Suite for all sub-packages of org.apache.poi.hssf<br/>
@ -54,6 +55,7 @@ public final class HSSFTests {
suite.addTest(new TestSuite(TestEventRecordFactory.class)); suite.addTest(new TestSuite(TestEventRecordFactory.class));
suite.addTest(new TestSuite(TestModelFactory.class)); suite.addTest(new TestSuite(TestModelFactory.class));
suite.addTest(AllSSFormulaTests.suite()); suite.addTest(AllSSFormulaTests.suite());
suite.addTest(AllSSUtilTests.suite());
return suite; return suite;
} }
} }

View File

@ -283,63 +283,63 @@ public final class TestFormulaParser extends TestCase {
cell.setCellFormula("1.3E21/3"); cell.setCellFormula("1.3E21/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "1.3E21/3", formula); assertEquals("Exponential formula string", "1.3E+21/3", formula);
cell.setCellFormula("-1.3E21/3"); cell.setCellFormula("-1.3E21/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-1.3E21/3", formula); assertEquals("Exponential formula string", "-1.3E+21/3", formula);
cell.setCellFormula("1322E21/3"); cell.setCellFormula("1322E21/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "1.322E24/3", formula); assertEquals("Exponential formula string", "1.322E+24/3", formula);
cell.setCellFormula("-1322E21/3"); cell.setCellFormula("-1322E21/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-1.322E24/3", formula); assertEquals("Exponential formula string", "-1.322E+24/3", formula);
cell.setCellFormula("1.3E1/3"); cell.setCellFormula("1.3E1/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "13.0/3", formula); assertEquals("Exponential formula string", "13/3", formula);
cell.setCellFormula("-1.3E1/3"); cell.setCellFormula("-1.3E1/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-13.0/3", formula); assertEquals("Exponential formula string", "-13/3", formula);
cell.setCellFormula("1.3E-4/3"); cell.setCellFormula("1.3E-4/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "1.3E-4/3", formula); assertEquals("Exponential formula string", "0.00013/3", formula);
cell.setCellFormula("-1.3E-4/3"); cell.setCellFormula("-1.3E-4/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-1.3E-4/3", formula); assertEquals("Exponential formula string", "-0.00013/3", formula);
cell.setCellFormula("13E-15/3"); cell.setCellFormula("13E-15/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "1.3E-14/3", formula); assertEquals("Exponential formula string", "0.000000000000013/3", formula);
cell.setCellFormula("-13E-15/3"); cell.setCellFormula("-13E-15/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-1.3E-14/3", formula); assertEquals("Exponential formula string", "-0.000000000000013/3", formula);
cell.setCellFormula("1.3E3/3"); cell.setCellFormula("1.3E3/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "1300.0/3", formula); assertEquals("Exponential formula string", "1300/3", formula);
cell.setCellFormula("-1.3E3/3"); cell.setCellFormula("-1.3E3/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-1300.0/3", formula); assertEquals("Exponential formula string", "-1300/3", formula);
cell.setCellFormula("1300000000000000/3"); cell.setCellFormula("1300000000000000/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "1.3E15/3", formula); assertEquals("Exponential formula string", "1300000000000000/3", formula);
cell.setCellFormula("-1300000000000000/3"); cell.setCellFormula("-1300000000000000/3");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-1.3E15/3", formula); assertEquals("Exponential formula string", "-1300000000000000/3", formula);
cell.setCellFormula("-10E-1/3.1E2*4E3/3E4"); cell.setCellFormula("-10E-1/3.1E2*4E3/3E4");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("Exponential formula string", "-1.0/310.0*4000.0/30000.0", formula); assertEquals("Exponential formula string", "-1/310*4000/30000", formula);
} }
public void testNumbers() { public void testNumbers() {
@ -370,15 +370,15 @@ public final class TestFormulaParser extends TestCase {
cell.setCellFormula("10E1"); cell.setCellFormula("10E1");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("100.0", formula); assertEquals("100", formula);
cell.setCellFormula("10E+1"); cell.setCellFormula("10E+1");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("100.0", formula); assertEquals("100", formula);
cell.setCellFormula("10E-1"); cell.setCellFormula("10E-1");
formula = cell.getCellFormula(); formula = cell.getCellFormula();
assertEquals("1.0", formula); assertEquals("1", formula);
} }
public void testRanges() { public void testRanges() {

View File

@ -17,19 +17,18 @@
package org.apache.poi.hssf.record.formula.functions; package org.apache.poi.hssf.record.formula.functions;
import junit.framework.TestCase;
import org.apache.poi.hssf.record.formula.eval.Eval; import org.apache.poi.hssf.record.formula.eval.Eval;
import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.NumberEval;
import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFErrorConstants; import org.apache.poi.hssf.usermodel.HSSFErrorConstants;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
/** /**
* Tests for {@link FinanceFunction#NPER}
* *
* @author Josh Micich * @author Josh Micich
*/ */
@ -54,7 +53,7 @@ public final class TestNper extends TestCase {
cell.setCellFormula("NPER(12,4500,100000,100000)"); cell.setCellFormula("NPER(12,4500,100000,100000)");
cell.setCellValue(15.0); cell.setCellValue(15.0);
assertEquals("NPER(12,4500,100000.0,100000.0)", cell.getCellFormula()); assertEquals("NPER(12,4500,100000,100000)", cell.getCellFormula());
assertEquals(HSSFCell.CELL_TYPE_NUMERIC, cell.getCachedFormulaResultType()); assertEquals(HSSFCell.CELL_TYPE_NUMERIC, cell.getCachedFormulaResultType());
assertEquals(15.0, cell.getNumericCellValue(), 0.0); assertEquals(15.0, cell.getNumericCellValue(), 0.0);
@ -63,5 +62,4 @@ public final class TestNper extends TestCase {
assertEquals(HSSFCell.CELL_TYPE_ERROR, cell.getCachedFormulaResultType()); assertEquals(HSSFCell.CELL_TYPE_ERROR, cell.getCachedFormulaResultType());
assertEquals(HSSFErrorConstants.ERROR_NUM, cell.getErrorCellValue()); assertEquals(HSSFErrorConstants.ERROR_NUM, cell.getErrorCellValue());
} }
} }

View File

@ -419,7 +419,7 @@ public final class TestBugs extends TestCase {
if (name.isFunctionName()) { if (name.isFunctionName()) {
continue; continue;
} }
name.getReference(); name.getRefersToFormula();
} }
} }
@ -1070,8 +1070,8 @@ public final class TestBugs extends TestCase {
HSSFSheet s = wb.createSheet(); HSSFSheet s = wb.createSheet();
s.createRow(0); s.createRow(0);
s.createRow(1); s.createRow(1);
HSSFCell c1 = s.getRow(0).createCell(0); s.getRow(0).createCell(0);
HSSFCell c2 = s.getRow(1).createCell(0); s.getRow(1).createCell(0);
assertEquals(4, wb.getNumberOfFonts()); assertEquals(4, wb.getNumberOfFonts());
@ -1162,7 +1162,7 @@ public final class TestBugs extends TestCase {
c3.setCellFormula("\"90210\""); c3.setCellFormula("\"90210\"");
// Check the formulas // Check the formulas
assertEquals("70164.0", c1.getCellFormula()); assertEquals("70164", c1.getCellFormula());
assertEquals("\"70164\"", c2.getCellFormula()); assertEquals("\"70164\"", c2.getCellFormula());
// And check the values - blank // And check the values - blank
@ -1420,60 +1420,60 @@ public final class TestBugs extends TestCase {
* header / footer text too long * header / footer text too long
*/ */
public void test45777() { public void test45777() {
HSSFWorkbook wb = new HSSFWorkbook(); HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet s = wb.createSheet(); HSSFSheet s = wb.createSheet();
String s248 = ""; String s248 = "";
for(int i=0; i<248; i++) { for(int i=0; i<248; i++) {
s248 += "x"; s248 += "x";
} }
String s249 = s248 + "1"; String s249 = s248 + "1";
String s250 = s248 + "12"; String s250 = s248 + "12";
String s251 = s248 + "123"; String s251 = s248 + "123";
assertEquals(248, s248.length()); assertEquals(248, s248.length());
assertEquals(249, s249.length()); assertEquals(249, s249.length());
assertEquals(250, s250.length()); assertEquals(250, s250.length());
assertEquals(251, s251.length()); assertEquals(251, s251.length());
// Try on headers // Try on headers
s.getHeader().setCenter(s248); s.getHeader().setCenter(s248);
assertEquals(254, s.getHeader().getRawHeader().length()); assertEquals(254, s.getHeader().getRawHeader().length());
writeOutAndReadBack(wb); writeOutAndReadBack(wb);
s.getHeader().setCenter(s249); s.getHeader().setCenter(s249);
assertEquals(255, s.getHeader().getRawHeader().length()); assertEquals(255, s.getHeader().getRawHeader().length());
writeOutAndReadBack(wb); writeOutAndReadBack(wb);
try { try {
s.getHeader().setCenter(s250); // 256 s.getHeader().setCenter(s250); // 256
fail(); fail();
} catch(IllegalArgumentException e) {} } catch(IllegalArgumentException e) {}
try { try {
s.getHeader().setCenter(s251); // 257 s.getHeader().setCenter(s251); // 257
fail(); fail();
} catch(IllegalArgumentException e) {} } catch(IllegalArgumentException e) {}
// Now try on footers // Now try on footers
s.getFooter().setCenter(s248); s.getFooter().setCenter(s248);
assertEquals(254, s.getFooter().getRawFooter().length()); assertEquals(254, s.getFooter().getRawFooter().length());
writeOutAndReadBack(wb); writeOutAndReadBack(wb);
s.getFooter().setCenter(s249); s.getFooter().setCenter(s249);
assertEquals(255, s.getFooter().getRawFooter().length()); assertEquals(255, s.getFooter().getRawFooter().length());
writeOutAndReadBack(wb); writeOutAndReadBack(wb);
try { try {
s.getFooter().setCenter(s250); // 256 s.getFooter().setCenter(s250); // 256
fail(); fail();
} catch(IllegalArgumentException e) {} } catch(IllegalArgumentException e) {}
try { try {
s.getFooter().setCenter(s251); // 257 s.getFooter().setCenter(s251); // 257
fail(); fail();
} catch(IllegalArgumentException e) {} } catch(IllegalArgumentException e) {}
} }
/** /**
@ -1490,36 +1490,36 @@ public final class TestBugs extends TestCase {
* Cell background colours * Cell background colours
*/ */
public void test45492() { public void test45492() {
HSSFWorkbook wb = openSample("45492.xls"); HSSFWorkbook wb = openSample("45492.xls");
HSSFSheet s = wb.getSheetAt(0); HSSFSheet s = wb.getSheetAt(0);
HSSFRow r = s.getRow(0); HSSFRow r = s.getRow(0);
HSSFPalette p = wb.getCustomPalette(); HSSFPalette p = wb.getCustomPalette();
HSSFCell auto = r.getCell(0); HSSFCell auto = r.getCell(0);
HSSFCell grey = r.getCell(1); HSSFCell grey = r.getCell(1);
HSSFCell red = r.getCell(2); HSSFCell red = r.getCell(2);
HSSFCell blue = r.getCell(3); HSSFCell blue = r.getCell(3);
HSSFCell green = r.getCell(4); HSSFCell green = r.getCell(4);
assertEquals(64, auto.getCellStyle().getFillForegroundColor()); assertEquals(64, auto.getCellStyle().getFillForegroundColor());
assertEquals(64, auto.getCellStyle().getFillBackgroundColor()); assertEquals(64, auto.getCellStyle().getFillBackgroundColor());
assertEquals("0:0:0", p.getColor(64).getHexString()); assertEquals("0:0:0", p.getColor(64).getHexString());
assertEquals(22, grey.getCellStyle().getFillForegroundColor()); assertEquals(22, grey.getCellStyle().getFillForegroundColor());
assertEquals(64, grey.getCellStyle().getFillBackgroundColor()); assertEquals(64, grey.getCellStyle().getFillBackgroundColor());
assertEquals("C0C0:C0C0:C0C0", p.getColor(22).getHexString()); assertEquals("C0C0:C0C0:C0C0", p.getColor(22).getHexString());
assertEquals(10, red.getCellStyle().getFillForegroundColor()); assertEquals(10, red.getCellStyle().getFillForegroundColor());
assertEquals(64, red.getCellStyle().getFillBackgroundColor()); assertEquals(64, red.getCellStyle().getFillBackgroundColor());
assertEquals("FFFF:0:0", p.getColor(10).getHexString()); assertEquals("FFFF:0:0", p.getColor(10).getHexString());
assertEquals(12, blue.getCellStyle().getFillForegroundColor()); assertEquals(12, blue.getCellStyle().getFillForegroundColor());
assertEquals(64, blue.getCellStyle().getFillBackgroundColor()); assertEquals(64, blue.getCellStyle().getFillBackgroundColor());
assertEquals("0:0:FFFF", p.getColor(12).getHexString()); assertEquals("0:0:FFFF", p.getColor(12).getHexString());
assertEquals(11, green.getCellStyle().getFillForegroundColor()); assertEquals(11, green.getCellStyle().getFillForegroundColor());
assertEquals(64, green.getCellStyle().getFillBackgroundColor()); assertEquals(64, green.getCellStyle().getFillBackgroundColor());
assertEquals("0:FFFF:0", p.getColor(11).getHexString()); assertEquals("0:FFFF:0", p.getColor(11).getHexString());
} }
/** /**

View File

@ -151,7 +151,7 @@ public final class TestFormulaEvaluatorBugs extends TestCase {
row = sheet.getRow(8); row = sheet.getRow(8);
cell = row.getCell(0); cell = row.getCell(0);
assertEquals("327680.0/32768", cell.getCellFormula()); assertEquals("327680/32768", cell.getCellFormula());
assertEquals(10, eva.evaluate(cell).getNumberValue(), 0); assertEquals(10, eva.evaluate(cell).getNumberValue(), 0);
row = sheet.getRow(9); row = sheet.getRow(9);
@ -166,15 +166,9 @@ public final class TestFormulaEvaluatorBugs extends TestCase {
row = sheet.getRow(11); row = sheet.getRow(11);
cell = row.getCell(0); cell = row.getCell(0);
assertEquals("-1000000.0-3000000.0", cell.getCellFormula()); assertEquals("-1000000-3000000", cell.getCellFormula());
assertEquals(-4000000, eva.evaluate(cell).getNumberValue(), 0); assertEquals(-4000000, eva.evaluate(cell).getNumberValue(), 0);
} }
// public static void main(String[] args) {
// new TestFormulaEvaluatorBugs().test44410();
// new TestFormulaEvaluatorBugs().testSlowEvaluate45376();
// new HSSFWorkbook();
// System.out.println("done");
// }
/** /**
* Bug 44410: SUM(C:C) is valid in excel, and means a sum * Bug 44410: SUM(C:C) is valid in excel, and means a sum

View File

@ -0,0 +1,35 @@
/* ====================================================================
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.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() {
TestSuite result = new TestSuite(AllSSUtilTests.class.getName());
result.addTestSuite(TestCellReference.class);
result.addTestSuite(TestNumberToTextConverter.class);
result.addTestSuite(TestRegion.class);
return result;
}
}

View File

@ -0,0 +1,251 @@
/* ====================================================================
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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
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.NumberToTextConversionExamples.ExampleConversion;
import org.apache.poi.util.HexDump;
import org.apache.poi.util.HexRead;
/**
* Creates a spreadsheet that demonstrates Excel's rendering of various IEEE double values.
*
* @author Josh Micich
*/
public class NumberRenderingSpreadsheetGenerator {
private static final class SheetWriter {
private final HSSFSheet _sheet;
private int _rowIndex;
private final List<Long> _replacementNaNs;
public SheetWriter(HSSFWorkbook wb) {
HSSFSheet sheet = wb.createSheet("Sheet1");
writeHeaderRow(wb, sheet);
_sheet = sheet;
_rowIndex = 1;
_replacementNaNs = new ArrayList<Long>();
}
public void addTestRow(long rawBits, String expectedExcelRendering) {
writeDataRow(_sheet, _rowIndex++, rawBits, expectedExcelRendering);
if(Double.isNaN(Double.longBitsToDouble(rawBits))) {
_replacementNaNs.add(new Long(rawBits));
}
}
public long[] getReplacementNaNs() {
int nRepls = _replacementNaNs.size();
long[] result = new long[nRepls];
for (int i = 0; i < nRepls; i++) {
result[i] = _replacementNaNs.get(i).longValue();
}
return result;
}
}
/** 0x7ff8000000000000 encoded in little endian order */
private static final byte[] JAVA_NAN_BYTES = HexRead.readFromString("00 00 00 00 00 00 F8 7F");
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, 3000);
sheet.setColumnWidth(1, 6000);
sheet.setColumnWidth(2, 6000);
sheet.setColumnWidth(3, 6000);
sheet.setColumnWidth(4, 6000);
sheet.setColumnWidth(5, 1600);
sheet.setColumnWidth(6, 20000);
HSSFRow row = sheet.createRow(0);
HSSFCellStyle style = wb.createCellStyle();
HSSFFont font = wb.createFont();
font.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD);
style.setFont(font);
writeHeaderCell(row, 0, "Value", style);
writeHeaderCell(row, 1, "Raw Long Bits", style);
writeHeaderCell(row, 2, "JDK Double Rendering", style);
writeHeaderCell(row, 3, "Actual Rendering", style);
writeHeaderCell(row, 4, "Expected Rendering", style);
writeHeaderCell(row, 5, "Match", style);
writeHeaderCell(row, 6, "Java Metadata", style);
}
static void writeDataRow(HSSFSheet sheet, int rowIx, long rawLongBits, String expectedExcelRendering) {
double d = Double.longBitsToDouble(rawLongBits);
HSSFRow row = sheet.createRow(rowIx);
int rowNum = rowIx + 1;
String cel0ref = "A" + rowNum;
String rawBitsText = formatLongAsHex(rawLongBits);
String jmExpr = "'ec(" + rawBitsText + ", ''\" & C" + rowNum + " & \"'', ''\" & D" + rowNum + " & \"''),'";
// The 'Match' column will contain 'OK' if the metadata (from NumberToTextConversionExamples)
// matches Excel's rendering.
String matchExpr = "if(D" + rowNum + "=E" + rowNum + ", \"OK\", \"ERROR\")";
row.createCell(0).setCellValue(d);
row.createCell(1).setCellValue(new HSSFRichTextString(rawBitsText));
row.createCell(2).setCellValue(new HSSFRichTextString(Double.toString(d)));
row.createCell(3).setCellFormula("\"\" & " + cel0ref);
row.createCell(4).setCellValue(new HSSFRichTextString(expectedExcelRendering));
row.createCell(5).setCellFormula(matchExpr);
row.createCell(6).setCellFormula(jmExpr.replaceAll("'", "\""));
if (false) {
// for observing arithmetic near numeric range boundaries
row.createCell(7).setCellFormula(cel0ref + " * 1.0001");
row.createCell(8).setCellFormula(cel0ref + " / 1.0001");
}
}
private static String formatLongAsHex(long l) {
StringBuilder sb = new StringBuilder(20);
sb.append(HexDump.longToHex(l)).append('L');
return sb.toString();
}
public static void main(String[] args) {
writeJavaDoc();
HSSFWorkbook wb = new HSSFWorkbook();
SheetWriter sw = new SheetWriter(wb);
ExampleConversion[] exampleValues = NumberToTextConversionExamples.getExampleConversions();
for (int i = 0; i < exampleValues.length; i++) {
ExampleConversion example = exampleValues[i];
sw.addTestRow(example.getRawDoubleBits(), example.getExcelRendering());
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
wb.write(baos);
} catch (IOException e) {
throw new RuntimeException(e);
}
byte[] fileContent = baos.toByteArray();
replaceNaNs(fileContent, sw.getReplacementNaNs());
File outputFile = new File("ExcelNumberRendering.xls");
try {
FileOutputStream os = new FileOutputStream(outputFile);
os.write(fileContent);
os.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("Finished writing '" + outputFile.getAbsolutePath() + "'");
}
public static void writeJavaDoc() {
ExampleConversion[] exampleConversions = NumberToTextConversionExamples.getExampleConversions();
for (int i = 0; i < exampleConversions.length; i++) {
ExampleConversion ec = exampleConversions[i];
String line = " * <tr><td>"
+ formatLongAsHex(ec.getRawDoubleBits())
+ "</td><td>" + Double.toString(ec.getDoubleValue())
+ "</td><td>" + ec.getExcelRendering() + "</td></tr>";
System.out.println(line);
}
}
private static void replaceNaNs(byte[] fileContent, long[] replacementNaNs) {
int countFound = 0;
for(int i=0; i<fileContent.length; i++) {
if(isNaNBytes(fileContent, i)) {
writeLong(fileContent, i, replacementNaNs[countFound]);
countFound++;
}
}
if (countFound < replacementNaNs.length) {
throw new RuntimeException("wrong repl count");
}
}
private static void writeLong(byte[] bb, int i, long val) {
String oldVal = interpretLong(bb, i);
bb[i+7] = (byte) (val >> 56);
bb[i+6] = (byte) (val >> 48);
bb[i+5] = (byte) (val >> 40);
bb[i+4] = (byte) (val >> 32);
bb[i+3] = (byte) (val >> 24);
bb[i+2] = (byte) (val >> 16);
bb[i+1] = (byte) (val >> 8);
bb[i+0] = (byte) (val >> 0);
if (false) {
String newVal = interpretLong(bb, i);
System.out.println("changed offset " + i + " from " + oldVal + " to " + newVal);
}
}
private static String interpretLong(byte[] fileContent, int offset) {
InputStream is = new ByteArrayInputStream(fileContent, offset, 8);
long l;
try {
l = new DataInputStream(is).readLong();
} catch (IOException e) {
throw new RuntimeException(e);
}
return "0x" + Long.toHexString(l).toUpperCase();
}
private static boolean isNaNBytes(byte[] fileContent, int offset) {
if(offset + JAVA_NAN_BYTES.length > fileContent.length) {
return false;
}
// excel NaN bits: 0xFFFF0420003C0000L
// java NaN bits :0x7ff8000000000000L
return areArraySectionsEqual(fileContent, offset, JAVA_NAN_BYTES);
}
private static boolean areArraySectionsEqual(byte[] bb, int off, byte[] section) {
for (int i=section.length-1; i>=0; i--) {
if (bb[off+i] != section[i]){
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,183 @@
/* ====================================================================
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;
/**
* Contains specific examples of <tt>double</tt> values and their rendering in Excel.
*
* @author Josh Micich
*/
final class NumberToTextConversionExamples {
private NumberToTextConversionExamples() {
// no instances of this class
}
public static final class ExampleConversion {
private final String _javaRendering;
private final String _excelRendering;
private final double _doubleValue;
private final long _rawDoubleBits;
ExampleConversion(long rawDoubleBits, String javaRendering, String excelRendering) {
double d = Double.longBitsToDouble(rawDoubleBits);
if ("NaN".equals(javaRendering)) {
if (!Double.isNaN(d)) {
throw new IllegalArgumentException("value must be NaN");
}
} else {
if (Double.isNaN(d)) {
throw new IllegalArgumentException("value must not be NaN");
}
// just to be dead sure test conversion in java both ways
boolean javaToStringOk = javaRendering.equals(Double.toString(d));
boolean javaParseOk = Double.parseDouble(javaRendering) == d;
if(!javaToStringOk || !javaParseOk) {
String msgA = "Specified rawDoubleBits " + doubleToHexString(d) + " encodes to double '" + d + "'.";
String msgB = "Specified javaRendering '" + javaRendering+ "' parses as double with rawDoubleBits "
+ doubleToHexString(Double.parseDouble(javaRendering));
System.err.println(msgA);
System.err.println(msgB);
throw new RuntimeException(msgA + msgB);
}
}
_rawDoubleBits = rawDoubleBits;
_javaRendering = javaRendering;
_excelRendering = excelRendering;
_doubleValue = d;
}
private static String doubleToHexString(double d) {
return "0x" + Long.toHexString(Double.doubleToLongBits(d)).toUpperCase() + "L";
}
public String getJavaRendering() {
return _javaRendering;
}
public String getExcelRendering() {
return _excelRendering;
}
public double getDoubleValue() {
return _doubleValue;
}
public boolean isNaN() {
return Double.isNaN(_doubleValue);
}
public long getRawDoubleBits() {
return _rawDoubleBits;
}
}
private static final ExampleConversion[] examples = {
// basic numbers
ec(0x0000000000000000L, "0.0", "0"),
ec(0x3FF0000000000000L, "1.0", "1"),
ec(0x3FF00068DB8BAC71L, "1.0001", "1.0001"),
ec(0x4087A00000000000L, "756.0", "756"),
ec(0x401E3D70A3D70A3DL, "7.56", "7.56"),
ec(0x405EDD3C07FB4C99L, "123.45678901234568", "123.456789012346"),
ec(0x4132D687E3DF2180L, "1234567.8901234567", "1234567.89012346"),
// small numbers
ec(0x3EE9E409302678BAL, "1.2345678901234568E-5", "1.23456789012346E-05"),
ec(0x3F202E85BE180B74L, "1.2345678901234567E-4", "0.000123456789012346"),
ec(0x3F543A272D9E0E51L, "0.0012345678901234567", "0.00123456789012346"),
ec(0x3F8948B0F90591E6L, "0.012345678901234568", "0.0123456789012346"),
ec(0x3EE9E409301B5A02L, "1.23456789E-5", "0.0000123456789"),
ec(0x3E6E7D05BDABDE50L, "5.6789012345E-8", "0.000000056789012345"),
ec(0x3E6E7D05BDAD407EL, "5.67890123456E-8", "5.67890123456E-08"),
ec(0x3E6E7D06029F18BEL, "5.678902E-8", "0.00000005678902"),
ec(0x2BCB5733CB32AE6EL, "9.999999999999123E-98", "9.99999999999912E-98"),
ec(0x2B617F7D4ED8C59EL, "1.0000000000001235E-99", "1.0000000000001E-99"),
ec(0x0036319916D67853L, "1.2345678901234578E-307", "1.2345678901235E-307"),
ec(0x359DEE7A4AD4B81FL, "2.0E-50", "2E-50"),
// large numbers
ec(0x41678C29DCD6E9E0L, "1.2345678901234567E7", "12345678.9012346"),
ec(0x42A674E79C5FE523L, "1.2345678901234568E13", "12345678901234.6"),
ec(0x42DC12218377DE6BL, "1.2345678901234567E14", "123456789012346"),
ec(0x43118B54F22AEB03L, "1.2345678901234568E15", "1234567890123460"),
ec(0x43E56A95319D63E1L, "1.2345678901234567E19", "12345678901234600000"),
ec(0x441AC53A7E04BCDAL, "1.2345678901234568E20", "1.23456789012346E+20"),
ec(0xC3E56A95319D63E1L, "-1.2345678901234567E19", "-12345678901234600000"),
ec(0xC41AC53A7E04BCDAL, "-1.2345678901234568E20", "-1.23456789012346E+20"),
ec(0x54820FE0BA17F46DL, "1.2345678901234577E99", "1.2345678901235E+99"),
ec(0x54B693D8E89DF188L, "1.2345678901234576E100", "1.2345678901235E+100"),
ec(0x4A611B0EC57E649AL, "2.0E50", "2E+50"),
// range extremities
ec(0x7FEFFFFFFFFFFFFFL, "1.7976931348623157E308", "1.7976931348623E+308"),
ec(0x0010000000000000L, "2.2250738585072014E-308", "2.2250738585072E-308"),
ec(0x000FFFFFFFFFFFFFL, "2.225073858507201E-308", "0"),
ec(0x0000000000000001L, "4.9E-324", "0"),
// infinity
ec(0x7FF0000000000000L, "Infinity", "1.7976931348623E+308"),
ec(0xFFF0000000000000L, "-Infinity", "1.7976931348623E+308"),
// shortening due to rounding
ec(0x441AC7A08EAD02F2L, "1.234999999999999E20", "1.235E+20"),
ec(0x40FE26BFFFFFFFF9L, "123499.9999999999", "123500"),
ec(0x3E4A857BFB2F2809L, "1.234999999999999E-8", "0.00000001235"),
ec(0x3BCD291DEF868C89L, "1.234999999999999E-20", "1.235E-20"),
// carry up due to rounding
// For clarity these tests choose values that don't round in java,
// but will round in excel. In some cases there is almost no difference
// between excel and java (e.g. 9.9..9E-8)
ec(0x444B1AE4D6E2EF4FL, "9.999999999999999E20", "1E+21"),
ec(0x412E847FFFFFFFFFL, "999999.9999999999", "1000000"),
ec(0x3E45798EE2308C39L, "9.999999999999999E-9", "0.00000001"),
ec(0x3C32725DD1D243ABL, "9.999999999999999E-19", "0.000000000000000001"),
ec(0x3BFD83C94FB6D2ABL, "9.999999999999999E-20", "1E-19"),
ec(0xC44B1AE4D6E2EF4FL, "-9.999999999999999E20", "-1E+21"),
ec(0xC12E847FFFFFFFFFL, "-999999.9999999999", "-1000000"),
ec(0xBE45798EE2308C39L, "-9.999999999999999E-9", "-0.00000001"),
ec(0xBC32725DD1D243ABL, "-9.999999999999999E-19", "-0.000000000000000001"),
ec(0xBBFD83C94FB6D2ABL, "-9.999999999999999E-20", "-1E-19"),
// NaNs
// Currently these test cases are not critical, since other limitations prevent any variety in
// or control of the bit patterns used to encode NaNs in evaluations.
ec(0xFFFF0420003C0000L, "NaN", "3.484840871308E+308"),
ec(0x7FF8000000000000L, "NaN", "2.6965397022935E+308"),
ec(0x7FFF0420003C0000L, "NaN", "3.484840871308E+308"),
ec(0xFFF8000000000000L, "NaN", "2.6965397022935E+308"),
ec(0xFFFF0AAAAAAAAAAAL, "NaN", "3.4877119413344E+308"),
ec(0x7FF80AAAAAAAAAAAL, "NaN", "2.7012211948322E+308"),
ec(0xFFFFFFFFFFFFFFFFL, "NaN", "3.5953862697246E+308"),
ec(0x7FFFFFFFFFFFFFFFL, "NaN", "3.5953862697246E+308"),
ec(0xFFF7FFFFFFFFFFFFL, "NaN", "2.6965397022935E+308"),
};
private static ExampleConversion ec(long rawDoubleBits, String javaRendering, String excelRendering) {
return new ExampleConversion(rawDoubleBits, javaRendering, excelRendering);
}
public static ExampleConversion[] getExampleConversions() {
return examples.clone();
}
}

View File

@ -0,0 +1,128 @@
/* ====================================================================
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.ComparisonFailure;
import junit.framework.TestCase;
import org.apache.poi.hssf.biff2.record.BIFF2RecordFactory.NumberRecord;
import org.apache.poi.hssf.record.FormulaRecord;
import org.apache.poi.hssf.record.constant.ConstantValueParser;
import org.apache.poi.hssf.record.formula.NumberPtg;
import org.apache.poi.ss.util.NumberToTextConverter;
import org.apache.poi.ss.util.NumberToTextConversionExamples.ExampleConversion;
/**
* Tests for {@link NumberToTextConverter}
*
* @author Josh Micich
*/
public final class TestNumberToTextConverter extends TestCase {
/**
* Confirms that <tt>ExcelNumberToTextConverter.toText(d)</tt> produces the right results.
* As part of preparing this test class, the <tt>ExampleConversion</tt> instances should be set
* up to contain the rendering as produced by Excel.
*/
public void testAll() {
int failureCount = 0;
ExampleConversion[] examples = NumberToTextConversionExamples.getExampleConversions();
for (int i = 0; i < examples.length; i++) {
ExampleConversion example = examples[i];
try {
if (example.isNaN()) {
confirmNaN(example.getRawDoubleBits(), example.getExcelRendering());
continue;
}
String actual = NumberToTextConverter.toText(example.getDoubleValue());
if (!example.getExcelRendering().equals(actual)) {
failureCount++;
String msg = "Error rendering for examples[" + i + "] "
+ formatExample(example) + " "
+ " bad-result='" + actual + "' "
+ new ComparisonFailure(null, example.getExcelRendering(), actual).getMessage();
System.err.println(msg);
continue;
}
} catch (RuntimeException e) {
failureCount++;
System.err.println("Error in excel rendering for examples[" + i + "] "
+ formatExample(example) + "':" + e.getMessage());
e.printStackTrace();
}
}
if (failureCount > 0) {
throw new AssertionFailedError(failureCount
+ " error(s) in excel number to text conversion (see std-err)");
}
}
private static String formatExample(ExampleConversion example) {
String hexLong = Long.toHexString(example.getRawDoubleBits()).toUpperCase();
String longRep = "0x" + "0000000000000000".substring(hexLong.length()) + hexLong+ "L";
return "ec(" + longRep + ", \"" + example.getJavaRendering() + "\", \"" + example.getExcelRendering() + "\")";
}
/**
* Excel's abnormal rendering of NaNs is both difficult to test and even reproduce in java. In
* general, Excel does not attempt to use raw NaN in the IEEE sense. In {@link FormulaRecord}s,
* Excel uses the NaN bit pattern to flag non-numeric (text, boolean, error) cached results.
* If the formula result actually evaluates to raw NaN, Excel transforms it to <i>#NUM!</i>.
* In other places (e.g. {@link NumberRecord}, {@link NumberPtg}, array items (via {@link
* ConstantValueParser}), there seems to be no special NaN translation scheme. If a NaN bit
* pattern is somehow encoded into any of these places Excel actually attempts to render the
* values as a plain number. That is the unusual functionality that this method is testing.<p/>
*
* There are multiple encodings (bit patterns) for NaN, and CPUs and applications can convert
* to a preferred NaN encoding (Java prefers <tt>0x7FF8000000000000L</tt>). Besides the
* special encoding in {@link FormulaRecord.SpecialCachedValue}, it is not known how/whether
* Excel attempts to encode NaN values.
*
* Observed NaN behaviour on HotSpot/Windows:
* <tt>Double.longBitsToDouble()</tt> will set one bit 51 (the NaN signaling flag) if it isn't
* already. <tt>Double.doubleToLongBits()</tt> will return a double with bit pattern
* <tt>0x7FF8000000000000L</tt> for any NaN bit pattern supplied.<br/>
* Differences are likely to be observed with other architectures.<p/>
*
* <p/>
* The few test case examples calling this method represent functionality which may not be
* important for POI to support.
*/
private void confirmNaN(long l, String excelRep) {
double d = Double.longBitsToDouble(l);
assertEquals("NaN", Double.toString(d));
String strExcel = NumberToTextConverter.rawDoubleBitsToText(l);
assertEquals(excelRep, strExcel);
}
public void testSimpleRendering_bug56156() {
double dResult = 0.05+0.01; // values chosen to produce rounding anomaly
String actualText = NumberToTextConverter.toText(dResult);
String jdkText = Double.toString(dResult);
if (jdkText.equals(actualText)) {
// "0.060000000000000005"
throw new AssertionFailedError("Should not use default JDK IEEE double rendering");
}
assertEquals("0.06", actualText);
}
}