From: Josh Micich Date: Fri, 12 Sep 2008 07:43:20 +0000 (+0000) Subject: Extended support for cached results of formula cells X-Git-Tag: REL_3_2_FINAL~63 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=21fa41ec23311c0d40f57407ba82282948a288d4;p=poi.git Extended support for cached results of formula cells git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@694631 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 02d75184a9..f789defe0d 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -37,6 +37,7 @@ + Extended support for cached results of formula cells 45639 - Fixed AIOOBE due to bad index logic in ColumnInfoRecordsAggregate Fixed special cases of INDEX function (single column/single row, errors) 45761 - Support for Very Hidden excel sheets in HSSF diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index dab203e98d..579816b5ba 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + Extended support for cached results of formula cells 45639 - Fixed AIOOBE due to bad index logic in ColumnInfoRecordsAggregate Fixed special cases of INDEX function (single column/single row, errors) 45761 - Support for Very Hidden excel sheets in HSSF diff --git a/src/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java b/src/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java index 8f3eebb2d3..2ea35c773e 100644 --- a/src/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java +++ b/src/java/org/apache/poi/hssf/extractor/EventBasedExcelExtractor.java @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ + package org.apache.poi.hssf.extractor; import java.io.IOException; @@ -49,10 +50,10 @@ import org.apache.poi.poifs.filesystem.POIFSFileSystem; /** * A text extractor for Excel files, that is based * on the hssf eventusermodel api. - * It will typically use less memory than + * It will typically use less memory than * {@link ExcelExtractor}, but may not provide * the same richness of formatting. - * Returns the textual content of the file, suitable for + * Returns the textual content of the file, suitable for * indexing by something like Lucene, but not really * intended for display to the user. * To turn an excel file into a CSV or similar, then see @@ -63,8 +64,8 @@ public class EventBasedExcelExtractor extends POIOLE2TextExtractor { private POIFSFileSystem fs; private boolean includeSheetNames = true; private boolean formulasNotResults = false; - - public EventBasedExcelExtractor(POIFSFileSystem fs) throws IOException { + + public EventBasedExcelExtractor(POIFSFileSystem fs) { super(null); this.fs = fs; } @@ -98,8 +99,8 @@ public class EventBasedExcelExtractor extends POIOLE2TextExtractor { public void setFormulasNotResults(boolean formulasNotResults) { this.formulasNotResults = formulasNotResults; } - - + + /** * Retreives the text contents of the file */ @@ -107,7 +108,7 @@ public class EventBasedExcelExtractor extends POIOLE2TextExtractor { String text = null; try { TextListener tl = triggerExtraction(); - + text = tl.text.toString(); if(! text.endsWith("\n")) { text = text + "\n"; @@ -115,37 +116,37 @@ public class EventBasedExcelExtractor extends POIOLE2TextExtractor { } catch(IOException e) { throw new RuntimeException(e); } - + return text; } - + private TextListener triggerExtraction() throws IOException { TextListener tl = new TextListener(); FormatTrackingHSSFListener ft = new FormatTrackingHSSFListener(tl); tl.ft = ft; - + // Register and process HSSFEventFactory factory = new HSSFEventFactory(); HSSFRequest request = new HSSFRequest(); request.addListenerForAllRecords(ft); - + factory.processWorkbookEvents(request, fs); - + return tl; } - + private class TextListener implements HSSFListener { private FormatTrackingHSSFListener ft; private SSTRecord sstRecord; - + private List sheetNames = new ArrayList(); private StringBuffer text = new StringBuffer(); private int sheetNum = -1; private int rowNum; - + private boolean outputNextStringValue = false; private int nextRow = -1; - + public void processRecord(Record record) { String thisText = null; int thisRow = -1; @@ -160,7 +161,7 @@ public class EventBasedExcelExtractor extends POIOLE2TextExtractor { if(bof.getType() == BOFRecord.TYPE_WORKSHEET) { sheetNum++; rowNum = -1; - + if(includeSheetNames) { if(text.length() > 0) text.append("\n"); text.append(sheetNames.get(sheetNum)); @@ -170,60 +171,60 @@ public class EventBasedExcelExtractor extends POIOLE2TextExtractor { case SSTRecord.sid: sstRecord = (SSTRecord)record; break; - - case FormulaRecord.sid: - FormulaRecord frec = (FormulaRecord) record; - thisRow = frec.getRow(); - - if(formulasNotResults) { - thisText = FormulaParser.toFormulaString(null, frec.getParsedExpression()); - } else { - if(Double.isNaN( frec.getValue() )) { - // Formula result is a string - // This is stored in the next record - outputNextStringValue = true; - nextRow = frec.getRow(); - } else { - thisText = formatNumberDateCell(frec, frec.getValue()); - } - } - break; - case StringRecord.sid: - if(outputNextStringValue) { - // String for formula - StringRecord srec = (StringRecord)record; - thisText = srec.getString(); - thisRow = nextRow; - outputNextStringValue = false; - } - break; - case LabelRecord.sid: - LabelRecord lrec = (LabelRecord) record; - thisRow = lrec.getRow(); - thisText = lrec.getValue(); - break; - case LabelSSTRecord.sid: - LabelSSTRecord lsrec = (LabelSSTRecord) record; - thisRow = lsrec.getRow(); - if(sstRecord == null) { - throw new IllegalStateException("No SST record found"); - } - thisText = sstRecord.getString(lsrec.getSSTIndex()).toString(); - break; - case NoteRecord.sid: - NoteRecord nrec = (NoteRecord) record; - thisRow = nrec.getRow(); - // TODO: Find object to match nrec.getShapeId() - break; - case NumberRecord.sid: - NumberRecord numrec = (NumberRecord) record; - thisRow = numrec.getRow(); - thisText = formatNumberDateCell(numrec, numrec.getValue()); - break; - default: - break; + + case FormulaRecord.sid: + FormulaRecord frec = (FormulaRecord) record; + thisRow = frec.getRow(); + + if(formulasNotResults) { + thisText = FormulaParser.toFormulaString(null, frec.getParsedExpression()); + } else { + if(frec.hasCachedResultString()) { + // Formula result is a string + // This is stored in the next record + outputNextStringValue = true; + nextRow = frec.getRow(); + } else { + thisText = formatNumberDateCell(frec, frec.getValue()); + } + } + break; + case StringRecord.sid: + if(outputNextStringValue) { + // String for formula + StringRecord srec = (StringRecord)record; + thisText = srec.getString(); + thisRow = nextRow; + outputNextStringValue = false; + } + break; + case LabelRecord.sid: + LabelRecord lrec = (LabelRecord) record; + thisRow = lrec.getRow(); + thisText = lrec.getValue(); + break; + case LabelSSTRecord.sid: + LabelSSTRecord lsrec = (LabelSSTRecord) record; + thisRow = lsrec.getRow(); + if(sstRecord == null) { + throw new IllegalStateException("No SST record found"); + } + thisText = sstRecord.getString(lsrec.getSSTIndex()).toString(); + break; + case NoteRecord.sid: + NoteRecord nrec = (NoteRecord) record; + thisRow = nrec.getRow(); + // TODO: Find object to match nrec.getShapeId() + break; + case NumberRecord.sid: + NumberRecord numrec = (NumberRecord) record; + thisRow = numrec.getRow(); + thisText = formatNumberDateCell(numrec, numrec.getValue()); + break; + default: + break; } - + if(thisText != null) { if(thisRow != rowNum) { rowNum = thisRow; @@ -235,42 +236,42 @@ public class EventBasedExcelExtractor extends POIOLE2TextExtractor { text.append(thisText); } } - + /** - * Formats a number or date cell, be that a real number, or the + * Formats a number or date cell, be that a real number, or the * answer to a formula */ private String formatNumberDateCell(CellValueRecordInterface cell, double value) { - // Get the built in format, if there is one + // Get the built in format, if there is one int formatIndex = ft.getFormatIndex(cell); String formatString = ft.getFormatString(cell); - + if(formatString == null) { - return Double.toString(value); - } else { - // Is it a date? - if(HSSFDateUtil.isADateFormat(formatIndex,formatString) && - HSSFDateUtil.isValidExcelDate(value)) { - // Java wants M not m for month - formatString = formatString.replace('m','M'); - // Change \- into -, if it's there - formatString = formatString.replaceAll("\\\\-","-"); - - // Format as a date - Date d = HSSFDateUtil.getJavaDate(value, false); - DateFormat df = new SimpleDateFormat(formatString); - return df.format(d); - } else { - if(formatString == "General") { - // Some sort of wierd default - return Double.toString(value); - } - - // Format as a number - DecimalFormat df = new DecimalFormat(formatString); - return df.format(value); - } - } + return Double.toString(value); + } else { + // Is it a date? + if(HSSFDateUtil.isADateFormat(formatIndex,formatString) && + HSSFDateUtil.isValidExcelDate(value)) { + // Java wants M not m for month + formatString = formatString.replace('m','M'); + // Change \- into -, if it's there + formatString = formatString.replaceAll("\\\\-","-"); + + // Format as a date + Date d = HSSFDateUtil.getJavaDate(value, false); + DateFormat df = new SimpleDateFormat(formatString); + return df.format(d); + } else { + if(formatString == "General") { + // Some sort of wierd default + return Double.toString(value); + } + + // Format as a number + DecimalFormat df = new DecimalFormat(formatString); + return df.format(value); + } + } } } } diff --git a/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java b/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java index c98757be51..d5dc30d00c 100644 --- a/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java +++ b/src/java/org/apache/poi/hssf/extractor/ExcelExtractor.java @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. ==================================================================== */ + package org.apache.poi.hssf.extractor; import java.io.IOException; import org.apache.poi.POIOLE2TextExtractor; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.usermodel.HeaderFooter; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFComment; -import org.apache.poi.hssf.usermodel.HSSFFooter; -import org.apache.poi.hssf.usermodel.HSSFHeader; import org.apache.poi.hssf.usermodel.HSSFRichTextString; import org.apache.poi.hssf.usermodel.HSSFRow; import org.apache.poi.hssf.usermodel.HSSFSheet; @@ -110,40 +110,52 @@ public class ExcelExtractor extends POIOLE2TextExtractor { int lastCell = row.getLastCellNum(); for(int k=firstCell;k 0) { - text.append(str.toString()); - } else { - // Try and treat it as a number - double val = cell.getNumericCellValue(); - text.append(val); + switch(cell.getCachedFormulaResultType()) { + case HSSFCell.CELL_TYPE_STRING: + HSSFRichTextString str = cell.getRichStringCellValue(); + if(str != null && str.length() > 0) { + text.append(str.toString()); + } + break; + case HSSFCell.CELL_TYPE_NUMERIC: + text.append(cell.getNumericCellValue()); + break; + case HSSFCell.CELL_TYPE_BOOLEAN: + text.append(cell.getBooleanCellValue()); + break; + case HSSFCell.CELL_TYPE_ERROR: + text.append(ErrorEval.getText(cell.getErrorCellValue())); + break; + } } - outputContents = true; break; + default: + throw new RuntimeException("Unexpected cell type (" + cell.getCellType() + ")"); } // Output the comment, if requested and exists diff --git a/src/java/org/apache/poi/hssf/record/FormulaRecord.java b/src/java/org/apache/poi/hssf/record/FormulaRecord.java index 46e8283dc2..b9616e0db7 100644 --- a/src/java/org/apache/poi/hssf/record/FormulaRecord.java +++ b/src/java/org/apache/poi/hssf/record/FormulaRecord.java @@ -18,6 +18,8 @@ package org.apache.poi.hssf.record; import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.util.BitField; import org.apache.poi.util.BitFieldFactory; import org.apache.poi.util.HexDump; @@ -32,264 +34,430 @@ import org.apache.poi.util.LittleEndian; */ public final class FormulaRecord extends Record implements CellValueRecordInterface { - public static final short sid = 0x0006; // docs say 406...because of a bug Microsoft support site article #Q184647) - private static int FIXED_SIZE = 22; - - private static final BitField alwaysCalc = BitFieldFactory.getInstance(0x0001); - private static final BitField calcOnLoad = BitFieldFactory.getInstance(0x0002); - private static final BitField sharedFormula = BitFieldFactory.getInstance(0x0008); - - private int field_1_row; - private short field_2_column; - private short field_3_xf; - private double field_4_value; - private short field_5_options; - private int field_6_zero; - private Ptg[] field_8_parsed_expr; - - /** - * Since the NaN support seems sketchy (different constants) we'll store and spit it out directly - */ - private byte[] value_data; - - /** Creates new FormulaRecord */ - - public FormulaRecord() { - field_8_parsed_expr = Ptg.EMPTY_PTG_ARRAY; - } - - /** - * Constructs a Formula record and sets its fields appropriately. - * Note - id must be 0x06 (NOT 0x406 see MSKB #Q184647 for an - * "explanation of this bug in the documentation) or an exception - * will be throw upon validation - * - * @param in the RecordInputstream to read the record from - */ - - public FormulaRecord(RecordInputStream in) { - super(in); - } - - protected void fillFields(RecordInputStream in) { - field_1_row = in.readUShort(); - field_2_column = in.readShort(); - field_3_xf = in.readShort(); - field_4_value = in.readDouble(); - field_5_options = in.readShort(); - - if (Double.isNaN(field_4_value)) { - value_data = in.getNANData(); - } - - field_6_zero = in.readInt(); - int field_7_expression_len = in.readShort(); // this length does not include any extra array data - field_8_parsed_expr = Ptg.readTokens(field_7_expression_len, in); - if (in.remaining() == 10) { - // TODO - this seems to occur when IntersectionPtg is present - // 10 extra bytes are just 0x01 and 0x00 - // This causes POI stderr: "WARN. Unread 10 bytes of record 0x6" - } - } - - public void setRow(int row) { - field_1_row = row; - } - - public void setColumn(short column) { - field_2_column = column; - } - - public void setXFIndex(short xf) { - field_3_xf = xf; - } - - /** - * set the calculated value of the formula - * - * @param value calculated value - */ - public void setValue(double value) { - field_4_value = value; - } - - /** - * set the option flags - * - * @param options bitmask - */ - public void setOptions(short options) { - field_5_options = options; - } - - public int getRow() { - return field_1_row; - } - - public short getColumn() { - return field_2_column; - } - - public short getXFIndex() { - return field_3_xf; - } - - /** - * get the calculated value of the formula - * - * @return calculated value - */ - public double getValue() { - return field_4_value; - } - - /** - * get the option flags - * - * @return bitmask - */ - public short getOptions() { - return field_5_options; - } - - public boolean isSharedFormula() { - return sharedFormula.isSet(field_5_options); - } - public void setSharedFormula(boolean flag) { - field_5_options = - sharedFormula.setShortBoolean(field_5_options, flag); - } - - public boolean isAlwaysCalc() { - return alwaysCalc.isSet(field_5_options); - } - public void setAlwaysCalc(boolean flag) { - field_5_options = - alwaysCalc.setShortBoolean(field_5_options, flag); - } - - public boolean isCalcOnLoad() { - return calcOnLoad.isSet(field_5_options); - } - public void setCalcOnLoad(boolean flag) { - field_5_options = - calcOnLoad.setShortBoolean(field_5_options, flag); - } - - /** - * @return the formula tokens. never null - */ - public Ptg[] getParsedExpression() { - return (Ptg[]) field_8_parsed_expr.clone(); - } - - public void setParsedExpression(Ptg[] ptgs) { - field_8_parsed_expr = ptgs; - } - - /** - * called by constructor, should throw runtime exception in the event of a - * record passed with a differing ID. - * - * @param id alleged id for this record - */ - protected void validateSid(short id) { - if (id != sid) { - throw new RecordFormatException("NOT A FORMULA RECORD"); - } - } - - public short getSid() { - return sid; - } - - private int getDataSize() { - return FIXED_SIZE + Ptg.getEncodedSize(field_8_parsed_expr); - } - public int serialize(int offset, byte [] data) { - - int dataSize = getDataSize(); - - LittleEndian.putShort(data, 0 + offset, sid); - LittleEndian.putUShort(data, 2 + offset, dataSize); - LittleEndian.putUShort(data, 4 + offset, getRow()); - LittleEndian.putShort(data, 6 + offset, getColumn()); - LittleEndian.putShort(data, 8 + offset, getXFIndex()); - - //only reserialize if the value is still NaN and we have old nan data - if (Double.isNaN(getValue()) && value_data != null) { - System.arraycopy(value_data,0,data,10 + offset,value_data.length); - } else { - LittleEndian.putDouble(data, 10 + offset, field_4_value); - } - - LittleEndian.putShort(data, 18 + offset, getOptions()); - - //when writing the chn field (offset 20), it's supposed to be 0 but ignored on read - //Microsoft Excel Developer's Kit Page 318 - LittleEndian.putInt(data, 20 + offset, 0); - int formulaTokensSize = Ptg.getEncodedSizeWithoutArrayData(field_8_parsed_expr); - LittleEndian.putUShort(data, 24 + offset, formulaTokensSize); - Ptg.serializePtgs(field_8_parsed_expr, data, 26+offset); - return 4 + dataSize; - } - - public int getRecordSize() { - return 4 + getDataSize(); - } - - public boolean isInValueSection() { - return true; - } - - public boolean isValue() { - return true; - } - - public String toString() { - - StringBuffer sb = new StringBuffer(); - sb.append("[FORMULA]\n"); - sb.append(" .row = ").append(HexDump.shortToHex(getRow())).append("\n"); - sb.append(" .column = ").append(HexDump.shortToHex(getColumn())).append("\n"); - sb.append(" .xf = ").append(HexDump.shortToHex(getXFIndex())).append("\n"); - sb.append(" .value = "); - if (Double.isNaN(this.getValue()) && value_data != null) { - sb.append("(NaN)").append(HexDump.dump(value_data,0,0)).append("\n"); - } else { - sb.append(getValue()).append("\n"); - } - sb.append(" .options = ").append(HexDump.shortToHex(getOptions())).append("\n"); - sb.append(" .alwaysCalc= ").append(alwaysCalc.isSet(getOptions())).append("\n"); - sb.append(" .calcOnLoad= ").append(calcOnLoad.isSet(getOptions())).append("\n"); - sb.append(" .shared = ").append(sharedFormula.isSet(getOptions())).append("\n"); - sb.append(" .zero = ").append(HexDump.intToHex(field_6_zero)).append("\n"); - - for (int k = 0; k < field_8_parsed_expr.length; k++ ) { - sb.append(" Ptg[").append(k).append("]="); - Ptg ptg = field_8_parsed_expr[k]; - sb.append(ptg.toString()).append(ptg.getRVAType()).append("\n"); - } - sb.append("[/FORMULA]\n"); - return sb.toString(); - } - - public Object clone() { - FormulaRecord rec = new FormulaRecord(); - rec.field_1_row = field_1_row; - rec.field_2_column = field_2_column; - rec.field_3_xf = field_3_xf; - rec.field_4_value = field_4_value; - rec.field_5_options = field_5_options; - rec.field_6_zero = field_6_zero; - int nTokens = field_8_parsed_expr.length; - Ptg[] ptgs = new Ptg[nTokens]; - for (int i=0; i< nTokens; i++) { - ptgs[i] = field_8_parsed_expr[i].copy(); - } - rec.field_8_parsed_expr = ptgs; - rec.value_data = value_data; - return rec; - } + public static final short sid = 0x0006; // docs say 406...because of a bug Microsoft support site article #Q184647) + private static int FIXED_SIZE = 22; + + private static final BitField alwaysCalc = BitFieldFactory.getInstance(0x0001); + private static final BitField calcOnLoad = BitFieldFactory.getInstance(0x0002); + private static final BitField sharedFormula = BitFieldFactory.getInstance(0x0008); + + /** + * Manages the cached formula result values of other types besides numeric. + * Excel encodes the same 8 bytes that would be field_4_value with various NaN + * values that are decoded/encoded by this class. + */ + private static final class SpecialCachedValue { + /** deliberately chosen by Excel in order to encode other values within Double NaNs */ + private static final long BIT_MARKER = 0xFFFF000000000000L; + private static final int VARIABLE_DATA_LENGTH = 6; + private static final int DATA_INDEX = 2; + + public static final int STRING = 0; + public static final int BOOLEAN = 1; + public static final int ERROR_CODE = 2; + public static final int EMPTY = 3; + + private final byte[] _variableData; + + private SpecialCachedValue(byte[] data) { + _variableData = data; + } + public int getTypeCode() { + return _variableData[0]; + } + + /** + * @return null if the double value encoded by valueLongBits + * is a normal (non NaN) double value. + */ + public static SpecialCachedValue create(long valueLongBits) { + if ((BIT_MARKER & valueLongBits) != BIT_MARKER) { + return null; + } + + byte[] result = new byte[VARIABLE_DATA_LENGTH]; + long x = valueLongBits; + for (int i=0; i>= 8; + } + switch (result[0]) { + case STRING: + case BOOLEAN: + case ERROR_CODE: + case EMPTY: + break; + default: + throw new RecordFormatException("Bad special value code (" + result[0] + ")"); + } + return new SpecialCachedValue(result); + } + public void serialize(byte[] data, int offset) { + System.arraycopy(_variableData, 0, data, offset, VARIABLE_DATA_LENGTH); + LittleEndian.putUShort(data, offset+VARIABLE_DATA_LENGTH, 0xFFFF); + } + public String formatDebugString() { + return formatValue() + ' ' + HexDump.toHex(_variableData); + } + private String formatValue() { + int typeCode = getTypeCode(); + switch (typeCode) { + case STRING: return ""; + case BOOLEAN: return getDataValue() == 0 ? "FALSE" : "TRUE"; + case ERROR_CODE: return ErrorEval.getText(getDataValue()); + case EMPTY: return ""; + } + return "#error(type=" + typeCode + ")#"; + } + private int getDataValue() { + return _variableData[DATA_INDEX]; + } + public static SpecialCachedValue createCachedEmptyValue() { + return create(EMPTY, 0); + } + public static SpecialCachedValue createForString() { + return create(STRING, 0); + } + public static SpecialCachedValue createCachedBoolean(boolean b) { + return create(BOOLEAN, b ? 0 : 1); + } + public static SpecialCachedValue createCachedErrorCode(int errorCode) { + return create(ERROR_CODE, errorCode); + } + private static SpecialCachedValue create(int code, int data) { + byte[] vd = { + (byte) code, + 0, + (byte) data, + 0, + 0, + 0, + }; + return new SpecialCachedValue(vd); + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()); + sb.append('[').append(formatValue()).append(']'); + return sb.toString(); + } + public int getValueType() { + int typeCode = getTypeCode(); + switch (typeCode) { + case STRING: return HSSFCell.CELL_TYPE_STRING; + case BOOLEAN: return HSSFCell.CELL_TYPE_BOOLEAN; + case ERROR_CODE: return HSSFCell.CELL_TYPE_ERROR; + case EMPTY: return HSSFCell.CELL_TYPE_STRING; // is this correct? + } + throw new IllegalStateException("Unexpected type id (" + typeCode + ")"); + } + public boolean getBooleanValue() { + if (getTypeCode() != BOOLEAN) { + throw new IllegalStateException("Not a boolean cached value - " + formatValue()); + } + return getDataValue() != 0; + } + public int getErrorValue() { + if (getTypeCode() != ERROR_CODE) { + throw new IllegalStateException("Not an error cached value - " + formatValue()); + } + return getDataValue(); + } + } + + + + private int field_1_row; + private short field_2_column; + private short field_3_xf; + private double field_4_value; + private short field_5_options; + private int field_6_zero; + private Ptg[] field_8_parsed_expr; + + /** + * Since the NaN support seems sketchy (different constants) we'll store and spit it out directly + */ + private SpecialCachedValue specialCachedValue; + + /** Creates new FormulaRecord */ + + public FormulaRecord() { + field_8_parsed_expr = Ptg.EMPTY_PTG_ARRAY; + } + + /** + * Constructs a Formula record and sets its fields appropriately. + * Note - id must be 0x06 (NOT 0x406 see MSKB #Q184647 for an + * "explanation of this bug in the documentation) or an exception + * will be throw upon validation + * + * @param in the RecordInputstream to read the record from + */ + + public FormulaRecord(RecordInputStream in) { + super(in); + } + + protected void fillFields(RecordInputStream in) { + field_1_row = in.readUShort(); + field_2_column = in.readShort(); + field_3_xf = in.readShort(); + long valueLongBits = in.readLong(); + field_5_options = in.readShort(); + specialCachedValue = SpecialCachedValue.create(valueLongBits); + if (specialCachedValue == null) { + field_4_value = Double.longBitsToDouble(valueLongBits); + } + + field_6_zero = in.readInt(); + int field_7_expression_len = in.readShort(); // this length does not include any extra array data + field_8_parsed_expr = Ptg.readTokens(field_7_expression_len, in); + if (in.remaining() == 10) { + // TODO - this seems to occur when IntersectionPtg is present + // 10 extra bytes are just 0x01 and 0x00 + // This causes POI stderr: "WARN. Unread 10 bytes of record 0x6" + } + } + + + public void setRow(int row) { + field_1_row = row; + } + + public void setColumn(short column) { + field_2_column = column; + } + + public void setXFIndex(short xf) { + field_3_xf = xf; + } + + /** + * set the calculated value of the formula + * + * @param value calculated value + */ + public void setValue(double value) { + field_4_value = value; + specialCachedValue = null; + } + + public void setCachedResultTypeEmptyString() { + specialCachedValue = SpecialCachedValue.createCachedEmptyValue(); + } + public void setCachedResultTypeString() { + specialCachedValue = SpecialCachedValue.createForString(); + } + public void setCachedResultErrorCode(int errorCode) { + specialCachedValue = SpecialCachedValue.createCachedErrorCode(errorCode); + } + public void setCachedResultBoolean(boolean value) { + specialCachedValue = SpecialCachedValue.createCachedBoolean(value); + } + /** + * @return true if this {@link FormulaRecord} is followed by a + * {@link StringRecord} representing the cached text result of the formula + * evaluation. + */ + public boolean hasCachedResultString() { + if (specialCachedValue == null) { + return false; + } + return specialCachedValue.getTypeCode() == SpecialCachedValue.STRING; + } + + public int getCachedResultType() { + if (specialCachedValue == null) { + return HSSFCell.CELL_TYPE_NUMERIC; + } + return specialCachedValue.getValueType(); + } + + public boolean getCachedBooleanValue() { + return specialCachedValue.getBooleanValue(); + } + public int getCachedErrorValue() { + return specialCachedValue.getErrorValue(); + } + + + /** + * set the option flags + * + * @param options bitmask + */ + public void setOptions(short options) { + field_5_options = options; + } + + public int getRow() { + return field_1_row; + } + + public short getColumn() { + return field_2_column; + } + + public short getXFIndex() { + return field_3_xf; + } + + /** + * get the calculated value of the formula + * + * @return calculated value + */ + public double getValue() { + return field_4_value; + } + + /** + * get the option flags + * + * @return bitmask + */ + public short getOptions() { + return field_5_options; + } + + public boolean isSharedFormula() { + return sharedFormula.isSet(field_5_options); + } + public void setSharedFormula(boolean flag) { + field_5_options = + sharedFormula.setShortBoolean(field_5_options, flag); + } + + public boolean isAlwaysCalc() { + return alwaysCalc.isSet(field_5_options); + } + public void setAlwaysCalc(boolean flag) { + field_5_options = + alwaysCalc.setShortBoolean(field_5_options, flag); + } + + public boolean isCalcOnLoad() { + return calcOnLoad.isSet(field_5_options); + } + public void setCalcOnLoad(boolean flag) { + field_5_options = + calcOnLoad.setShortBoolean(field_5_options, flag); + } + + /** + * @return the formula tokens. never null + */ + public Ptg[] getParsedExpression() { + return (Ptg[]) field_8_parsed_expr.clone(); + } + + public void setParsedExpression(Ptg[] ptgs) { + field_8_parsed_expr = ptgs; + } + + /** + * called by constructor, should throw runtime exception in the event of a + * record passed with a differing ID. + * + * @param id alleged id for this record + */ + protected void validateSid(short id) { + if (id != sid) { + throw new RecordFormatException("NOT A FORMULA RECORD"); + } + } + + public short getSid() { + return sid; + } + + private int getDataSize() { + return FIXED_SIZE + Ptg.getEncodedSize(field_8_parsed_expr); + } + public int serialize(int offset, byte [] data) { + + int dataSize = getDataSize(); + + LittleEndian.putShort(data, 0 + offset, sid); + LittleEndian.putUShort(data, 2 + offset, dataSize); + LittleEndian.putUShort(data, 4 + offset, getRow()); + LittleEndian.putShort(data, 6 + offset, getColumn()); + LittleEndian.putShort(data, 8 + offset, getXFIndex()); + + if (specialCachedValue == null) { + LittleEndian.putDouble(data, 10 + offset, field_4_value); + } else { + specialCachedValue.serialize(data, 10+offset); + } + + LittleEndian.putShort(data, 18 + offset, getOptions()); + + //when writing the chn field (offset 20), it's supposed to be 0 but ignored on read + //Microsoft Excel Developer's Kit Page 318 + LittleEndian.putInt(data, 20 + offset, 0); + int formulaTokensSize = Ptg.getEncodedSizeWithoutArrayData(field_8_parsed_expr); + LittleEndian.putUShort(data, 24 + offset, formulaTokensSize); + Ptg.serializePtgs(field_8_parsed_expr, data, 26+offset); + return 4 + dataSize; + } + + public int getRecordSize() { + return 4 + getDataSize(); + } + + public boolean isInValueSection() { + return true; + } + + public boolean isValue() { + return true; + } + + public String toString() { + + StringBuffer sb = new StringBuffer(); + sb.append("[FORMULA]\n"); + sb.append(" .row = ").append(HexDump.shortToHex(getRow())).append("\n"); + sb.append(" .column = ").append(HexDump.shortToHex(getColumn())).append("\n"); + sb.append(" .xf = ").append(HexDump.shortToHex(getXFIndex())).append("\n"); + sb.append(" .value = "); + if (specialCachedValue == null) { + sb.append(field_4_value).append("\n"); + } else { + sb.append(specialCachedValue.formatDebugString()).append("\n"); + } + sb.append(" .options = ").append(HexDump.shortToHex(getOptions())).append("\n"); + sb.append(" .alwaysCalc= ").append(alwaysCalc.isSet(getOptions())).append("\n"); + sb.append(" .calcOnLoad= ").append(calcOnLoad.isSet(getOptions())).append("\n"); + sb.append(" .shared = ").append(sharedFormula.isSet(getOptions())).append("\n"); + sb.append(" .zero = ").append(HexDump.intToHex(field_6_zero)).append("\n"); + + for (int k = 0; k < field_8_parsed_expr.length; k++ ) { + sb.append(" Ptg[").append(k).append("]="); + Ptg ptg = field_8_parsed_expr[k]; + sb.append(ptg.toString()).append(ptg.getRVAType()).append("\n"); + } + sb.append("[/FORMULA]\n"); + return sb.toString(); + } + + public Object clone() { + FormulaRecord rec = new FormulaRecord(); + rec.field_1_row = field_1_row; + rec.field_2_column = field_2_column; + rec.field_3_xf = field_3_xf; + rec.field_4_value = field_4_value; + rec.field_5_options = field_5_options; + rec.field_6_zero = field_6_zero; + int nTokens = field_8_parsed_expr.length; + Ptg[] ptgs = new Ptg[nTokens]; + for (int i = 0; i < nTokens; i++) { + ptgs[i] = field_8_parsed_expr[i].copy(); + } + rec.field_8_parsed_expr = ptgs; + rec.specialCachedValue = specialCachedValue; + return rec; + } } diff --git a/src/java/org/apache/poi/hssf/record/RecordInputStream.java b/src/java/org/apache/poi/hssf/record/RecordInputStream.java index 12c818b183..fe6a4b2ea3 100755 --- a/src/java/org/apache/poi/hssf/record/RecordInputStream.java +++ b/src/java/org/apache/poi/hssf/record/RecordInputStream.java @@ -209,30 +209,18 @@ public class RecordInputStream extends InputStream { return result; } - byte[] NAN_data = null; public double readDouble() { - checkRecordPosition(); - //Reset NAN data - NAN_data = null; - double result = LittleEndian.getDouble(data, recordOffset); - //Excel represents NAN in several ways, at this point in time we do not often - //know the sequence of bytes, so as a hack we store the NAN byte sequence - //so that it is not corrupted. + checkRecordPosition(); + long valueLongBits = LittleEndian.getLong(data, recordOffset); + double result = Double.longBitsToDouble(valueLongBits); if (Double.isNaN(result)) { - NAN_data = new byte[8]; - System.arraycopy(data, recordOffset, NAN_data, 0, 8); + throw new RuntimeException("Did not expect to read NaN"); } - recordOffset += LittleEndian.DOUBLE_SIZE; pos += LittleEndian.DOUBLE_SIZE; return result; } - - public byte[] getNANData() { - if (NAN_data == null) - throw new RecordFormatException("Do NOT call getNANData without calling readDouble that returns NaN"); - return NAN_data; - } + public short[] readShortArray() { checkRecordPosition(); @@ -276,9 +264,6 @@ public class RecordInputStream extends InputStream { } public String readCompressedUnicode(int length) { - if(length == 0) { - return ""; - } if ((length < 0) || ((remaining() < length) && !isContinueNext())) { throw new IllegalArgumentException("Illegal length " + length); } @@ -291,9 +276,7 @@ public class RecordInputStream extends InputStream { if(compressByte != 0) throw new IllegalArgumentException("compressByte in continue records must be 0 while reading compressed unicode"); } byte b = readByte(); - //Typecast direct to char from byte with high bit set causes all ones - //in the high byte of the char (which is of course incorrect) - char ch = (char)( (short)0xff & (short)b ); + char ch = (char)(0x00FF & b); // avoid sex buf.append(ch); } return buf.toString(); diff --git a/src/java/org/apache/poi/hssf/record/aggregates/FormulaRecordAggregate.java b/src/java/org/apache/poi/hssf/record/aggregates/FormulaRecordAggregate.java index 68d5f453dc..06bb53a38d 100644 --- a/src/java/org/apache/poi/hssf/record/aggregates/FormulaRecordAggregate.java +++ b/src/java/org/apache/poi/hssf/record/aggregates/FormulaRecordAggregate.java @@ -20,6 +20,7 @@ package org.apache.poi.hssf.record.aggregates; import org.apache.poi.hssf.record.CellValueRecordInterface; import org.apache.poi.hssf.record.FormulaRecord; import org.apache.poi.hssf.record.Record; +import org.apache.poi.hssf.record.RecordFormatException; import org.apache.poi.hssf.record.StringRecord; /** @@ -34,9 +35,9 @@ public final class FormulaRecordAggregate extends RecordAggregate implements Cel private SharedValueManager _sharedValueManager; /** caches the calculated result of the formula */ private StringRecord _stringRecord; - + /** - * @param stringRec may be null if this formula does not have a cached text + * @param stringRec may be null if this formula does not have a cached text * value. * @param svm the {@link SharedValueManager} for the current sheet */ @@ -44,6 +45,14 @@ public final class FormulaRecordAggregate extends RecordAggregate implements Cel if (svm == null) { throw new IllegalArgumentException("sfm must not be null"); } + boolean hasStringRec = stringRec != null; + boolean hasCachedStringFlag = formulaRec.hasCachedResultString(); + if (hasStringRec != hasCachedStringFlag) { + throw new RecordFormatException("String record was " + + (hasStringRec ? "": "not ") + " supplied but formula record flag is " + + (hasCachedStringFlag ? "" : "not ") + " set"); + } + if (formulaRec.isSharedFormula()) { svm.convertSharedFormulaRecord(formulaRec); } @@ -52,18 +61,18 @@ public final class FormulaRecordAggregate extends RecordAggregate implements Cel _stringRecord = stringRec; } - public void setStringRecord(StringRecord stringRecord) { - _stringRecord = stringRecord; - } - public FormulaRecord getFormulaRecord() { return _formulaRecord; } + /** + * debug only + * TODO - encapsulate + */ public StringRecord getStringRecord() { return _stringRecord; } - + public short getXFIndex() { return _formulaRecord.getXFIndex(); } @@ -91,7 +100,7 @@ public final class FormulaRecordAggregate extends RecordAggregate implements Cel public String toString() { return _formulaRecord.toString(); } - + public void visitContainedRecords(RecordVisitor rv) { rv.visitRecord(_formulaRecord); Record sharedFormulaRecord = _sharedValueManager.getRecordForFirstCell(_formulaRecord); @@ -102,11 +111,33 @@ public final class FormulaRecordAggregate extends RecordAggregate implements Cel rv.visitRecord(_stringRecord); } } - + public String getStringValue() { if(_stringRecord==null) { return null; } return _stringRecord.getString(); } + + public void setCachedStringResult(String value) { + + // Save the string into a String Record, creating one if required + if(_stringRecord == null) { + _stringRecord = new StringRecord(); + } + _stringRecord.setString(value); + if (value.length() < 1) { + _formulaRecord.setCachedResultTypeEmptyString(); + } else { + _formulaRecord.setCachedResultTypeString(); + } + } + public void setCachedBooleanResult(boolean value) { + _stringRecord = null; + _formulaRecord.setCachedResultBoolean(value); + } + public void setCachedErrorResult(int errorCode) { + _stringRecord = null; + _formulaRecord.setCachedResultErrorCode(errorCode); + } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java index c0f655bf51..98d7f5b48e 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java @@ -23,6 +23,7 @@ import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import org.apache.poi.hssf.model.FormulaParser; import org.apache.poi.hssf.model.Sheet; @@ -55,8 +56,8 @@ import org.apache.poi.hssf.record.formula.eval.ErrorEval; * Cells can be numeric, formula-based or string-based (text). The cell type * specifies this. String cells cannot conatin numbers and numeric cells cannot * contain strings (at least according to our model). Client apps should do the - * conversions themselves. Formula cells have the formula string, as well as - * the formula result, which can be numeric or string. + * conversions themselves. Formula cells have the formula string, as well as + * the formula result, which can be numeric or string. *

* Cells should have their number (0 based) before being added to a row. Only * cells that have values should be added. @@ -82,14 +83,15 @@ public final class HSSFCell { public final static int CELL_TYPE_BOOLEAN = 4; /** Error Cell type (5) @see #setCellType(int) @see #getCellType() */ public final static int CELL_TYPE_ERROR = 5; - + public final static short ENCODING_UNCHANGED = -1; public final static short ENCODING_COMPRESSED_UNICODE = 0; public final static short ENCODING_UTF_16 = 1; + + private final HSSFWorkbook book; + private final Sheet sheet; private int cellType; private HSSFRichTextString stringValue; - private HSSFWorkbook book; - private Sheet sheet; private CellValueRecordInterface record; private HSSFComment comment; @@ -122,6 +124,9 @@ public final class HSSFCell { short xfindex = sheet.getXFIndexForColAt(col); setCellType(CELL_TYPE_BLANK, false, row, col,xfindex); } + /* package */ Sheet getSheet() { + return sheet; + } /** * Creates new Cell - Should only be called by HSSFRow. This creates a cell @@ -144,7 +149,7 @@ public final class HSSFCell { stringValue = null; this.book = book; this.sheet = sheet; - + short xfindex = sheet.getXFIndexForColAt(col); setCellType(type,false,row,col,xfindex); } @@ -186,10 +191,10 @@ public final class HSSFCell { * used internally -- given a cell value record, figure out its type */ private static int determineType(CellValueRecordInterface cval) { - if (cval instanceof FormulaRecordAggregate) { - return HSSFCell.CELL_TYPE_FORMULA; - } - // all others are plain BIFF records + if (cval instanceof FormulaRecordAggregate) { + return HSSFCell.CELL_TYPE_FORMULA; + } + // all others are plain BIFF records Record record = ( Record ) cval; switch (record.getSid()) { @@ -205,13 +210,13 @@ public final class HSSFCell { } throw new RuntimeException("Bad cell value rec (" + cval.getClass().getName() + ")"); } - + /** * Returns the Workbook that this Cell is bound to * @return */ protected Workbook getBoundWorkbook() { - return book.getWorkbook(); + return book.getWorkbook(); } /** @@ -229,7 +234,7 @@ public final class HSSFCell { { record.setColumn(num); } - + /** * Updates the cell record's idea of what * column it belongs in (0 based) @@ -237,7 +242,7 @@ public final class HSSFCell { */ protected void updateCellNum(short num) { - record.setColumn(num); + record.setColumn(num); } /** @@ -417,14 +422,14 @@ public final class HSSFCell { errRec.setColumn(col); if (setValue) { - errRec.setValue(getErrorCellValue()); + errRec.setValue((byte)HSSFErrorConstants.ERROR_VALUE); } errRec.setXFIndex(styleIndex); errRec.setRow(row); record = errRec; break; } - if (cellType != this.cellType && + if (cellType != this.cellType && this.cellType!=-1 ) // Special Value to indicate an uninitialized Cell { sheet.replaceValueRecord(record); @@ -453,21 +458,20 @@ public final class HSSFCell { * precalculated value, for numerics we'll set its value. For other types we * will change the cell to a numeric cell and set its value. */ - public void setCellValue(double value) - { + public void setCellValue(double value) { int row=record.getRow(); short col=record.getColumn(); short styleIndex=record.getXFIndex(); - if ((cellType != CELL_TYPE_NUMERIC) && (cellType != CELL_TYPE_FORMULA)) - { - setCellType(CELL_TYPE_NUMERIC, false, row, col, styleIndex); - } - - // Save into the appropriate record - if(record instanceof FormulaRecordAggregate) { - (( FormulaRecordAggregate ) record).getFormulaRecord().setValue(value); - } else { - (( NumberRecord ) record).setValue(value); + + switch (cellType) { + default: + setCellType(CELL_TYPE_NUMERIC, false, row, col, styleIndex); + case CELL_TYPE_ERROR: + (( NumberRecord ) record).setValue(value); + break; + case CELL_TYPE_FORMULA: + ((FormulaRecordAggregate)record).getFormulaRecord().setValue(value); + break; } } @@ -487,7 +491,7 @@ public final class HSSFCell { /** * set a date value for the cell. Excel treats dates as numeric so you will need to format the cell as * a date. - * + * * This will set the cell value based on the Calendar's timezone. As Excel * does not support timezones this means that both 20:00+03:00 and * 20:00-03:00 will be reported as the same value (20:00) even that there @@ -539,31 +543,20 @@ public final class HSSFCell { return; } if (cellType == CELL_TYPE_FORMULA) { - // Set the 'pre-evaluated result' for the formula + // Set the 'pre-evaluated result' for the formula // note - formulas do not preserve text formatting. FormulaRecordAggregate fr = (FormulaRecordAggregate) record; - - // Save the string into a String Record, creating - // one if required - StringRecord sr = fr.getStringRecord(); - if(sr == null) { - // Wasn't a string before, need a new one - sr = new StringRecord(); - fr.setStringRecord(sr); - } - - // Save, loosing the formatting - sr.setString(value.getString()); + fr.setCachedStringResult(value.getString()); // Update our local cache to the un-formatted version - stringValue = new HSSFRichTextString(sr.getString()); - + stringValue = new HSSFRichTextString(value.getString()); + // All done return; } // If we get here, we're not dealing with a formula, // so handle things as a normal rich text cell - + if (cellType != CELL_TYPE_STRING) { setCellType(CELL_TYPE_STRING, false, row, col, styleIndex); } @@ -591,95 +584,95 @@ public final class HSSFCell { FormulaRecord frec = rec.getFormulaRecord(); frec.setOptions((short) 2); frec.setValue(0); - + //only set to default if there is no extended format index already set if (rec.getXFIndex() == (short)0) { - rec.setXFIndex((short) 0x0f); - } + rec.setXFIndex((short) 0x0f); + } Ptg[] ptgs = FormulaParser.parse(formula, book); frec.setParsedExpression(ptgs); } + /* package */ void setFormulaOnly(Ptg[] ptgs) { + if (ptgs == null) { + throw new IllegalArgumentException("ptgs must not be null"); + } + ((FormulaRecordAggregate)record).getFormulaRecord().setParsedExpression(ptgs); + } public String getCellFormula() { return FormulaParser.toFormulaString(book, ((FormulaRecordAggregate)record).getFormulaRecord().getParsedExpression()); } + /** + * Used to help format error messages + */ + private static String getCellTypeName(int cellTypeCode) { + switch (cellTypeCode) { + case CELL_TYPE_BLANK: return "blank"; + case CELL_TYPE_STRING: return "text"; + case CELL_TYPE_BOOLEAN: return "boolean"; + case CELL_TYPE_ERROR: return "error"; + case CELL_TYPE_NUMERIC: return "numeric"; + case CELL_TYPE_FORMULA: return "formula"; + } + return "#unknown cell type (" + cellTypeCode + ")#"; + } + + private static RuntimeException typeMismatch(int expectedTypeCode, int actualTypeCode, boolean isFormulaCell) { + String msg = "Cannot get a " + + getCellTypeName(expectedTypeCode) + " value from a " + + getCellTypeName(actualTypeCode) + " " + (isFormulaCell ? "formula " : "") + "cell"; + return new IllegalStateException(msg); + } + private static void checkFormulaCachedValueType(int expectedTypeCode, FormulaRecord fr) { + int cachedValueType = fr.getCachedResultType(); + if (cachedValueType != expectedTypeCode) { + throw typeMismatch(expectedTypeCode, cachedValueType, true); + } + } /** - * Get the value of the cell as a number. + * Get the value of the cell as a number. * For strings we throw an exception. * For blank cells we return a 0. * See {@link HSSFDataFormatter} for turning this * number into a string similar to that which - * Excel would render this number as. + * Excel would render this number as. */ - public double getNumericCellValue() - { - if (cellType == CELL_TYPE_BLANK) - { - return 0; - } - if (cellType == CELL_TYPE_STRING) - { - throw new NumberFormatException( - "You cannot get a numeric value from a String based cell"); - } - if (cellType == CELL_TYPE_BOOLEAN) - { - throw new NumberFormatException( - "You cannot get a numeric value from a boolean cell"); - } - if (cellType == CELL_TYPE_ERROR) - { - throw new NumberFormatException( - "You cannot get a numeric value from an error cell"); - } - if(cellType == CELL_TYPE_NUMERIC) - { - return ((NumberRecord)record).getValue(); - } - if(cellType == CELL_TYPE_FORMULA) - { - return ((FormulaRecordAggregate)record).getFormulaRecord().getValue(); + public double getNumericCellValue() { + + switch(cellType) { + case CELL_TYPE_BLANK: + return 0.0; + case CELL_TYPE_NUMERIC: + return ((NumberRecord)record).getValue(); + default: + throw typeMismatch(CELL_TYPE_NUMERIC, cellType, false); + case CELL_TYPE_FORMULA: + break; } - throw new NumberFormatException("Unknown Record Type in Cell:"+cellType); + FormulaRecord fr = ((FormulaRecordAggregate)record).getFormulaRecord(); + checkFormulaCachedValueType(CELL_TYPE_NUMERIC, fr); + return fr.getValue(); } /** - * Get the value of the cell as a date. + * Get the value of the cell as a date. * For strings we throw an exception. * For blank cells we return a null. * See {@link HSSFDataFormatter} for formatting * this date into a string similar to how excel does. */ - public Date getDateCellValue() - { - if (cellType == CELL_TYPE_BLANK) - { + public Date getDateCellValue() { + + if (cellType == CELL_TYPE_BLANK) { return null; } - if (cellType == CELL_TYPE_STRING) - { - throw new NumberFormatException( - "You cannot get a date value from a String based cell"); - } - if (cellType == CELL_TYPE_BOOLEAN) - { - throw new NumberFormatException( - "You cannot get a date value from a boolean cell"); - } - if (cellType == CELL_TYPE_ERROR) - { - throw new NumberFormatException( - "You cannot get a date value from an error cell"); - } - double value=this.getNumericCellValue(); + double value = getNumericCellValue(); if (book.getWorkbook().isUsing1904DateWindowing()) { - return HSSFDateUtil.getJavaDate(value,true); - } - else { - return HSSFDateUtil.getJavaDate(value,false); + return HSSFDateUtil.getJavaDate(value, true); } + return HSSFDateUtil.getJavaDate(value, false); } /** @@ -700,33 +693,22 @@ public final class HSSFCell { * For blank cells we return an empty string. * For formulaCells that are not string Formulas, we return empty String */ + public HSSFRichTextString getRichStringCellValue() { - public HSSFRichTextString getRichStringCellValue() - { - if (cellType == CELL_TYPE_BLANK) - { - return new HSSFRichTextString(""); - } - if (cellType == CELL_TYPE_NUMERIC) - { - throw new NumberFormatException( - "You cannot get a string value from a numeric cell"); - } - if (cellType == CELL_TYPE_BOOLEAN) - { - throw new NumberFormatException( - "You cannot get a string value from a boolean cell"); - } - if (cellType == CELL_TYPE_ERROR) - { - throw new NumberFormatException( - "You cannot get a string value from an error cell"); - } - if (cellType == CELL_TYPE_FORMULA) - { - if (stringValue==null) return new HSSFRichTextString(""); + switch(cellType) { + case CELL_TYPE_BLANK: + return new HSSFRichTextString(""); + case CELL_TYPE_STRING: + return stringValue; + default: + throw typeMismatch(CELL_TYPE_STRING, cellType, false); + case CELL_TYPE_FORMULA: + break; } - return stringValue; + FormulaRecordAggregate fra = ((FormulaRecordAggregate)record); + checkFormulaCachedValueType(CELL_TYPE_STRING, fra.getFormulaRecord()); + String strVal = fra.getStringValue(); + return new HSSFRichTextString(strVal == null ? "" : strVal); } /** @@ -737,47 +719,56 @@ public final class HSSFCell { * will change the cell to a boolean cell and set its value. */ - public void setCellValue(boolean value) - { + public void setCellValue(boolean value) { int row=record.getRow(); short col=record.getColumn(); short styleIndex=record.getXFIndex(); - if ((cellType != CELL_TYPE_BOOLEAN ) && ( cellType != CELL_TYPE_FORMULA)) - { - setCellType(CELL_TYPE_BOOLEAN, false, row, col, styleIndex); + + switch (cellType) { + default: + setCellType(CELL_TYPE_BOOLEAN, false, row, col, styleIndex); + case CELL_TYPE_ERROR: + (( BoolErrRecord ) record).setValue(value); + break; + case CELL_TYPE_FORMULA: + ((FormulaRecordAggregate)record).getFormulaRecord().setCachedResultBoolean(value); + break; } - (( BoolErrRecord ) record).setValue(value); } /** * set a error value for the cell * - * @param value the error value to set this cell to. For formulas we'll set the - * precalculated value ??? IS THIS RIGHT??? , for errors we'll set + * @param errorCode the error value to set this cell to. For formulas we'll set the + * precalculated value , for errors we'll set * its value. For other types we will change the cell to an error * cell and set its value. */ - - public void setCellErrorValue(byte value) - { + public void setCellErrorValue(byte errorCode) { int row=record.getRow(); short col=record.getColumn(); short styleIndex=record.getXFIndex(); - if (cellType != CELL_TYPE_ERROR) { - setCellType(CELL_TYPE_ERROR, false, row, col, styleIndex); + switch (cellType) { + default: + setCellType(CELL_TYPE_ERROR, false, row, col, styleIndex); + case CELL_TYPE_ERROR: + (( BoolErrRecord ) record).setValue(errorCode); + break; + case CELL_TYPE_FORMULA: + ((FormulaRecordAggregate)record).getFormulaRecord().setCachedResultErrorCode(errorCode); + break; } - (( BoolErrRecord ) record).setValue(value); } /** * Chooses a new boolean value for the cell when its type is changing.

- * - * Usually the caller is calling setCellType() with the intention of calling + * + * Usually the caller is calling setCellType() with the intention of calling * setCellValue(boolean) straight afterwards. This method only exists to give * the cell a somewhat reasonable value until the setCellValue() call (if at all). * TODO - perhaps a method like setCellTypeAndValue(int, Object) should be introduced to avoid this */ private boolean convertCellValueToBoolean() { - + switch (cellType) { case CELL_TYPE_BOOLEAN: return (( BoolErrRecord ) record).getBooleanValue(); @@ -788,11 +779,11 @@ public final class HSSFCell { // All other cases convert to false // These choices are not well justified. - case CELL_TYPE_FORMULA: + case CELL_TYPE_FORMULA: // should really evaluate, but HSSFCell can't call HSSFFormulaEvaluator case CELL_TYPE_ERROR: case CELL_TYPE_BLANK: - return false; + return false; } throw new RuntimeException("Unexpected cell type (" + cellType + ")"); } @@ -801,38 +792,39 @@ public final class HSSFCell { * get the value of the cell as a boolean. For strings, numbers, and errors, we throw an exception. * For blank cells we return a false. */ + public boolean getBooleanCellValue() { - public boolean getBooleanCellValue() - { - if (cellType == CELL_TYPE_BOOLEAN) - { - return (( BoolErrRecord ) record).getBooleanValue(); - } - if (cellType == CELL_TYPE_BLANK) - { - return false; + switch(cellType) { + case CELL_TYPE_BLANK: + return false; + case CELL_TYPE_BOOLEAN: + return (( BoolErrRecord ) record).getBooleanValue(); + default: + throw typeMismatch(CELL_TYPE_BOOLEAN, cellType, false); + case CELL_TYPE_FORMULA: + break; } - throw new NumberFormatException( - "You cannot get a boolean value from a non-boolean cell"); + FormulaRecord fr = ((FormulaRecordAggregate)record).getFormulaRecord(); + checkFormulaCachedValueType(CELL_TYPE_BOOLEAN, fr); + return fr.getCachedBooleanValue(); } /** * get the value of the cell as an error code. For strings, numbers, and booleans, we throw an exception. * For blank cells we return a 0. */ - - public byte getErrorCellValue() - { - if (cellType == CELL_TYPE_ERROR) - { - return (( BoolErrRecord ) record).getErrorValue(); - } - if (cellType == CELL_TYPE_BLANK) - { - return ( byte ) 0; + public byte getErrorCellValue() { + switch(cellType) { + case CELL_TYPE_ERROR: + return (( BoolErrRecord ) record).getErrorValue(); + default: + throw typeMismatch(CELL_TYPE_ERROR, cellType, false); + case CELL_TYPE_FORMULA: + break; } - throw new NumberFormatException( - "You cannot get an error value from a non-error cell"); + FormulaRecord fr = ((FormulaRecordAggregate)record).getFormulaRecord(); + checkFormulaCachedValueType(CELL_TYPE_ERROR, fr); + return (byte) fr.getCachedErrorValue(); } /** @@ -888,7 +880,7 @@ public final class HSSFCell { throw new RuntimeException("You cannot reference columns with an index of less then 0."); } } - + /** * Sets this cell as the active cell for the worksheet */ @@ -899,42 +891,42 @@ public final class HSSFCell { this.sheet.setActiveCellRow(row); this.sheet.setActiveCellCol(col); } - + /** * Returns a string representation of the cell - * - * This method returns a simple representation, + * + * This method returns a simple representation, * anthing more complex should be in user code, with - * knowledge of the semantics of the sheet being processed. - * - * Formula cells return the formula string, - * rather than the formula result. + * knowledge of the semantics of the sheet being processed. + * + * Formula cells return the formula string, + * rather than the formula result. * Dates are displayed in dd-MMM-yyyy format * Errors are displayed as #ERR<errIdx> */ public String toString() { - switch (getCellType()) { - case CELL_TYPE_BLANK: - return ""; - case CELL_TYPE_BOOLEAN: - return getBooleanCellValue()?"TRUE":"FALSE"; - case CELL_TYPE_ERROR: - return ErrorEval.getText((( BoolErrRecord ) record).getErrorValue()); - case CELL_TYPE_FORMULA: - return getCellFormula(); - case CELL_TYPE_NUMERIC: - //TODO apply the dataformat for this cell - if (HSSFDateUtil.isCellDateFormatted(this)) { - DateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy"); - return sdf.format(getDateCellValue()); - } else { - return getNumericCellValue() + ""; - } - case CELL_TYPE_STRING: - return getStringCellValue(); - default: - return "Unknown Cell Type: " + getCellType(); - } + switch (getCellType()) { + case CELL_TYPE_BLANK: + return ""; + case CELL_TYPE_BOOLEAN: + return getBooleanCellValue()?"TRUE":"FALSE"; + case CELL_TYPE_ERROR: + return ErrorEval.getText((( BoolErrRecord ) record).getErrorValue()); + case CELL_TYPE_FORMULA: + return getCellFormula(); + case CELL_TYPE_NUMERIC: + //TODO apply the dataformat for this cell + if (HSSFDateUtil.isCellDateFormatted(this)) { + DateFormat sdf = new SimpleDateFormat("dd-MMM-yyyy"); + return sdf.format(getDateCellValue()); + } else { + return getNumericCellValue() + ""; + } + case CELL_TYPE_STRING: + return getStringCellValue(); + default: + return "Unknown Cell Type: " + getCellType(); + } } /** @@ -945,11 +937,11 @@ public final class HSSFCell { * @param comment comment associated with this cell */ public void setCellComment(HSSFComment comment){ - if(comment == null) { - removeCellComment(); - return; - } - + if(comment == null) { + removeCellComment(); + return; + } + comment.setRow((short)record.getRow()); comment.setColumn(record.getColumn()); this.comment = comment; @@ -966,7 +958,7 @@ public final class HSSFCell { } return comment; } - + /** * Removes the comment for this cell, if * there is one. @@ -974,40 +966,41 @@ public final class HSSFCell { * all comments after performing this action! */ public void removeCellComment() { - HSSFComment comment = findCellComment(sheet, record.getRow(), record.getColumn()); - this.comment = null; - - if(comment == null) { - // Nothing to do - return; - } - - // Zap the underlying NoteRecord - sheet.getRecords().remove(comment.getNoteRecord()); - - // If we have a TextObjectRecord, is should - // be proceeed by: - // MSODRAWING with container - // OBJ - // MSODRAWING with EscherTextboxRecord - if(comment.getTextObjectRecord() != null) { - TextObjectRecord txo = comment.getTextObjectRecord(); - int txoAt = sheet.getRecords().indexOf(txo); - - if(sheet.getRecords().get(txoAt-3) instanceof DrawingRecord && - sheet.getRecords().get(txoAt-2) instanceof ObjRecord && - sheet.getRecords().get(txoAt-1) instanceof DrawingRecord) { - // Zap these, in reverse order - sheet.getRecords().remove(txoAt-1); - sheet.getRecords().remove(txoAt-2); - sheet.getRecords().remove(txoAt-3); - } else { - throw new IllegalStateException("Found the wrong records before the TextObjectRecord, can't remove comment"); - } - - // Now remove the text record - sheet.getRecords().remove(txo); - } + HSSFComment comment = findCellComment(sheet, record.getRow(), record.getColumn()); + this.comment = null; + + if(comment == null) { + // Nothing to do + return; + } + + // Zap the underlying NoteRecord + List sheetRecords = sheet.getRecords(); + sheetRecords.remove(comment.getNoteRecord()); + + // If we have a TextObjectRecord, is should + // be proceeed by: + // MSODRAWING with container + // OBJ + // MSODRAWING with EscherTextboxRecord + if(comment.getTextObjectRecord() != null) { + TextObjectRecord txo = comment.getTextObjectRecord(); + int txoAt = sheetRecords.indexOf(txo); + + if(sheetRecords.get(txoAt-3) instanceof DrawingRecord && + sheetRecords.get(txoAt-2) instanceof ObjRecord && + sheetRecords.get(txoAt-1) instanceof DrawingRecord) { + // Zap these, in reverse order + sheetRecords.remove(txoAt-1); + sheetRecords.remove(txoAt-2); + sheetRecords.remove(txoAt-3); + } else { + throw new IllegalStateException("Found the wrong records before the TextObjectRecord, can't remove comment"); + } + + // Now remove the text record + sheetRecords.remove(txo); + } } /** @@ -1100,4 +1093,16 @@ public final class HSSFCell { int eofLoc = sheet.findFirstRecordLocBySid( EOFRecord.sid ); sheet.getRecords().add( eofLoc, link.record ); } + /** + * Only valid for formula cells + * @return one of ({@link #CELL_TYPE_NUMERIC}, {@link #CELL_TYPE_STRING}, + * {@link #CELL_TYPE_BOOLEAN}, {@link #CELL_TYPE_ERROR}) depending + * on the cached value of the formula + */ + public int getCachedFormulaResultType() { + if (this.cellType != CELL_TYPE_FORMULA) { + throw new IllegalStateException("Only formula cells have cached results"); + } + return ((FormulaRecordAggregate)record).getFormulaRecord().getCachedResultType(); + } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java b/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java index 3480dbe6a3..f097d562a3 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFSheet.java @@ -1295,9 +1295,7 @@ public final class HSSFSheet { // If any references were changed, then // re-create the formula string if(changed) { - c.setCellFormula( - FormulaParser.toFormulaString(workbook, ptgs) - ); + c.setFormulaOnly(ptgs); } } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java index 3dd31d7a04..a708bd16c1 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFWorkbook.java @@ -660,6 +660,16 @@ public class HSSFWorkbook extends POIDocument return -1; } + /* package */ int findSheetIndex(Sheet sheet) { + for(int i=0; i<_sheets.size(); i++) { + HSSFSheet hSheet = (HSSFSheet) _sheets.get(i); + if(hSheet.getSheet() == sheet) { + return i; + } + } + throw new IllegalArgumentException("Specified sheet not found in this workbook"); + } + /** * Returns the external sheet index of the sheet * with the given internal index, creating one diff --git a/src/testcases/org/apache/poi/hssf/model/TestSheet.java b/src/testcases/org/apache/poi/hssf/model/TestSheet.java index 8321774f32..3bc79299b5 100644 --- a/src/testcases/org/apache/poi/hssf/model/TestSheet.java +++ b/src/testcases/org/apache/poi/hssf/model/TestSheet.java @@ -214,7 +214,9 @@ public final class TestSheet extends TestCase { records.add(new DimensionsRecord()); records.add(new RowRecord(0)); records.add(new RowRecord(1)); - records.add(new FormulaRecord()); + FormulaRecord formulaRecord = new FormulaRecord(); + formulaRecord.setCachedResultTypeString(); + records.add(formulaRecord); records.add(new StringRecord()); records.add(new RowRecord(2)); records.add(createWindow2Record()); diff --git a/src/testcases/org/apache/poi/hssf/record/TestFormulaRecord.java b/src/testcases/org/apache/poi/hssf/record/TestFormulaRecord.java index 0d96d737cf..4696539f23 100644 --- a/src/testcases/org/apache/poi/hssf/record/TestFormulaRecord.java +++ b/src/testcases/org/apache/poi/hssf/record/TestFormulaRecord.java @@ -26,12 +26,14 @@ import org.apache.poi.hssf.record.formula.FuncVarPtg; import org.apache.poi.hssf.record.formula.IntPtg; import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.RefPtg; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFErrorConstants; /** * Tests the serialization and deserialization of the FormulaRecord - * class works correctly. + * class works correctly. * - * @author Andrew C. Oliver + * @author Andrew C. Oliver */ public final class TestFormulaRecord extends TestCase { @@ -40,52 +42,66 @@ public final class TestFormulaRecord extends TestCase { record.setColumn((short)0); record.setRow(1); record.setXFIndex((short)4); - + assertEquals(record.getColumn(),0); assertEquals(record.getRow(), 1); assertEquals(record.getXFIndex(),4); } - + /** * Make sure a NAN value is preserved - * This formula record is a representation of =1/0 at row 0, column 0 + * This formula record is a representation of =1/0 at row 0, column 0 */ public void testCheckNanPreserve() { - byte[] formulaByte = new byte[29]; - - formulaByte[4] = (byte)0x0F; - formulaByte[6] = (byte)0x02; - formulaByte[8] = (byte)0x07; - formulaByte[12] = (byte)0xFF; - formulaByte[13] = (byte)0xFF; - formulaByte[18] = (byte)0xE0; - formulaByte[19] = (byte)0xFC; - formulaByte[20] = (byte)0x07; - formulaByte[22] = (byte)0x1E; - formulaByte[23] = (byte)0x01; - formulaByte[25] = (byte)0x1E; - formulaByte[28] = (byte)0x06; - + byte[] formulaByte = { + 0, 0, 0, 0, + 0x0F, 0x00, + + // 8 bytes cached number is a 'special value' in this case + 0x02, // special cached value type 'error' + 0x00, + HSSFErrorConstants.ERROR_DIV_0, + 0x00, + 0x00, + 0x00, + (byte)0xFF, + (byte)0xFF, + + 0x00, + 0x00, + 0x00, + 0x00, + + (byte)0xE0, //18 + (byte)0xFC, + // Ptgs + 0x07, 0x00, // encoded length + 0x1E, 0x01, 0x00, // IntPtg(1) + 0x1E, 0x00, 0x00, // IntPtg(0) + 0x06, // DividePtg + + }; + FormulaRecord record = new FormulaRecord(new TestcaseRecordInputStream(FormulaRecord.sid, (short)29, formulaByte)); assertEquals("Row", 0, record.getRow()); - assertEquals("Column", 0, record.getColumn()); - assertTrue("Value is not NaN", Double.isNaN(record.getValue())); - + assertEquals("Column", 0, record.getColumn()); + assertEquals(HSSFCell.CELL_TYPE_ERROR, record.getCachedResultType()); + byte[] output = record.serialize(); assertEquals("Output size", 33, output.length); //includes sid+recordlength - + for (int i = 5; i < 13;i++) { assertEquals("FormulaByte NaN doesn't match", formulaByte[i], output[i+4]); } } - + /** * Tests to see if the shared formula cells properly reserialize the expPtg * */ public void testExpFormula() { byte[] formulaByte = new byte[27]; - + formulaByte[4] =(byte)0x0F; formulaByte[14]=(byte)0x08; formulaByte[18]=(byte)0xE0; @@ -99,13 +115,13 @@ public final class TestFormulaRecord extends TestCase { assertEquals("Output size", 31, output.length); //includes sid+recordlength assertEquals("Offset 22", 1, output[26]); } - + public void testWithConcat() { // =CHOOSE(2,A2,A3,A4) byte[] data = { 6, 0, 68, 0, 1, 0, 1, 0, 15, 0, 0, 0, 0, 0, 0, 0, 57, - 64, 0, 0, 12, 0, 12, -4, 46, 0, + 64, 0, 0, 12, 0, 12, -4, 46, 0, 30, 2, 0, // Int - 2 25, 4, 3, 0, // Attr 8, 0, 17, 0, 26, 0, // jumpTable @@ -115,14 +131,14 @@ public final class TestFormulaRecord extends TestCase { 36, 2, 0, 0, -64, // Ref - A3 25, 8, 12, 0, // Attr 36, 3, 0, 0, -64, // Ref - A4 - 25, 8, 3, 0, // Attr + 25, 8, 3, 0, // Attr 66, 4, 100, 0 // CHOOSE }; RecordInputStream inp = new RecordInputStream( new ByteArrayInputStream(data)); inp.nextRecord(); - + FormulaRecord fr = new FormulaRecord(inp); - + Ptg[] ptgs = fr.getParsedExpression(); assertEquals(9, ptgs.length); assertEquals(IntPtg.class, ptgs[0].getClass()); @@ -134,7 +150,7 @@ public final class TestFormulaRecord extends TestCase { assertEquals(RefPtg.class, ptgs[6].getClass()); assertEquals(AttrPtg.class, ptgs[7].getClass()); assertEquals(FuncVarPtg.class, ptgs[8].getClass()); - + FuncVarPtg choose = (FuncVarPtg)ptgs[8]; assertEquals("CHOOSE", choose.getName()); } diff --git a/src/testcases/org/apache/poi/hssf/record/aggregates/TestFormulaRecordAggregate.java b/src/testcases/org/apache/poi/hssf/record/aggregates/TestFormulaRecordAggregate.java index 9b95356027..978f400fcd 100644 --- a/src/testcases/org/apache/poi/hssf/record/aggregates/TestFormulaRecordAggregate.java +++ b/src/testcases/org/apache/poi/hssf/record/aggregates/TestFormulaRecordAggregate.java @@ -30,6 +30,7 @@ public final class TestFormulaRecordAggregate extends TestCase { public void testBasic() throws Exception { FormulaRecord f = new FormulaRecord(); + f.setCachedResultTypeString(); StringRecord s = new StringRecord(); s.setString("abc"); FormulaRecordAggregate fagg = new FormulaRecordAggregate(f, s, SharedValueManager.EMPTY); diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java index 6e738adfb3..0f19a68e07 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestBugs.java @@ -961,7 +961,7 @@ public final class TestBugs extends TestCase { writeOutAndReadBack(wb); assertTrue("no errors writing sample xls", true); } - + /** * Problems with extracting check boxes from * HSSFObjectData @@ -973,35 +973,35 @@ public final class TestBugs extends TestCase { // Take a look at the embeded objects List objects = wb.getAllEmbeddedObjects(); assertEquals(1, objects.size()); - + HSSFObjectData obj = (HSSFObjectData)objects.get(0); assertNotNull(obj); - + // Peek inside the underlying record EmbeddedObjectRefSubRecord rec = obj.findObjectRecord(); assertNotNull(rec); - + assertEquals(32, rec.field_1_stream_id_offset); assertEquals(0, rec.field_6_stream_id); // WRONG! assertEquals("Forms.CheckBox.1", rec.field_5_ole_classname); assertEquals(12, rec.remainingBytes.length); - + // Doesn't have a directory assertFalse(obj.hasDirectoryEntry()); assertNotNull(obj.getObjectData()); assertEquals(12, obj.getObjectData().length); assertEquals("Forms.CheckBox.1", obj.getOLE2ClassName()); - + try { obj.getDirectory(); fail(); } catch(FileNotFoundException e) { - // expectd during successful test + // expectd during successful test } catch (IOException e) { - throw new RuntimeException(e); - } + throw new RuntimeException(e); + } } - + /** * Test that we can delete sheets without * breaking the build in named ranges @@ -1011,73 +1011,73 @@ public final class TestBugs extends TestCase { HSSFWorkbook wb = openSample("30978-alt.xls"); assertEquals(1, wb.getNumberOfNames()); assertEquals(3, wb.getNumberOfSheets()); - + // Check all names fit within range, and use // DeletedArea3DPtg Workbook w = wb.getWorkbook(); for(int i=0; i