diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 2d8a6b2c5..7dfc88688 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -67,6 +67,7 @@ Created a common interface for handling Excel files, irrespective of if they are .xls or .xlsx + 45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas Optimised the FormulaEvaluator to take cell dependencies into account 16936 - Initial support for whole-row cell styling Update hssf.extractor.ExcelExtractor to optionally output blank cells too diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index b25423c6f..09014b72c 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -64,6 +64,7 @@ Created a common interface for handling Excel files, irrespective of if they are .xls or .xlsx + 45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas Optimised the FormulaEvaluator to take cell dependencies into account 16936 - Initial support for whole-row cell styling Update hssf.extractor.ExcelExtractor to optionally output blank cells too diff --git a/src/java/org/apache/poi/hssf/model/LinkTable.java b/src/java/org/apache/poi/hssf/model/LinkTable.java index 2caab0fb5..998712e5e 100755 --- a/src/java/org/apache/poi/hssf/model/LinkTable.java +++ b/src/java/org/apache/poi/hssf/model/LinkTable.java @@ -29,6 +29,7 @@ import org.apache.poi.hssf.record.ExternalNameRecord; import org.apache.poi.hssf.record.NameRecord; import org.apache.poi.hssf.record.Record; import org.apache.poi.hssf.record.SupBookRecord; +import org.apache.poi.hssf.record.UnicodeString; import org.apache.poi.hssf.record.formula.NameXPtg; /** @@ -109,8 +110,8 @@ final class LinkTable { temp.toArray(_crnBlocks); } - public ExternalBookBlock(short numberOfSheets) { - _externalBookRecord = SupBookRecord.createInternalReferences(numberOfSheets); + public ExternalBookBlock(int numberOfSheets) { + _externalBookRecord = SupBookRecord.createInternalReferences((short)numberOfSheets); _externalNameRecords = new ExternalNameRecord[0]; _crnBlocks = new CRNBlock[0]; } @@ -197,7 +198,7 @@ final class LinkTable { return ExternSheetRecord.combine(esrs); } - public LinkTable(short numberOfSheets, WorkbookRecordList workbookRecordList) { + public LinkTable(int numberOfSheets, WorkbookRecordList workbookRecordList) { _workbookRecordList = workbookRecordList; _definedNames = new ArrayList(); _externalBookBlocks = new ExternalBookBlock[] { @@ -303,8 +304,62 @@ final class LinkTable { return lastName.getSheetNumber() == firstName.getSheetNumber(); } - - public int getIndexToSheet(int extRefIndex) { + public String[] getExternalBookAndSheetName(int extRefIndex) { + int ebIx = _externSheetRecord.getExtbookIndexFromRefIndex(extRefIndex); + SupBookRecord ebr = _externalBookBlocks[ebIx].getExternalBookRecord(); + if (!ebr.isExternalReferences()) { + return null; + } + int shIx = _externSheetRecord.getFirstSheetIndexFromRefIndex(extRefIndex); + UnicodeString usSheetName = ebr.getSheetNames()[shIx]; + return new String[] { + ebr.getURL(), + usSheetName.getString(), + }; + } + + public int getExternalSheetIndex(String workbookName, String sheetName) { + SupBookRecord ebrTarget = null; + int externalBookIndex = -1; + for (int i=0; i<_externalBookBlocks.length; i++) { + SupBookRecord ebr = _externalBookBlocks[i].getExternalBookRecord(); + if (!ebr.isExternalReferences()) { + continue; + } + if (workbookName.equals(ebr.getURL())) { // not sure if 'equals()' works when url has a directory + ebrTarget = ebr; + externalBookIndex = i; + break; + } + } + if (ebrTarget == null) { + throw new RuntimeException("No external workbook with name '" + workbookName + "'"); + } + int sheetIndex = getSheetIndex(ebrTarget.getSheetNames(), sheetName); + + int result = _externSheetRecord.getRefIxForSheet(externalBookIndex, sheetIndex); + if (result < 0) { + throw new RuntimeException("ExternSheetRecord does not contain combination (" + + externalBookIndex + ", " + sheetIndex + ")"); + } + return result; + } + + private static int getSheetIndex(UnicodeString[] sheetNames, String sheetName) { + for (int i = 0; i < sheetNames.length; i++) { + if (sheetNames[i].getString().equals(sheetName)) { + return i; + } + + } + throw new RuntimeException("External workbook does not contain sheet '" + sheetName + "'"); + } + + /** + * @param extRefIndex as from a {@link Ref3DPtg} or {@link Area3DPtg} + * @return -1 if the reference is to an external book + */ + public int getIndexToInternalSheet(int extRefIndex) { return _externSheetRecord.getFirstSheetIndexFromRefIndex(extRefIndex); } @@ -315,20 +370,26 @@ final class LinkTable { return _externSheetRecord.getFirstSheetIndexFromRefIndex(extRefIndex); } - public int addSheetIndexToExternSheet(int sheetNumber) { - // TODO - what about the first parameter (extBookIndex)? - return _externSheetRecord.addRef(0, sheetNumber, sheetNumber); - } - - public short checkExternSheet(int sheetIndex) { + public int checkExternSheet(int sheetIndex) { + int thisWbIndex = -1; // this is probably always zero + for (int i=0; i<_externalBookBlocks.length; i++) { + SupBookRecord ebr = _externalBookBlocks[i].getExternalBookRecord(); + if (ebr.isInternalReferences()) { + thisWbIndex = i; + break; + } + } + if (thisWbIndex < 0) { + throw new RuntimeException("Could not find 'internal references' EXTERNALBOOK"); + } //Trying to find reference to this sheet - int i = _externSheetRecord.getRefIxForSheet(sheetIndex); + int i = _externSheetRecord.getRefIxForSheet(thisWbIndex, sheetIndex); if (i>=0) { - return (short)i; + return i; } - //We Haven't found reference to this sheet - return (short)addSheetIndexToExternSheet((short) sheetIndex); + //We haven't found reference to this sheet + return _externSheetRecord.addRef(thisWbIndex, sheetIndex, sheetIndex); } diff --git a/src/java/org/apache/poi/hssf/model/Workbook.java b/src/java/org/apache/poi/hssf/model/Workbook.java index dbf7ecf7e..0728d0e29 100644 --- a/src/java/org/apache/poi/hssf/model/Workbook.java +++ b/src/java/org/apache/poi/hssf/model/Workbook.java @@ -26,6 +26,7 @@ import org.apache.poi.ddf.*; import org.apache.poi.hssf.record.*; import org.apache.poi.hssf.record.formula.NameXPtg; import org.apache.poi.hssf.util.HSSFColor; +import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; @@ -328,9 +329,9 @@ public final class Workbook implements Model { for ( int k = 0; k < nBoundSheets; k++ ) { BoundSheetRecord bsr = retval.createBoundSheet(k); - records.add(bsr); - retval.boundsheets.add(bsr); - retval.records.setBspos(records.size() - 1); + records.add(bsr); + retval.boundsheets.add(bsr); + retval.records.setBspos(records.size() - 1); } // retval.records.supbookpos = retval.records.bspos + 1; // retval.records.namepos = retval.records.supbookpos + 2; @@ -586,19 +587,19 @@ public final class Workbook implements Model { * @param hidden 0 for not hidden, 1 for hidden, 2 for very hidden */ public void setSheetHidden(int sheetnum, int hidden) { - BoundSheetRecord bsr = getBoundSheetRec(sheetnum); - boolean h = false; - boolean vh = false; - if(hidden == 0) { - } else if(hidden == 1) { - h = true; - } else if(hidden == 2) { - vh = true; - } else { - throw new IllegalArgumentException("Invalid hidden flag " + hidden + " given, must be 0, 1 or 2"); - } - bsr.setHidden(h); - bsr.setVeryHidden(vh); + BoundSheetRecord bsr = getBoundSheetRec(sheetnum); + boolean h = false; + boolean vh = false; + if(hidden == 0) { + } else if(hidden == 1) { + h = true; + } else if(hidden == 2) { + vh = true; + } else { + throw new IllegalArgumentException("Invalid hidden flag " + hidden + " given, must be 0, 1 or 2"); + } + bsr.setHidden(h); + bsr.setVeryHidden(vh); } @@ -761,23 +762,23 @@ public final class Workbook implements Model { * have a Style set. */ public StyleRecord getStyleRecord(int xfIndex) { - // Style records always follow after - // the ExtendedFormat records - boolean done = false; - for(int i=records.getXfpos(); i firstRow) { + setFirstRow(firstRow); + setLastRow(lastRow); + setFirstRowRelative(firstRowRelative); + setLastRowRelative(lastRowRelative); + } else { + setFirstRow(lastRow); + setLastRow(firstRow); + setFirstRowRelative(lastRowRelative); + setLastRowRelative(firstRowRelative); + } + + if (lastColumn > firstColumn) { + setFirstColumn(firstColumn); + setLastColumn(lastColumn); + setFirstColRelative(firstColRelative); + setLastColRelative(lastColRelative); + } else { + setFirstColumn(lastColumn); + setLastColumn(firstColumn); + setFirstColRelative(lastColRelative); + setLastColRelative(firstColRelative); + } + } - private static void checkColumnBounds(int colIx) { - if((colIx & 0x0FF) != colIx) { - throw new IllegalArgumentException("colIx (" + colIx + ") is out of range"); - } - } - private static void checkRowBounds(int rowIx) { - if((rowIx & 0x0FFFF) != rowIx) { - throw new IllegalArgumentException("rowIx (" + rowIx + ") is out of range"); - } - } + private static void checkColumnBounds(int colIx) { + if((colIx & 0x0FF) != colIx) { + throw new IllegalArgumentException("colIx (" + colIx + ") is out of range"); + } + } + private static void checkRowBounds(int rowIx) { + if((rowIx & 0x0FFFF) != rowIx) { + throw new IllegalArgumentException("rowIx (" + rowIx + ") is out of range"); + } + } - protected final void readCoordinates(RecordInputStream in) { - field_1_first_row = in.readUShort(); - field_2_last_row = in.readUShort(); - field_3_first_column = in.readUShort(); - field_4_last_column = in.readUShort(); - } - protected final void writeCoordinates(byte[] array, int offset) { - LittleEndian.putUShort(array, offset + 0, field_1_first_row); - LittleEndian.putUShort(array, offset + 2, field_2_last_row); - LittleEndian.putUShort(array, offset + 4, field_3_first_column); - LittleEndian.putUShort(array, offset + 6, field_4_last_column); - } + protected final void readCoordinates(RecordInputStream in) { + field_1_first_row = in.readUShort(); + field_2_last_row = in.readUShort(); + field_3_first_column = in.readUShort(); + field_4_last_column = in.readUShort(); + } + protected final void writeCoordinates(byte[] array, int offset) { + LittleEndian.putUShort(array, offset + 0, field_1_first_row); + LittleEndian.putUShort(array, offset + 2, field_2_last_row); + LittleEndian.putUShort(array, offset + 4, field_3_first_column); + LittleEndian.putUShort(array, offset + 6, field_4_last_column); + } - /** - * @return the first row in the area - */ - public final int getFirstRow() { - return field_1_first_row; - } + /** + * @return the first row in the area + */ + public final int getFirstRow() { + return field_1_first_row; + } - /** - * sets the first row - * @param rowIx number (0-based) - */ - public final void setFirstRow(int rowIx) { - checkRowBounds(rowIx); - field_1_first_row = rowIx; - } + /** + * sets the first row + * @param rowIx number (0-based) + */ + public final void setFirstRow(int rowIx) { + checkRowBounds(rowIx); + field_1_first_row = rowIx; + } - /** - * @return last row in the range (x2 in x1,y1-x2,y2) - */ - public final int getLastRow() { - return field_2_last_row; - } + /** + * @return last row in the range (x2 in x1,y1-x2,y2) + */ + public final int getLastRow() { + return field_2_last_row; + } - /** - * @param rowIx last row number in the area - */ - public final void setLastRow(int rowIx) { - checkRowBounds(rowIx); - field_2_last_row = rowIx; - } + /** + * @param rowIx last row number in the area + */ + public final void setLastRow(int rowIx) { + checkRowBounds(rowIx); + field_2_last_row = rowIx; + } - /** - * @return the first column number in the area. - */ - public final int getFirstColumn() { - return columnMask.getValue(field_3_first_column); - } + /** + * @return the first column number in the area. + */ + public final int getFirstColumn() { + return columnMask.getValue(field_3_first_column); + } - /** - * @return the first column number + the options bit settings unstripped - */ - public final short getFirstColumnRaw() { - return (short) field_3_first_column; // TODO - } + /** + * @return the first column number + the options bit settings unstripped + */ + public final short getFirstColumnRaw() { + return (short) field_3_first_column; // TODO + } - /** - * @return whether or not the first row is a relative reference or not. - */ - public final boolean isFirstRowRelative() { - return rowRelative.isSet(field_3_first_column); - } - - /** - * sets the first row to relative or not - * @param rel is relative or not. - */ - public final void setFirstRowRelative(boolean rel) { - field_3_first_column=rowRelative.setBoolean(field_3_first_column,rel); - } + /** + * @return whether or not the first row is a relative reference or not. + */ + public final boolean isFirstRowRelative() { + return rowRelative.isSet(field_3_first_column); + } + + /** + * sets the first row to relative or not + * @param rel is relative or not. + */ + public final void setFirstRowRelative(boolean rel) { + field_3_first_column=rowRelative.setBoolean(field_3_first_column,rel); + } - /** - * @return isrelative first column to relative or not - */ - public final boolean isFirstColRelative() { - return colRelative.isSet(field_3_first_column); - } - - /** - * set whether the first column is relative - */ - public final void setFirstColRelative(boolean rel) { - field_3_first_column=colRelative.setBoolean(field_3_first_column,rel); - } + /** + * @return isrelative first column to relative or not + */ + public final boolean isFirstColRelative() { + return colRelative.isSet(field_3_first_column); + } + + /** + * set whether the first column is relative + */ + public final void setFirstColRelative(boolean rel) { + field_3_first_column=colRelative.setBoolean(field_3_first_column,rel); + } - /** - * set the first column in the area - */ - public final void setFirstColumn(int colIx) { - checkColumnBounds(colIx); - field_3_first_column=columnMask.setValue(field_3_first_column, colIx); - } + /** + * set the first column in the area + */ + public final void setFirstColumn(int colIx) { + checkColumnBounds(colIx); + field_3_first_column=columnMask.setValue(field_3_first_column, colIx); + } - /** - * set the first column irrespective of the bitmasks - */ - public final void setFirstColumnRaw(int column) { - field_3_first_column = column; - } + /** + * set the first column irrespective of the bitmasks + */ + public final void setFirstColumnRaw(int column) { + field_3_first_column = column; + } - /** - * @return lastcolumn in the area - */ - public final int getLastColumn() { - return columnMask.getValue(field_4_last_column); - } + /** + * @return lastcolumn in the area + */ + public final int getLastColumn() { + return columnMask.getValue(field_4_last_column); + } - /** - * @return last column and bitmask (the raw field) - */ - public final short getLastColumnRaw() { - return (short) field_4_last_column; - } + /** + * @return last column and bitmask (the raw field) + */ + public final short getLastColumnRaw() { + return (short) field_4_last_column; + } - /** - * @return last row relative or not - */ - public final boolean isLastRowRelative() { - return rowRelative.isSet(field_4_last_column); - } - - /** - * set whether the last row is relative or not - * @param rel true if the last row relative, else - * false - */ - public final void setLastRowRelative(boolean rel) { - field_4_last_column=rowRelative.setBoolean(field_4_last_column,rel); - } + /** + * @return last row relative or not + */ + public final boolean isLastRowRelative() { + return rowRelative.isSet(field_4_last_column); + } + + /** + * set whether the last row is relative or not + * @param rel true if the last row relative, else + * false + */ + public final void setLastRowRelative(boolean rel) { + field_4_last_column=rowRelative.setBoolean(field_4_last_column,rel); + } - /** - * @return lastcol relative or not - */ - public final boolean isLastColRelative() { - return colRelative.isSet(field_4_last_column); - } - - /** - * set whether the last column should be relative or not - */ - public final void setLastColRelative(boolean rel) { - field_4_last_column=colRelative.setBoolean(field_4_last_column,rel); - } - - /** - * set the last column in the area - */ - public final void setLastColumn(int colIx) { - checkColumnBounds(colIx); - field_4_last_column=columnMask.setValue(field_4_last_column, colIx); - } + /** + * @return lastcol relative or not + */ + public final boolean isLastColRelative() { + return colRelative.isSet(field_4_last_column); + } + + /** + * set whether the last column should be relative or not + */ + public final void setLastColRelative(boolean rel) { + field_4_last_column=colRelative.setBoolean(field_4_last_column,rel); + } + + /** + * set the last column in the area + */ + public final void setLastColumn(int colIx) { + checkColumnBounds(colIx); + field_4_last_column=columnMask.setValue(field_4_last_column, colIx); + } - /** - * set the last column irrespective of the bitmasks - */ - public final void setLastColumnRaw(short column) { - field_4_last_column = column; - } - protected final String formatReferenceAsString() { - CellReference topLeft = new CellReference(getFirstRow(),getFirstColumn(),!isFirstRowRelative(),!isFirstColRelative()); - CellReference botRight = new CellReference(getLastRow(),getLastColumn(),!isLastRowRelative(),!isLastColRelative()); - - if(AreaReference.isWholeColumnReference(topLeft, botRight)) { - return (new AreaReference(topLeft, botRight)).formatAsString(); - } - return topLeft.formatAsString() + ":" + botRight.formatAsString(); - } - - public String toFormulaString() { - return formatReferenceAsString(); - } + /** + * set the last column irrespective of the bitmasks + */ + public final void setLastColumnRaw(short column) { + field_4_last_column = column; + } + protected final String formatReferenceAsString() { + CellReference topLeft = new CellReference(getFirstRow(),getFirstColumn(),!isFirstRowRelative(),!isFirstColRelative()); + CellReference botRight = new CellReference(getLastRow(),getLastColumn(),!isLastRowRelative(),!isLastColRelative()); + + if(AreaReference.isWholeColumnReference(topLeft, botRight)) { + return (new AreaReference(topLeft, botRight)).formatAsString(); + } + return topLeft.formatAsString() + ":" + botRight.formatAsString(); + } + + public String toFormulaString() { + return formatReferenceAsString(); + } - public byte getDefaultOperandClass() { - return Ptg.CLASS_REF; - } + public byte getDefaultOperandClass() { + return Ptg.CLASS_REF; + } } diff --git a/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java b/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java index 34558ebdf..701a7aee7 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java @@ -34,13 +34,13 @@ public final class AttrPtg extends ControlPtg { private final static int SIZE = 4; private byte field_1_options; private short field_2_data; - + /** only used for tAttrChoose: table of offsets to starts of args */ private final int[] _jumpTable; /** only used for tAttrChoose: offset to the tFuncVar for CHOOSE() */ private final int _chooseFuncOffset; - - // flags 'volatile' and 'space', can be combined. + + // flags 'volatile' and 'space', can be combined. // OOO spec says other combinations are theoretically possible but not likely to occur. private static final BitField semiVolatile = BitFieldFactory.getInstance(0x01); private static final BitField optiIf = BitFieldFactory.getInstance(0x02); @@ -49,12 +49,14 @@ public final class AttrPtg extends ControlPtg { private static final BitField sum = BitFieldFactory.getInstance(0x10); private static final BitField baxcel = BitFieldFactory.getInstance(0x20); // 'assignment-style formula in a macro sheet' private static final BitField space = BitFieldFactory.getInstance(0x40); - + + public static final AttrPtg SUM = new AttrPtg(0x0010, 0, null, -1); + public static final class SpaceType { private SpaceType() { // no instances of this class } - + /** 00H = Spaces before the next token (not allowed before tParen token) */ public static final int SPACE_BEFORE = 0x00; /** 01H = Carriage returns before the next token (not allowed before tParen token) */ @@ -75,7 +77,7 @@ public final class AttrPtg extends ControlPtg { _jumpTable = null; _chooseFuncOffset = -1; } - + public AttrPtg(RecordInputStream in) { field_1_options = in.readByte(); @@ -92,7 +94,7 @@ public final class AttrPtg extends ControlPtg { _jumpTable = null; _chooseFuncOffset = -1; } - + } private AttrPtg(int options, int data, int[] jt, int chooseFuncOffset) { field_1_options = (byte) options; @@ -100,7 +102,7 @@ public final class AttrPtg extends ControlPtg { _jumpTable = jt; _chooseFuncOffset = chooseFuncOffset; } - + /** * @param type a constant from SpaceType * @param count the number of space characters @@ -145,7 +147,7 @@ public final class AttrPtg extends ControlPtg { { return sum.isSet(getOptions()); } - + public void setSum(boolean bsum) { field_1_options=sum.setByteBoolean(field_1_options,bsum); } @@ -155,13 +157,13 @@ public final class AttrPtg extends ControlPtg { } /** - * Flags this ptg as a goto/jump + * Flags this ptg as a goto/jump * @param isGoto */ public void setGoto(boolean isGoto) { field_1_options=optGoto.setByteBoolean(field_1_options, isGoto); } - + // lets hope no one uses this anymore public boolean isBaxcel() { @@ -201,7 +203,7 @@ public final class AttrPtg extends ControlPtg { } else if(isOptimizedChoose()) { sb.append("choose nCases=").append(getData()); } else if(isGoto()) { - sb.append("skip dist=").append(getData()); + sb.append("skip dist=").append(getData()); } else if(isSum()) { sb.append("sum "); } else if(isBaxcel()) { @@ -218,7 +220,7 @@ public final class AttrPtg extends ControlPtg { LittleEndian.putShort(array,offset+2, field_2_data); int[] jt = _jumpTable; if (jt != null) { - int joff = offset+4; + int joff = offset+4; LittleEndian.putUShort(array, joff, _chooseFuncOffset); joff+=2; for (int i = 0; i < jt.length; i++) { @@ -227,7 +229,7 @@ public final class AttrPtg extends ControlPtg { } LittleEndian.putUShort(array, joff, _chooseFuncOffset); } - + } public int getSize() @@ -249,7 +251,7 @@ public final class AttrPtg extends ControlPtg { return toFormulaString() + "(" + operands[ 0 ] + ")"; } } - + public int getNumberOfOperands() { @@ -260,7 +262,7 @@ public final class AttrPtg extends ControlPtg { { return -1; } - + public String toFormulaString() { if(semiVolatile.isSet(field_1_options)) { return "ATTR(semiVolatile)"; diff --git a/src/java/org/apache/poi/hssf/record/formula/ExternSheetNameResolver.java b/src/java/org/apache/poi/hssf/record/formula/ExternSheetNameResolver.java index 412c110d3..dadbcb870 100644 --- a/src/java/org/apache/poi/hssf/record/formula/ExternSheetNameResolver.java +++ b/src/java/org/apache/poi/hssf/record/formula/ExternSheetNameResolver.java @@ -18,6 +18,7 @@ package org.apache.poi.hssf.record.formula; import org.apache.poi.ss.formula.FormulaRenderingWorkbook; +import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; /** * @author Josh Micich @@ -29,13 +30,22 @@ final class ExternSheetNameResolver { } public static String prependSheetName(FormulaRenderingWorkbook book, int field_1_index_extern_sheet, String cellRefText) { - String sheetName = book.getSheetNameByExternSheet(field_1_index_extern_sheet); - StringBuffer sb = new StringBuffer(sheetName.length() + cellRefText.length() + 4); - if (sheetName.length() < 1) { - // What excel does if sheet has been deleted - sb.append("#REF"); // note - '!' added just once below + ExternalSheet externalSheet = book.getExternalSheet(field_1_index_extern_sheet); + StringBuffer sb; + if (externalSheet != null) { + String wbName = externalSheet.getWorkbookName(); + String sheetName = externalSheet.getSheetName(); + sb = new StringBuffer(wbName.length() + sheetName.length() + cellRefText.length() + 4); + SheetNameFormatter.appendFormat(sb, wbName, sheetName); } else { - SheetNameFormatter.appendFormat(sb, sheetName); + String sheetName = book.getSheetNameByExternSheet(field_1_index_extern_sheet); + sb = new StringBuffer(sheetName.length() + cellRefText.length() + 4); + if (sheetName.length() < 1) { + // What excel does if sheet has been deleted + sb.append("#REF"); // note - '!' added just once below + } else { + SheetNameFormatter.appendFormat(sb, sheetName); + } } sb.append('!'); sb.append(cellRefText); diff --git a/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java b/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java index 261e98ce8..053057925 100644 --- a/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/Ref3DPtg.java @@ -19,8 +19,9 @@ package org.apache.poi.hssf.record.formula; import org.apache.poi.hssf.record.RecordInputStream; import org.apache.poi.ss.util.CellReference; -import org.apache.poi.ss.formula.WorkbookDependentFormula; +import org.apache.poi.ss.formula.ExternSheetReferenceToken; import org.apache.poi.ss.formula.FormulaRenderingWorkbook; +import org.apache.poi.ss.formula.WorkbookDependentFormula; import org.apache.poi.util.LittleEndian; /** @@ -31,7 +32,7 @@ import org.apache.poi.util.LittleEndian; * @author Jason Height (jheight at chariot dot net dot au) * @version 1.0-pre */ -public final class Ref3DPtg extends RefPtgBase implements WorkbookDependentFormula { +public final class Ref3DPtg extends RefPtgBase implements WorkbookDependentFormula, ExternSheetReferenceToken { public final static byte sid = 0x3a; private final static int SIZE = 7; // 6 + 1 for Ptg @@ -75,11 +76,11 @@ public final class Ref3DPtg extends RefPtgBase implements WorkbookDependentFormu return SIZE; } - public int getExternSheetIndex(){ + public int getExternSheetIndex() { return field_1_index_extern_sheet; } - public void setExternSheetIndex(int index){ + public void setExternSheetIndex(int index) { field_1_index_extern_sheet = index; } diff --git a/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java b/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java index ace857da1..fd822c398 100755 --- a/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java +++ b/src/java/org/apache/poi/hssf/record/formula/SheetNameFormatter.java @@ -66,6 +66,22 @@ public final class SheetNameFormatter { out.append(rawSheetName); } } + public static void appendFormat(StringBuffer out, String workbookName, String rawSheetName) { + boolean needsQuotes = needsDelimiting(workbookName) || needsDelimiting(rawSheetName); + if(needsQuotes) { + out.append(DELIMITER); + out.append('['); + appendAndEscape(out, workbookName.replace('[', '(').replace(']', ')')); + out.append(']'); + appendAndEscape(out, rawSheetName); + out.append(DELIMITER); + } else { + out.append('['); + out.append(workbookName); + out.append(']'); + out.append(rawSheetName); + } + } private static void appendAndEscape(StringBuffer sb, String rawSheetName) { int len = rawSheetName.length(); @@ -101,13 +117,27 @@ public final class SheetNameFormatter { return true; } } + if (nameLooksLikeBooleanLiteral(rawSheetName)) { + return true; + } + // Error constant literals all contain '#' and other special characters + // so they don't get this far return false; } + private static boolean nameLooksLikeBooleanLiteral(String rawSheetName) { + switch(rawSheetName.charAt(0)) { + case 'T': case 't': + return "TRUE".equalsIgnoreCase(rawSheetName); + case 'F': case 'f': + return "FALSE".equalsIgnoreCase(rawSheetName); + } + return false; + } /** * @return true if the presence of the specified character in a sheet name would * require the sheet name to be delimited in formulas. This includes every non-alphanumeric - * character besides underscore '_'. + * character besides underscore '_' and dot '.'. */ /* package */ static boolean isSpecialChar(char ch) { // note - Character.isJavaIdentifierPart() would allow dollars '$' @@ -115,7 +145,8 @@ public final class SheetNameFormatter { return false; } switch(ch) { - case '_': // underscore is ok + case '.': // dot is OK + case '_': // underscore is OK return false; case '\n': case '\r': diff --git a/src/java/org/apache/poi/hssf/record/formula/eval/RangeEval.java b/src/java/org/apache/poi/hssf/record/formula/eval/RangeEval.java new file mode 100644 index 000000000..9398108fd --- /dev/null +++ b/src/java/org/apache/poi/hssf/record/formula/eval/RangeEval.java @@ -0,0 +1,71 @@ +/* ==================================================================== + 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.hssf.record.formula.eval; + + +/** + * + * @author Josh Micich + */ +public final class RangeEval implements OperationEval { + + public static final OperationEval instance = new RangeEval(); + + private RangeEval() { + } + + public Eval evaluate(Eval[] args, int srcCellRow, short srcCellCol) { + if(args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + + try { + RefEval reA = evaluateRef(args[0]); + RefEval reB = evaluateRef(args[1]); + return resolveRange(reA, reB); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + private static AreaEval resolveRange(RefEval reA, RefEval reB) { + + int height = reB.getRow() - reA.getRow(); + int width = reB.getColumn() - reA.getColumn(); + + return reA.offset(0, height, 0, width); + } + + private static RefEval evaluateRef(Eval arg) throws EvaluationException { + if (arg instanceof RefEval) { + return (RefEval) arg; + } + if (arg instanceof ErrorEval) { + throw new EvaluationException((ErrorEval)arg); + } + throw new IllegalArgumentException("Unexpected ref arg class (" + arg.getClass().getName() + ")"); + } + + public int getNumberOfOperands() { + return 2; + } + + public int getType() { + throw new RuntimeException("obsolete code should not be called"); + } +} diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java index d82a9d114..0b6a95449 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFEvaluationWorkbook.java @@ -2,7 +2,9 @@ package org.apache.poi.hssf.usermodel; import org.apache.poi.hssf.model.HSSFFormulaParser; import org.apache.poi.hssf.model.Workbook; +import org.apache.poi.hssf.record.FormulaRecord; import org.apache.poi.hssf.record.NameRecord; +import org.apache.poi.hssf.record.aggregates.FormulaRecordAggregate; import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NameXPtg; import org.apache.poi.hssf.record.formula.Ptg; @@ -39,6 +41,9 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E int sheetIndex = _uBook.getSheetIndex(sheetName); return _iBook.checkExternSheet(sheetIndex); } + public int getExternalSheetIndex(String workbookName, String sheetName) { + return _iBook.getExternalSheetIndex(workbookName, sheetName); + } public EvaluationName getName(int index) { return new Name(_iBook.getNameRecord(index), index); @@ -57,6 +62,9 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E public int getSheetIndex(Sheet sheet) { return _uBook.getSheetIndex(sheet); } + public int getSheetIndex(String sheetName) { + return _uBook.getSheetIndex(sheetName); + } public String getSheetName(int sheetIndex) { return _uBook.getSheetName(sheetIndex); @@ -75,8 +83,12 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E } public int convertFromExternSheetIndex(int externSheetIndex) { return _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex); -} + } + public ExternalSheet getExternalSheet(int externSheetIndex) { + return _iBook.getExternalSheet(externSheetIndex); + } + public HSSFWorkbook getWorkbook() { return _uBook; } @@ -96,7 +108,15 @@ public final class HSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return new Name(_iBook.getNameRecord(ix), ix); } public Ptg[] getFormulaTokens(Cell cell) { - return HSSFFormulaParser.parse(cell.getCellFormula(), _uBook); + if (false) { + // re-parsing the formula text also works, but is a waste of time + // It is useful from time to time to run all unit tests with this code + // to make sure that all formulas POI can evaluate can also be parsed. + return HSSFFormulaParser.parse(cell.getCellFormula(), _uBook); + } + HSSFCell hCell = (HSSFCell) cell; + FormulaRecord fr = ((FormulaRecordAggregate) hCell.getCellValueRecord()).getFormulaRecord(); + return fr.getParsedExpression(); } private static final class Name implements EvaluationName { diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java index d7a320738..2f30d15c8 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFFormulaEvaluator.java @@ -25,6 +25,7 @@ import org.apache.poi.hssf.record.formula.eval.ErrorEval; import org.apache.poi.hssf.record.formula.eval.NumberEval; import org.apache.poi.hssf.record.formula.eval.StringEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment; import org.apache.poi.ss.formula.WorkbookEvaluator; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellValue; @@ -56,6 +57,21 @@ public class HSSFFormulaEvaluator /* almost implements FormulaEvaluator */ { public HSSFFormulaEvaluator(HSSFWorkbook workbook) { _bookEvaluator = new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook)); } + + /** + * Coordinates several formula evaluators together so that formulas that involve external + * references can be evaluated. + * @param workbookNames the simple file names used to identify the workbooks in formulas + * with external links (for example "MyData.xls" as used in a formula "[MyData.xls]Sheet1!A1") + * @param evaluators all evaluators for the full set of workbooks required by the formulas. + */ + public static void setupEnvironment(String[] workbookNames, HSSFFormulaEvaluator[] evaluators) { + WorkbookEvaluator[] wbEvals = new WorkbookEvaluator[evaluators.length]; + for (int i = 0; i < wbEvals.length; i++) { + wbEvals[i] = evaluators[i]._bookEvaluator; + } + CollaboratingWorkbooksEnvironment.setup(workbookNames, wbEvals); + } /** * Does nothing diff --git a/src/java/org/apache/poi/hssf/util/AreaReference.java b/src/java/org/apache/poi/hssf/util/AreaReference.java index d6575642e..6a0b0e2d3 100644 --- a/src/java/org/apache/poi/hssf/util/AreaReference.java +++ b/src/java/org/apache/poi/hssf/util/AreaReference.java @@ -30,6 +30,7 @@ public final class AreaReference extends org.apache.poi.ss.util.AreaReference { /** * Creates an area ref from a pair of Cell References. + * Also normalises such that the top-left */ public AreaReference(CellReference topLeft, CellReference botRight) { super(topLeft, botRight); diff --git a/src/java/org/apache/poi/hssf/util/CellReference.java b/src/java/org/apache/poi/hssf/util/CellReference.java index 0ddb0764e..2e31b2a21 100644 --- a/src/java/org/apache/poi/hssf/util/CellReference.java +++ b/src/java/org/apache/poi/hssf/util/CellReference.java @@ -18,22 +18,13 @@ package org.apache.poi.hssf.util; /** - * Common convertion functions between Excel style A1, C27 style + * Common conversion functions between Excel style A1, C27 style * cell references, and POI usermodel style row=0, column=0 * style references. * @author Avik Sengupta * @author Dennis Doubleday (patch to seperateRowColumns()) */ public final class CellReference extends org.apache.poi.ss.util.CellReference { - /** - * Used to classify identifiers found in formulas as cell references or not. - */ - public static final class NameType { - public static final int CELL = 1; - public static final int NAMED_RANGE = 2; - public static final int BAD_CELL_OR_NAMED_RANGE = -1; - } - /** * Create an cell ref from a string representation. Sheet names containing special characters should be * delimited and escaped as per normal syntax rules for formulas. @@ -45,9 +36,6 @@ public final class CellReference extends org.apache.poi.ss.util.CellReference { public CellReference(int pRow, int pCol) { super(pRow, pCol, true, true); } - public CellReference(int pRow, short pCol) { - super(pRow, (int)pCol, true, true); - } public CellReference(int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { super(null, pRow, pCol, pAbsRow, pAbsCol); @@ -55,10 +43,6 @@ public final class CellReference extends org.apache.poi.ss.util.CellReference { public CellReference(String pSheetName, int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { super(pSheetName, pRow, pCol, pAbsRow, pAbsCol); } - - protected void appendCellReference(StringBuffer sb) { - super.appendCellReference(sb); - } protected static String convertNumToColString(int col) { return org.apache.poi.ss.util.CellReference.convertNumToColString(col); } diff --git a/src/java/org/apache/poi/ss/formula/CellLocation.java b/src/java/org/apache/poi/ss/formula/CellLocation.java index d6bb9b7bf..6857c4bc1 100644 --- a/src/java/org/apache/poi/ss/formula/CellLocation.java +++ b/src/java/org/apache/poi/ss/formula/CellLocation.java @@ -17,25 +17,32 @@ package org.apache.poi.ss.formula; +import org.apache.poi.hssf.util.CellReference; + /** * Stores the parameters that identify the evaluation of one cell.
*/ final class CellLocation { public static final CellLocation[] EMPTY_ARRAY = { }; + private final EvaluationWorkbook _book; private final int _sheetIndex; private final int _rowIndex; private final int _columnIndex; private final int _hashCode; - public CellLocation(int sheetIndex, int rowIndex, int columnIndex) { + public CellLocation(EvaluationWorkbook book, int sheetIndex, int rowIndex, int columnIndex) { if (sheetIndex < 0) { throw new IllegalArgumentException("sheetIndex must not be negative"); } + _book = book; _sheetIndex = sheetIndex; _rowIndex = rowIndex; _columnIndex = columnIndex; - _hashCode = sheetIndex + 17 * (rowIndex + 17 * columnIndex); + _hashCode = System.identityHashCode(book) + sheetIndex + 17 * (rowIndex + 17 * columnIndex); + } + public Object getBook() { + return _book; } public int getSheetIndex() { return _sheetIndex; @@ -49,15 +56,18 @@ final class CellLocation { public boolean equals(Object obj) { CellLocation other = (CellLocation) obj; - if (getSheetIndex() != other.getSheetIndex()) { - return false; - } if (getRowIndex() != other.getRowIndex()) { return false; } if (getColumnIndex() != other.getColumnIndex()) { return false; } + if (getSheetIndex() != other.getSheetIndex()) { + return false; + } + if (getBook() != other.getBook()) { + return false; + } return true; } public int hashCode() { @@ -68,7 +78,8 @@ final class CellLocation { * @return human readable string for debug purposes */ public String formatAsString() { - return "ShIx=" + getSheetIndex() + " R=" + getRowIndex() + " C=" + getColumnIndex(); + CellReference cr = new CellReference(_rowIndex, _columnIndex, false, false); + return "ShIx=" + getSheetIndex() + " " + cr.formatAsString(); } public String toString() { diff --git a/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java b/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java new file mode 100644 index 000000000..c62d2f182 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/CollaboratingWorkbooksEnvironment.java @@ -0,0 +1,155 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ +package org.apache.poi.ss.formula; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + + +/** + * Manages a collection of {@link WorkbookEvaluator}s, in order to support evaluation of formulas + * across spreadsheets.

+ * + * For POI internal use only + * + * @author Josh Micich + */ +public final class CollaboratingWorkbooksEnvironment { + + public static final CollaboratingWorkbooksEnvironment EMPTY = new CollaboratingWorkbooksEnvironment(); + + private final Map _evaluatorsByName; + private final WorkbookEvaluator[] _evaluators; + + private boolean _unhooked; + private CollaboratingWorkbooksEnvironment() { + _evaluatorsByName = Collections.EMPTY_MAP; + _evaluators = new WorkbookEvaluator[0]; + } + public static void setup(String[] workbookNames, WorkbookEvaluator[] evaluators) { + int nItems = workbookNames.length; + if (evaluators.length != nItems) { + throw new IllegalArgumentException("Number of workbook names is " + nItems + + " but number of evaluators is " + evaluators.length); + } + if (nItems < 1) { + throw new IllegalArgumentException("Must provide at least one collaborating worbook"); + } + new CollaboratingWorkbooksEnvironment(workbookNames, evaluators, nItems); + } + + private CollaboratingWorkbooksEnvironment(String[] workbookNames, WorkbookEvaluator[] evaluators, int nItems) { + Map m = new HashMap(nItems * 3 / 2); + IdentityHashMap uniqueEvals = new IdentityHashMap(nItems * 3 / 2); + for(int i=0; i0) { + sb.append(", "); + } + sb.append("'").append(i.next()).append("'"); + } + sb.append(")"); + } + throw new RuntimeException(sb.toString()); + } + return result; + } +} diff --git a/src/java/org/apache/poi/ss/formula/EvaluationCache.java b/src/java/org/apache/poi/ss/formula/EvaluationCache.java index fdc933f6f..b0c34fd78 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationCache.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationCache.java @@ -81,13 +81,7 @@ final class EvaluationCache { + cellLoc.formatAsString()); } } - if (_evaluationListener == null) { - // optimisation - don't bother sorting if there is no listener. - } else { - // for testing - // make order of callbacks to listener more deterministic - Arrays.sort(usedCells, CellLocationComparator); - } + sortCellLocationsForLogging(usedCells); CellCacheEntry entry = getEntry(cellLoc); CellLocation[] consumingFormulaCells = entry.getConsumingCells(); CellLocation[] prevUsedCells = entry.getUsedCells(); @@ -110,6 +104,18 @@ final class EvaluationCache { recurseClearCachedFormulaResults(consumingFormulaCells, 0); } + /** + * This method sorts the supplied cellLocs so that the order of call-backs to the evaluation + * listener is more deterministic + */ + private void sortCellLocationsForLogging(CellLocation[] cellLocs) { + if (_evaluationListener == null) { + // optimisation - don't bother sorting if there is no listener. + } else { + Arrays.sort(cellLocs, CellLocationComparator); + } + } + private void unlinkConsumingCells(CellLocation[] prevUsedCells, CellLocation[] usedCells, CellLocation cellLoc) { if (prevUsedCells == null) { @@ -149,6 +155,7 @@ final class EvaluationCache { * @param formulaCells */ private void recurseClearCachedFormulaResults(CellLocation[] formulaCells, int depth) { + sortCellLocationsForLogging(formulaCells); int nextDepth = depth+1; for (int i = 0; i < formulaCells.length; i++) { CellLocation fc = formulaCells[i]; @@ -196,6 +203,10 @@ final class EvaluationCache { CellLocation clB = (CellLocation) b; int cmp; + cmp = System.identityHashCode(clA.getBook()) - System.identityHashCode(clB.getBook()); + if (cmp != 0) { + return cmp; + } cmp = clA.getSheetIndex() - clB.getSheetIndex(); if (cmp != 0) { return cmp; diff --git a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java index 02917d82a..6cf938fe9 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java @@ -31,12 +31,36 @@ import org.apache.poi.ss.usermodel.Sheet; */ public interface EvaluationWorkbook { String getSheetName(int sheetIndex); + /** + * @return -1 if the specified sheet is from a different book + */ int getSheetIndex(Sheet sheet); + int getSheetIndex(String sheetName); Sheet getSheet(int sheetIndex); + /** + * @return null if externSheetIndex refers to a sheet inside the current workbook + */ + ExternalSheet getExternalSheet(int externSheetIndex); int convertFromExternSheetIndex(int externSheetIndex); EvaluationName getName(NamePtg namePtg); String resolveNameXText(NameXPtg ptg); Ptg[] getFormulaTokens(Cell cell); + + class ExternalSheet { + private final String _workbookName; + private final String _sheetName; + + public ExternalSheet(String workbookName, String sheetName) { + _workbookName = workbookName; + _sheetName = sheetName; + } + public String getWorkbookName() { + return _workbookName; + } + public String getSheetName() { + return _sheetName; + } + } } diff --git a/src/java/org/apache/poi/ss/formula/ExternSheetReferenceToken.java b/src/java/org/apache/poi/ss/formula/ExternSheetReferenceToken.java new file mode 100644 index 000000000..09262a132 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/ExternSheetReferenceToken.java @@ -0,0 +1,29 @@ +/* ==================================================================== + 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.formula; + +/** + * Should be implemented by any {@link Ptg} subclass that needs has an extern sheet index
+ * + * For POI internal use only + * + * @author Josh Micich + */ +public interface ExternSheetReferenceToken { + int getExternSheetIndex(); +} diff --git a/src/java/org/apache/poi/ss/formula/FormulaParser.java b/src/java/org/apache/poi/ss/formula/FormulaParser.java index df2f8c282..1398399ff 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaParser.java +++ b/src/java/org/apache/poi/ss/formula/FormulaParser.java @@ -49,6 +49,7 @@ import org.apache.poi.hssf.record.formula.ParenthesisPtg; import org.apache.poi.hssf.record.formula.PercentPtg; import org.apache.poi.hssf.record.formula.PowerPtg; import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.RangePtg; import org.apache.poi.hssf.record.formula.Ref3DPtg; import org.apache.poi.hssf.record.formula.RefPtg; import org.apache.poi.hssf.record.formula.StringPtg; @@ -60,7 +61,7 @@ import org.apache.poi.hssf.record.formula.function.FunctionMetadataRegistry; import org.apache.poi.hssf.usermodel.HSSFErrorConstants; import org.apache.poi.hssf.util.AreaReference; import org.apache.poi.hssf.util.CellReference; -import org.apache.poi.hssf.util.CellReference.NameType; +import org.apache.poi.ss.util.CellReference.NameType; /** * This class parses a formula string into a List of tokens in RPN order. @@ -81,6 +82,33 @@ import org.apache.poi.hssf.util.CellReference.NameType; * @author Josh Micich */ public final class FormulaParser { + private static final class Identifier { + private final String _name; + private final boolean _isQuoted; + + public Identifier(String name, boolean isQuoted) { + _name = name; + _isQuoted = isQuoted; + } + public String getName() { + return _name; + } + public boolean isQuoted() { + return _isQuoted; + } + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()); + sb.append(" ["); + if (_isQuoted) { + sb.append("'").append(_name).append("'"); + } else { + sb.append(_name); + } + sb.append("]"); + return sb.toString(); + } + } /** * Specific exception thrown when a supplied formula does not parse properly.
@@ -176,23 +204,23 @@ public final class FormulaParser { } /** Recognize an Alpha Character */ - private boolean IsAlpha(char c) { + private static boolean IsAlpha(char c) { return Character.isLetter(c) || c == '$' || c=='_'; } /** Recognize a Decimal Digit */ - private boolean IsDigit(char c) { + private static boolean IsDigit(char c) { return Character.isDigit(c); } /** Recognize an Alphanumeric */ - private boolean IsAlNum(char c) { - return (IsAlpha(c) || IsDigit(c)); + private static boolean IsAlNum(char c) { + return IsAlpha(c) || IsDigit(c); } /** Recognize White Space */ - private boolean IsWhite( char c) { - return (c ==' ' || c== TAB); + private static boolean IsWhite( char c) { + return c ==' ' || c== TAB; } /** Skip Over Leading White Space */ @@ -213,7 +241,13 @@ public final class FormulaParser { } GetChar(); } - + private String parseUnquotedIdentifier() { + Identifier iden = parseIdentifier(); + if (iden.isQuoted()) { + throw expected("unquoted identifier"); + } + return iden.getName(); + } /** * Parses a sheet name, named range name, or simple cell reference.
* Note - identifiers in Excel can contain dots, so this method may return a String @@ -221,18 +255,17 @@ public final class FormulaParser { * may return a value like "A1..B2", in which case the caller must convert it to * an area reference like "A1:B2" */ - private String parseIdentifier() { - StringBuffer Token = new StringBuffer(); - if (!IsAlpha(look) && look != '\'') { + private Identifier parseIdentifier() { + StringBuffer sb = new StringBuffer(); + if (!IsAlpha(look) && look != '\'' && look != '[') { throw expected("Name"); } - if(look == '\'') - { + boolean isQuoted = look == '\''; + if(isQuoted) { Match('\''); boolean done = look == '\''; - while(!done) - { - Token.append(look); + while(!done) { + sb.append(look); GetChar(); if(look == '\'') { @@ -240,17 +273,15 @@ public final class FormulaParser { done = look != '\''; } } - } - else - { + } else { // allow for any sequence of dots and identifier chars // special case of two consecutive dots is best treated in the calling code - while (IsAlNum(look) || look == '.') { - Token.append(look); + while (IsAlNum(look) || look == '.' || look == '[' || look == ']') { + sb.append(look); GetChar(); } } - return Token.toString(); + return new Identifier(sb.toString(), isQuoted); } /** Get a Number */ @@ -265,72 +296,112 @@ public final class FormulaParser { } private ParseNode parseFunctionReferenceOrName() { - String name = parseIdentifier(); + Identifier iden = parseIdentifier(); if (look == '('){ //This is a function - return function(name); + return function(iden.getName()); } - return new ParseNode(parseNameOrReference(name)); + if (!iden.isQuoted()) { + String name = iden.getName(); + if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) { + return new ParseNode(new BoolPtg(name.toUpperCase())); + } + } + return parseRangeExpression(iden); } - private Ptg parseNameOrReference(String name) { + private ParseNode parseRangeExpression(Identifier iden) { + Ptg ptgA = parseNameOrCellRef(iden); + if (look == ':') { + GetChar(); + Identifier iden2 = parseIdentifier(); + Ptg ptgB = parseNameOrCellRef(iden2); + Ptg simplified = reduceRangeExpression(ptgA, ptgB); + + if (simplified == null) { + ParseNode[] children = { + new ParseNode(ptgA), + new ParseNode(ptgB), + }; + return new ParseNode(RangePtg.instance, children); + } + return new ParseNode(simplified); + } + return new ParseNode(ptgA); + } + + /** + * + * "A1", "B3" -> "A1:B3" + * "sheet1!A1", "B3" -> "sheet1!A1:B3" + * + * @return null if the range expression cannot / shouldn't be reduced. + */ + private static Ptg reduceRangeExpression(Ptg ptgA, Ptg ptgB) { + if (!(ptgB instanceof RefPtg)) { + // only when second ref is simple 2-D ref can the range + // expression be converted to an area ref + return null; + } + RefPtg refB = (RefPtg) ptgB; + if (ptgA instanceof RefPtg) { + RefPtg refA = (RefPtg) ptgA; + return new AreaPtg(refA.getRow(), refB.getRow(), refA.getColumn(), refB.getColumn(), + refA.isRowRelative(), refB.isRowRelative(), refA.isColRelative(), refB.isColRelative()); + } + if (ptgA instanceof Ref3DPtg) { + Ref3DPtg refA = (Ref3DPtg) ptgA; + return new Area3DPtg(refA.getRow(), refB.getRow(), refA.getColumn(), refB.getColumn(), + refA.isRowRelative(), refB.isRowRelative(), refA.isColRelative(), refB.isColRelative(), + refA.getExternSheetIndex()); + } + // Note - other operand types (like AreaPtg) which probably can't evaluate + // do not cause validation errors at parse time + return null; + } + + private Ptg parseNameOrCellRef(Identifier iden) { + + if (look == '!') { + GetChar(); + // 3-D ref + // this code assumes iden is a sheetName + // TODO - handle ! + int externIdx = getExternalSheetIndex(iden.getName()); + String secondIden = parseUnquotedIdentifier(); + AreaReference areaRef = parseArea(secondIden); + if (areaRef == null) { + return new Ref3DPtg(secondIden, externIdx); + } + // will happen if dots are used instead of colon + return new Area3DPtg(areaRef.formatAsString(), externIdx); + } + + String name = iden.getName(); AreaReference areaRef = parseArea(name); if (areaRef != null) { // will happen if dots are used instead of colon return new AreaPtg(areaRef.formatAsString()); } - - if (look == ':' || look == '.') { // this is a AreaReference - GetChar(); - - while (look == '.') { // formulas can have . or .. or ... instead of : - GetChar(); - } - - String first = name; - String second = parseIdentifier(); - return new AreaPtg(first+":"+second); - } - - if (look == '!') { - Match('!'); - String sheetName = name; - String first = parseIdentifier(); - int externIdx = book.getExternalSheetIndex(sheetName); - areaRef = parseArea(name); - if (areaRef != null) { - // will happen if dots are used instead of colon - return new Area3DPtg(areaRef.formatAsString(), externIdx); - } - if (look == ':') { - Match(':'); - String second=parseIdentifier(); - if (look == '!') { - //The sheet name was included in both of the areas. Only really - //need it once - Match('!'); - String third=parseIdentifier(); - - if (!sheetName.equals(second)) - throw new RuntimeException("Unhandled double sheet reference."); - - return new Area3DPtg(first+":"+third,externIdx); - } - return new Area3DPtg(first+":"+second,externIdx); - } - return new Ref3DPtg(first, externIdx); - } - if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) { - return new BoolPtg(name.toUpperCase()); - } - // This can be either a cell ref or a named range - // Try to spot which it is + + int nameType = CellReference.classifyCellReference(name); if (nameType == NameType.CELL) { return new RefPtg(name); } + if (look == ':') { + if (nameType == NameType.COLUMN) { + GetChar(); + String secondIden = parseUnquotedIdentifier(); + if (CellReference.classifyCellReference(secondIden) != NameType.COLUMN) { + throw new FormulaParseException("Expected full column after '" + name + + ":' but got '" + secondIden + "'"); + } + return new AreaPtg(name + ":" + secondIden); + } + } if (nameType != NameType.NAMED_RANGE) { new FormulaParseException("Name '" + name + "' does not look like a cell reference or named range"); @@ -347,6 +418,17 @@ public final class FormulaParser { + name + "' is not a range as expected"); } + private int getExternalSheetIndex(String name) { + if (name.charAt(0) == '[') { + // we have a sheet name qualified with workbook name e.g. '[MyData.xls]Sheet1' + int pos = name.lastIndexOf(']'); // safe because sheet names never have ']' + String wbName = name.substring(1, pos); + String sheetName = name.substring(pos+1); + return book.getExternalSheetIndex(wbName, sheetName); + } + return book.getExternalSheetIndex(name); + } + /** * @param name an 'identifier' like string (i.e. contains alphanums, and dots) * @return null if name cannot be split at a dot @@ -585,7 +667,7 @@ public final class FormulaParser { Match('}'); return arrayNode; } - if (IsAlpha(look) || look == '\''){ + if (IsAlpha(look) || look == '\'' || look == '['){ return parseFunctionReferenceOrName(); } // else - assume number @@ -662,7 +744,7 @@ public final class FormulaParser { } private Boolean parseBooleanLiteral() { - String iden = parseIdentifier(); + String iden = parseUnquotedIdentifier(); if ("TRUE".equalsIgnoreCase(iden)) { return Boolean.TRUE; } @@ -720,7 +802,7 @@ public final class FormulaParser { private int parseErrorLiteral() { Match('#'); - String part1 = parseIdentifier().toUpperCase(); + String part1 = parseUnquotedIdentifier().toUpperCase(); switch(part1.charAt(0)) { case 'V': diff --git a/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java b/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java index 69431c2c2..e9be7d1d3 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/FormulaParsingWorkbook.java @@ -32,6 +32,16 @@ public interface FormulaParsingWorkbook { */ EvaluationName getName(String name); - int getExternalSheetIndex(String sheetName); NameXPtg getNameXPtg(String name); + + /** + * gets the externSheet index for a sheet from this workbook + */ + int getExternalSheetIndex(String sheetName); + /** + * gets the externSheet index for a sheet from an external workbook + * @param workbookName e.g. "Budget.xls" + * @param sheetName a name of a sheet in that workbook + */ + int getExternalSheetIndex(String workbookName, String sheetName); } diff --git a/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java b/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java index ac95f4da0..c9b95f6b1 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/FormulaRenderingWorkbook.java @@ -19,6 +19,7 @@ package org.apache.poi.ss.formula; import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NameXPtg; +import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; /** * Abstracts a workbook for the purpose of converting formula to text.
@@ -29,6 +30,10 @@ import org.apache.poi.hssf.record.formula.NameXPtg; */ public interface FormulaRenderingWorkbook { + /** + * @return null if externSheetIndex refers to a sheet inside the current workbook + */ + ExternalSheet getExternalSheet(int externSheetIndex); String getSheetNameByExternSheet(int externSheetIndex); String resolveNameXText(NameXPtg nameXPtg); String getNameText(NamePtg namePtg); diff --git a/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java b/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java index 79087e4d6..91226bd53 100644 --- a/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java +++ b/src/java/org/apache/poi/ss/formula/OperandClassTransformer.java @@ -19,6 +19,7 @@ package org.apache.poi.ss.formula; import org.apache.poi.hssf.record.formula.AbstractFunctionPtg; import org.apache.poi.hssf.record.formula.ControlPtg; +import org.apache.poi.hssf.record.formula.RangePtg; import org.apache.poi.hssf.record.formula.ValueOperatorPtg; import org.apache.poi.hssf.record.formula.Ptg; @@ -115,6 +116,10 @@ final class OperandClassTransformer { return; } if (children.length > 0) { + if (token == RangePtg.instance) { + // TODO is any token transformation required under the various ref operators? + return; + } throw new IllegalStateException("Node should not have any children"); } diff --git a/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java b/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java index eaa57c114..09c93723f 100755 --- a/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java +++ b/src/java/org/apache/poi/ss/formula/OperationEvaluatorFactory.java @@ -40,6 +40,7 @@ import org.apache.poi.hssf.record.formula.OperationPtg; import org.apache.poi.hssf.record.formula.PercentPtg; import org.apache.poi.hssf.record.formula.PowerPtg; import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.RangePtg; import org.apache.poi.hssf.record.formula.SubtractPtg; import org.apache.poi.hssf.record.formula.UnaryMinusPtg; import org.apache.poi.hssf.record.formula.UnaryPlusPtg; @@ -57,6 +58,7 @@ import org.apache.poi.hssf.record.formula.eval.NotEqualEval; import org.apache.poi.hssf.record.formula.eval.OperationEval; import org.apache.poi.hssf.record.formula.eval.PercentEval; import org.apache.poi.hssf.record.formula.eval.PowerEval; +import org.apache.poi.hssf.record.formula.eval.RangeEval; import org.apache.poi.hssf.record.formula.eval.SubtractEval; import org.apache.poi.hssf.record.formula.eval.UnaryMinusEval; import org.apache.poi.hssf.record.formula.eval.UnaryPlusEval; @@ -101,6 +103,7 @@ final class OperationEvaluatorFactory { add(m, SubtractPtg.class, SubtractEval.instance); add(m, UnaryMinusPtg.class, UnaryMinusEval.instance); add(m, UnaryPlusPtg.class, UnaryPlusEval.instance); + add(m, RangePtg.class, RangeEval.instance); return m; } diff --git a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java index b41e2c2c3..9c02ca9f9 100644 --- a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -22,12 +22,18 @@ import java.util.Map; import java.util.Stack; import org.apache.poi.hssf.record.formula.Area3DPtg; +import org.apache.poi.hssf.record.formula.AreaErrPtg; import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.AttrPtg; import org.apache.poi.hssf.record.formula.BoolPtg; import org.apache.poi.hssf.record.formula.ControlPtg; +import org.apache.poi.hssf.record.formula.DeletedArea3DPtg; +import org.apache.poi.hssf.record.formula.DeletedRef3DPtg; import org.apache.poi.hssf.record.formula.ErrPtg; +import org.apache.poi.hssf.record.formula.FuncVarPtg; import org.apache.poi.hssf.record.formula.IntPtg; import org.apache.poi.hssf.record.formula.MemErrPtg; +import org.apache.poi.hssf.record.formula.MemFuncPtg; import org.apache.poi.hssf.record.formula.MissingArgPtg; import org.apache.poi.hssf.record.formula.NamePtg; import org.apache.poi.hssf.record.formula.NameXPtg; @@ -35,6 +41,7 @@ import org.apache.poi.hssf.record.formula.NumberPtg; import org.apache.poi.hssf.record.formula.OperationPtg; import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ref3DPtg; +import org.apache.poi.hssf.record.formula.RefErrorPtg; import org.apache.poi.hssf.record.formula.RefPtg; import org.apache.poi.hssf.record.formula.StringPtg; import org.apache.poi.hssf.record.formula.UnionPtg; @@ -53,6 +60,7 @@ import org.apache.poi.hssf.record.formula.eval.RefEval; import org.apache.poi.hssf.record.formula.eval.StringEval; import org.apache.poi.hssf.record.formula.eval.ValueEval; import org.apache.poi.hssf.util.CellReference; +import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; @@ -68,13 +76,14 @@ import org.apache.poi.ss.usermodel.Sheet; * * @author Josh Micich */ -public class WorkbookEvaluator { +public final class WorkbookEvaluator { private final EvaluationWorkbook _workbook; - private final EvaluationCache _cache; + private EvaluationCache _cache; private final IEvaluationListener _evaluationListener; private final Map _sheetIndexesBySheet; + private CollaboratingWorkbooksEnvironment _collaboratingWorkbookEnvironment; public WorkbookEvaluator(EvaluationWorkbook workbook) { this (workbook, null); @@ -84,6 +93,7 @@ public class WorkbookEvaluator { _evaluationListener = evaluationListener; _cache = new EvaluationCache(evaluationListener); _sheetIndexesBySheet = new IdentityHashMap(); + _collaboratingWorkbookEnvironment = CollaboratingWorkbooksEnvironment.EMPTY; } /** @@ -101,7 +111,22 @@ public class WorkbookEvaluator { System.out.println(s); } } + /* package */ void attachToEnvironment(CollaboratingWorkbooksEnvironment collaboratingWorkbooksEnvironment, EvaluationCache cache) { + _collaboratingWorkbookEnvironment = collaboratingWorkbooksEnvironment; + _cache = cache; + } + /* package */ CollaboratingWorkbooksEnvironment getEnvironment() { + return _collaboratingWorkbookEnvironment; + } + /* package */ void detachFromEnvironment() { + _collaboratingWorkbookEnvironment = CollaboratingWorkbooksEnvironment.EMPTY; + _cache = new EvaluationCache(_evaluationListener); + } + /* package */ IEvaluationListener getEvaluationListener() { + return _evaluationListener; + } + /** * Should be called whenever there are changes to input cells in the evaluated workbook. * Failure to call this method after changing cell values will cause incorrect behaviour @@ -123,7 +148,7 @@ public class WorkbookEvaluator { throw new IllegalArgumentException("value must not be null"); } int sheetIndex = getSheetIndex(sheet); - _cache.setValue(new CellLocation(sheetIndex, rowIndex, columnIndex), true, CellLocation.EMPTY_ARRAY, value); + _cache.setValue(new CellLocation(_workbook, sheetIndex, rowIndex, columnIndex), true, CellLocation.EMPTY_ARRAY, value); } /** @@ -132,13 +157,17 @@ public class WorkbookEvaluator { */ public void notifySetFormula(Sheet sheet, int rowIndex, int columnIndex) { int sheetIndex = getSheetIndex(sheet); - _cache.setValue(new CellLocation(sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null); + _cache.setValue(new CellLocation(_workbook, sheetIndex, rowIndex, columnIndex), false, CellLocation.EMPTY_ARRAY, null); } private int getSheetIndex(Sheet sheet) { Integer result = (Integer) _sheetIndexesBySheet.get(sheet); if (result == null) { - result = new Integer(_workbook.getSheetIndex(sheet)); + int sheetIndex = _workbook.getSheetIndex(sheet); + if (sheetIndex < 0) { + throw new RuntimeException("Specified sheet from a different book"); + } + result = new Integer(sheetIndex); _sheetIndexesBySheet.put(sheet, result); } return result.intValue(); @@ -146,7 +175,7 @@ public class WorkbookEvaluator { public ValueEval evaluate(Cell srcCell) { int sheetIndex = getSheetIndex(srcCell.getSheet()); - CellLocation cellLoc = new CellLocation(sheetIndex, srcCell.getRowIndex(), srcCell.getCellNum()); + CellLocation cellLoc = new CellLocation(_workbook, sheetIndex, srcCell.getRowIndex(), srcCell.getCellNum()); return internalEvaluate(srcCell, cellLoc, new EvaluationTracker(_cache)); } @@ -181,10 +210,10 @@ public class WorkbookEvaluator { isPlainFormulaCell = false; Ptg[] ptgs = _workbook.getFormulaTokens(srcCell); if(evalListener == null) { - result = evaluateCell(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker); + result = evaluateFormula(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker); } else { evalListener.onStartEvaluate(sheetIndex, rowIndex, columnIndex, ptgs); - result = evaluateCell(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker); + result = evaluateFormula(sheetIndex, rowIndex, (short)columnIndex, ptgs, tracker); evalListener.onEndEvaluate(sheetIndex, rowIndex, columnIndex, result); } } @@ -225,17 +254,31 @@ public class WorkbookEvaluator { } throw new RuntimeException("Unexpected cell type (" + cellType + ")"); } - private ValueEval evaluateCell(int sheetIndex, int srcRowNum, short srcColNum, Ptg[] ptgs, EvaluationTracker tracker) { + // visibility raised for testing + /* package */ ValueEval evaluateFormula(int sheetIndex, int srcRowNum, int srcColNum, Ptg[] ptgs, EvaluationTracker tracker) { Stack stack = new Stack(); for (int i = 0, iSize = ptgs.length; i < iSize; i++) { // since we don't know how to handle these yet :( Ptg ptg = ptgs[i]; + if (ptg instanceof AttrPtg) { + AttrPtg attrPtg = (AttrPtg) ptg; + if (attrPtg.isSum()) { + // Excel prefers to encode 'SUM()' as a tAttr token, but this evaluator + // expects the equivalent function token + byte nArgs = 1; // tAttrSum always has 1 parameter + ptg = new FuncVarPtg("SUM", nArgs); + } + } if (ptg instanceof ControlPtg) { // skip Parentheses, Attr, etc continue; } + if (ptg instanceof MemFuncPtg) { + // can ignore, rest of tokens for this expression are in OK RPN order + continue; + } if (ptg instanceof MemErrPtg) { continue; } if (ptg instanceof MissingArgPtg) { // TODO - might need to push BlankEval or MissingArgEval @@ -289,7 +332,7 @@ public class WorkbookEvaluator { * @return a NumberEval, StringEval, BoolEval, * BlankEval or ErrorEval. Never null. */ - private static ValueEval dereferenceValue(ValueEval evaluationResult, int srcRowNum, short srcColNum) { + private static ValueEval dereferenceValue(ValueEval evaluationResult, int srcRowNum, int srcColNum) { if (evaluationResult instanceof RefEval) { RefEval rv = (RefEval) evaluationResult; return rv.getInnerValueEval(); @@ -321,6 +364,20 @@ public class WorkbookEvaluator { } return operation.evaluate(ops, srcRowNum, (short)srcColNum); } + private SheetRefEvaluator createExternSheetRefEvaluator(EvaluationTracker tracker, + ExternSheetReferenceToken ptg) { + int externSheetIndex = ptg.getExternSheetIndex(); + ExternalSheet externalSheet = _workbook.getExternalSheet(externSheetIndex); + if (externalSheet != null) { + WorkbookEvaluator otherEvaluator = _collaboratingWorkbookEnvironment.getWorkbookEvaluator(externalSheet.getWorkbookName()); + EvaluationWorkbook otherBook = otherEvaluator._workbook; + int otherSheetIndex = otherBook.getSheetIndex(externalSheet.getSheetName()); + return new SheetRefEvaluator(otherEvaluator, tracker, otherBook, otherSheetIndex); + } + int otherSheetIndex = _workbook.convertFromExternSheetIndex(externSheetIndex); + return new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex); + + } /** * returns an appropriate Eval impl instance for the Ptg. The Ptg must be @@ -329,6 +386,8 @@ public class WorkbookEvaluator { * passed here! */ private Eval getEvalForPtg(Ptg ptg, int sheetIndex, EvaluationTracker tracker) { + // consider converting all these (ptg instanceof XxxPtg) expressions to (ptg.getClass() == XxxPtg.class) + if (ptg instanceof NamePtg) { // named ranges, macro functions NamePtg namePtg = (NamePtg) ptg; @@ -361,16 +420,18 @@ public class WorkbookEvaluator { if (ptg instanceof ErrPtg) { return ErrorEval.valueOf(((ErrPtg) ptg).getErrorCode()); } + if (ptg instanceof AreaErrPtg ||ptg instanceof RefErrorPtg + || ptg instanceof DeletedArea3DPtg || ptg instanceof DeletedRef3DPtg) { + return ErrorEval.REF_INVALID; + } if (ptg instanceof Ref3DPtg) { Ref3DPtg refPtg = (Ref3DPtg) ptg; - int otherSheetIndex = _workbook.convertFromExternSheetIndex(refPtg.getExternSheetIndex()); - SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex); + SheetRefEvaluator sre = createExternSheetRefEvaluator(tracker, refPtg); return new LazyRefEval(refPtg, sre); } if (ptg instanceof Area3DPtg) { Area3DPtg aptg = (Area3DPtg) ptg; - int otherSheetIndex = _workbook.convertFromExternSheetIndex(aptg.getExternSheetIndex()); - SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, otherSheetIndex); + SheetRefEvaluator sre = createExternSheetRefEvaluator(tracker, aptg); return new LazyAreaEval(aptg, sre); } SheetRefEvaluator sre = new SheetRefEvaluator(this, tracker, _workbook, sheetIndex); @@ -410,7 +471,7 @@ public class WorkbookEvaluator { } else { cell = row.getCell(columnIndex); } - CellLocation cellLoc = new CellLocation(sheetIndex, rowIndex, columnIndex); + CellLocation cellLoc = new CellLocation(_workbook, sheetIndex, rowIndex, columnIndex); tracker.acceptDependency(cellLoc); return internalEvaluate(cell, cellLoc, tracker); } diff --git a/src/java/org/apache/poi/ss/util/AreaReference.java b/src/java/org/apache/poi/ss/util/AreaReference.java index ae2c9192f..30d92ebcf 100644 --- a/src/java/org/apache/poi/ss/util/AreaReference.java +++ b/src/java/org/apache/poi/ss/util/AreaReference.java @@ -90,7 +90,7 @@ public class AreaReference { for(int i=refPart.length()-1; i>=0; i--) { int ch = refPart.charAt(i); if (ch == '$' && i==0) { - continue; + continue; } if (ch < 'A' || ch > 'Z') { return false; @@ -103,8 +103,45 @@ public class AreaReference { * Creates an area ref from a pair of Cell References. */ public AreaReference(CellReference topLeft, CellReference botRight) { - _firstCell = topLeft; - _lastCell = botRight; + boolean swapRows = topLeft.getRow() > botRight.getRow(); + boolean swapCols = topLeft.getCol() > botRight.getCol(); + if (swapRows || swapCols) { + int firstRow; + int lastRow; + int firstColumn; + int lastColumn; + boolean firstRowAbs; + boolean lastRowAbs; + boolean firstColAbs; + boolean lastColAbs; + if (swapRows) { + firstRow = botRight.getRow(); + firstRowAbs = botRight.isRowAbsolute(); + lastRow = topLeft.getRow(); + lastRowAbs = topLeft.isRowAbsolute(); + } else { + firstRow = topLeft.getRow(); + firstRowAbs = topLeft.isRowAbsolute(); + lastRow = botRight.getRow(); + lastRowAbs = botRight.isRowAbsolute(); + } + if (swapCols) { + firstColumn = botRight.getCol(); + firstColAbs = botRight.isColAbsolute(); + lastColumn = topLeft.getCol(); + lastColAbs = topLeft.isColAbsolute(); + } else { + firstColumn = topLeft.getCol(); + firstColAbs = topLeft.isColAbsolute(); + lastColumn = botRight.getCol(); + lastColAbs = botRight.isColAbsolute(); + } + _firstCell = new CellReference(firstRow, firstColumn, firstRowAbs, firstColAbs); + _lastCell = new CellReference(lastRow, lastColumn, lastRowAbs, lastColAbs); + } else { + _firstCell = topLeft; + _lastCell = botRight; + } _isSingleCell = false; } diff --git a/src/java/org/apache/poi/ss/util/CellReference.java b/src/java/org/apache/poi/ss/util/CellReference.java index e5005db5d..23b4c806e 100644 --- a/src/java/org/apache/poi/ss/util/CellReference.java +++ b/src/java/org/apache/poi/ss/util/CellReference.java @@ -34,21 +34,28 @@ public class CellReference { public static final class NameType { public static final int CELL = 1; public static final int NAMED_RANGE = 2; + public static final int COLUMN = 3; public static final int BAD_CELL_OR_NAMED_RANGE = -1; } - /** The character ($) that signifies a row or column value is absolute instead of relative */ - private static final char ABSOLUTE_REFERENCE_MARKER = '$'; - /** The character (!) that separates sheet names from cell references */ - private static final char SHEET_NAME_DELIMITER = '!'; - /** The character (') used to quote sheet names when they contain special characters */ - private static final char SPECIAL_NAME_DELIMITER = '\''; - - /** - * Matches a run of letters followed by a run of digits. The run of letters is group 1 and the - * run of digits is group 2. Each group may optionally be prefixed with a single '$'. - */ + /** The character ($) that signifies a row or column value is absolute instead of relative */ + private static final char ABSOLUTE_REFERENCE_MARKER = '$'; + /** The character (!) that separates sheet names from cell references */ + private static final char SHEET_NAME_DELIMITER = '!'; + /** The character (') used to quote sheet names when they contain special characters */ + private static final char SPECIAL_NAME_DELIMITER = '\''; + + /** + * Matches a run of one or more letters followed by a run of one or more digits. + * The run of letters is group 1 and the run of digits is group 2. + * Each group may optionally be prefixed with a single '$'. + */ private static final Pattern CELL_REF_PATTERN = Pattern.compile("\\$?([A-Za-z]+)\\$?([0-9]+)"); + /** + * Matches a run of one or more letters. The run of letters is group 1. + * The text may optionally be prefixed with a single '$'. + */ + private static final Pattern COLUMN_REF_PATTERN = Pattern.compile("\\$?([A-Za-z]+)"); /** * Named range names must start with a letter or underscore. Subsequent characters may include * digits or dot. (They can even end in dot). @@ -65,96 +72,96 @@ public class CellReference { private final boolean _isRowAbs; private final boolean _isColAbs; - /** - * Create an cell ref from a string representation. Sheet names containing special characters should be - * delimited and escaped as per normal syntax rules for formulas. - */ - public CellReference(String cellRef) { - String[] parts = separateRefParts(cellRef); - _sheetName = parts[0]; - String colRef = parts[1]; - if (colRef.length() < 1) { - throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); - } - _isColAbs = colRef.charAt(0) == '$'; - if (_isColAbs) { - colRef=colRef.substring(1); - } - _colIndex = convertColStringToIndex(colRef); - - String rowRef=parts[2]; - if (rowRef.length() < 1) { - throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); - } - _isRowAbs = rowRef.charAt(0) == '$'; - if (_isRowAbs) { - rowRef=rowRef.substring(1); - } - _rowIndex = Integer.parseInt(rowRef)-1; // -1 to convert 1-based to zero-based - } + /** + * Create an cell ref from a string representation. Sheet names containing special characters should be + * delimited and escaped as per normal syntax rules for formulas. + */ + public CellReference(String cellRef) { + String[] parts = separateRefParts(cellRef); + _sheetName = parts[0]; + String colRef = parts[1]; + if (colRef.length() < 1) { + throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); + } + _isColAbs = colRef.charAt(0) == '$'; + if (_isColAbs) { + colRef=colRef.substring(1); + } + _colIndex = convertColStringToIndex(colRef); + + String rowRef=parts[2]; + if (rowRef.length() < 1) { + throw new IllegalArgumentException("Invalid Formula cell reference: '"+cellRef+"'"); + } + _isRowAbs = rowRef.charAt(0) == '$'; + if (_isRowAbs) { + rowRef=rowRef.substring(1); + } + _rowIndex = Integer.parseInt(rowRef)-1; // -1 to convert 1-based to zero-based + } - public CellReference(int pRow, int pCol) { - this(pRow, pCol & 0xFFFF, false, false); - } - public CellReference(int pRow, short pCol) { - this(pRow, (int)pCol, false, false); - } + public CellReference(int pRow, int pCol) { + this(pRow, pCol, false, false); + } + public CellReference(int pRow, short pCol) { + this(pRow, pCol & 0xFFFF, false, false); + } - public CellReference(int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { - this(null, pRow, pCol, pAbsRow, pAbsCol); - } - public CellReference(String pSheetName, int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { - // TODO - "-1" is a special value being temporarily used for whole row and whole column area references. - // so these checks are currently N.Q.R. - if(pRow < -1) { - throw new IllegalArgumentException("row index may not be negative"); - } - if(pCol < -1) { - throw new IllegalArgumentException("column index may not be negative"); - } - _sheetName = pSheetName; - _rowIndex=pRow; - _colIndex=pCol; - _isRowAbs = pAbsRow; - _isColAbs=pAbsCol; - } + public CellReference(int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { + this(null, pRow, pCol, pAbsRow, pAbsCol); + } + public CellReference(String pSheetName, int pRow, int pCol, boolean pAbsRow, boolean pAbsCol) { + // TODO - "-1" is a special value being temporarily used for whole row and whole column area references. + // so these checks are currently N.Q.R. + if(pRow < -1) { + throw new IllegalArgumentException("row index may not be negative"); + } + if(pCol < -1) { + throw new IllegalArgumentException("column index may not be negative"); + } + _sheetName = pSheetName; + _rowIndex=pRow; + _colIndex=pCol; + _isRowAbs = pAbsRow; + _isColAbs=pAbsCol; + } - public int getRow(){return _rowIndex;} - public short getCol(){return (short) _colIndex;} - public boolean isRowAbsolute(){return _isRowAbs;} - public boolean isColAbsolute(){return _isColAbs;} - /** - * @return possibly null if this is a 2D reference. Special characters are not - * escaped or delimited - */ - public String getSheetName(){ - return _sheetName; - } - - public static boolean isPartAbsolute(String part) { - return part.charAt(0) == ABSOLUTE_REFERENCE_MARKER; - } - - /** - * takes in a column reference portion of a CellRef and converts it from - * ALPHA-26 number format to 0-based base 10. - * 'A' -> 0 - * 'Z' -> 25 - * 'AA' -> 26 - * 'IV' -> 255 - * @return zero based column index - */ - protected static int convertColStringToIndex(String ref) { - int pos = 0; - int retval=0; + public int getRow(){return _rowIndex;} + public short getCol(){return (short) _colIndex;} + public boolean isRowAbsolute(){return _isRowAbs;} + public boolean isColAbsolute(){return _isColAbs;} + /** + * @return possibly null if this is a 2D reference. Special characters are not + * escaped or delimited + */ + public String getSheetName(){ + return _sheetName; + } + + public static boolean isPartAbsolute(String part) { + return part.charAt(0) == ABSOLUTE_REFERENCE_MARKER; + } + /** + * takes in a column reference portion of a CellRef and converts it from + * ALPHA-26 number format to 0-based base 10. + * 'A' -> 0 + * 'Z' -> 25 + * 'AA' -> 26 + * 'IV' -> 255 + * @return zero based column index + */ + public static int convertColStringToIndex(String ref) { + + int pos = 0; + int retval=0; for (int k = ref.length()-1; k >= 0; k--) { char thechar = ref.charAt(k); - if (thechar == ABSOLUTE_REFERENCE_MARKER) { - if (k != 0) { - throw new IllegalArgumentException("Bad col ref format '" + ref + "'"); - } - break; - } + if (thechar == ABSOLUTE_REFERENCE_MARKER) { + if (k != 0) { + throw new IllegalArgumentException("Bad col ref format '" + ref + "'"); + } + break; + } // Character.getNumericValue() returns the values // 10-35 for the letter A-Z int shift = (int)Math.pow(26, pos); @@ -162,64 +169,70 @@ public class CellReference { pos++; } return retval-1; - } + } - /** - * Classifies an identifier as either a simple (2D) cell reference or a named range name - * @return one of the values from NameType - */ - public static int classifyCellReference(String str) { - int len = str.length(); - if (len < 1) { - throw new IllegalArgumentException("Empty string not allowed"); - } - char firstChar = str.charAt(0); - switch (firstChar) { - case ABSOLUTE_REFERENCE_MARKER: - case '.': - case '_': - break; - default: - if (!Character.isLetter(firstChar)) { - throw new IllegalArgumentException("Invalid first char (" + firstChar - + ") of cell reference or named range. Letter expected"); - } - } - if (!Character.isDigit(str.charAt(len-1))) { - // no digits at end of str - return validateNamedRangeName(str); - } - Matcher cellRefPatternMatcher = CELL_REF_PATTERN.matcher(str); - if (!cellRefPatternMatcher.matches()) { - return validateNamedRangeName(str); - } - String lettersGroup = cellRefPatternMatcher.group(1); - String digitsGroup = cellRefPatternMatcher.group(2); - if (cellReferenceIsWithinRange(lettersGroup, digitsGroup)) { - // valid cell reference - return NameType.CELL; - } - // If str looks like a cell reference, but is out of (row/col) range, it is a valid - // named range name - // This behaviour is a little weird. For example, "IW123" is a valid named range name - // because the column "IW" is beyond the maximum "IV". Note - this behaviour is version - // dependent. In Excel 2007, "IW123" is not a valid named range name. - if (str.indexOf(ABSOLUTE_REFERENCE_MARKER) >= 0) { - // Of course, named range names cannot have '$' - return NameType.BAD_CELL_OR_NAMED_RANGE; - } - return NameType.NAMED_RANGE; - } + /** + * Classifies an identifier as either a simple (2D) cell reference or a named range name + * @return one of the values from NameType + */ + public static int classifyCellReference(String str) { + int len = str.length(); + if (len < 1) { + throw new IllegalArgumentException("Empty string not allowed"); + } + char firstChar = str.charAt(0); + switch (firstChar) { + case ABSOLUTE_REFERENCE_MARKER: + case '.': + case '_': + break; + default: + if (!Character.isLetter(firstChar)) { + throw new IllegalArgumentException("Invalid first char (" + firstChar + + ") of cell reference or named range. Letter expected"); + } + } + if (!Character.isDigit(str.charAt(len-1))) { + // no digits at end of str + return validateNamedRangeName(str); + } + Matcher cellRefPatternMatcher = CELL_REF_PATTERN.matcher(str); + if (!cellRefPatternMatcher.matches()) { + return validateNamedRangeName(str); + } + String lettersGroup = cellRefPatternMatcher.group(1); + String digitsGroup = cellRefPatternMatcher.group(2); + if (cellReferenceIsWithinRange(lettersGroup, digitsGroup)) { + // valid cell reference + return NameType.CELL; + } + // If str looks like a cell reference, but is out of (row/col) range, it is a valid + // named range name + // This behaviour is a little weird. For example, "IW123" is a valid named range name + // because the column "IW" is beyond the maximum "IV". Note - this behaviour is version + // dependent. In BIFF12, "IW123" is not a valid named range name, but in BIFF8 it is. + if (str.indexOf(ABSOLUTE_REFERENCE_MARKER) >= 0) { + // Of course, named range names cannot have '$' + return NameType.BAD_CELL_OR_NAMED_RANGE; + } + return NameType.NAMED_RANGE; + } - private static int validateNamedRangeName(String str) { + private static int validateNamedRangeName(String str) { + Matcher colMatcher = COLUMN_REF_PATTERN.matcher(str); + if (colMatcher.matches()) { + String colStr = colMatcher.group(1); + if (isColumnWithnRange(colStr)) { + return NameType.COLUMN; + } + } if (!NAMED_RANGE_NAME_PATTERN.matcher(str).matches()) { return NameType.BAD_CELL_OR_NAMED_RANGE; } return NameType.NAMED_RANGE; - } - - + + /** * Used to decide whether a name of the form "[A-Z]*[0-9]*" that appears in a formula can be * interpreted as a cell reference. Names of that form can be also used for sheets and/or @@ -240,7 +253,7 @@ public class CellReference { *

* - * + * * * * @@ -257,23 +270,13 @@ public class CellReference { * @return true if the row and col parameters are within range of a BIFF8 spreadsheet. */ public static boolean cellReferenceIsWithinRange(String colStr, String rowStr) { - int numberOfLetters = colStr.length(); - if(numberOfLetters > BIFF8_LAST_COLUMN_TEXT_LEN) { - // "Sheet1" case etc - return false; // that was easy + if (!isColumnWithnRange(colStr)) { + return false; } int nDigits = rowStr.length(); if(nDigits > BIFF8_LAST_ROW_TEXT_LEN) { return false; } - if(numberOfLetters == BIFF8_LAST_COLUMN_TEXT_LEN) { - if(colStr.toUpperCase().compareTo(BIFF8_LAST_COLUMN) > 0) { - return false; - } - } else { - // apparent column name has less chars than max - // no need to check range - } if(nDigits == BIFF8_LAST_ROW_TEXT_LEN) { // ASCII comparison is valid if digit count is same @@ -288,87 +291,104 @@ public class CellReference { return true; } + private static boolean isColumnWithnRange(String colStr) { + int numberOfLetters = colStr.length(); + if(numberOfLetters > BIFF8_LAST_COLUMN_TEXT_LEN) { + // "Sheet1" case etc + return false; // that was easy + } + if(numberOfLetters == BIFF8_LAST_COLUMN_TEXT_LEN) { + if(colStr.toUpperCase().compareTo(BIFF8_LAST_COLUMN) > 0) { + return false; + } + } else { + // apparent column name has less chars than max + // no need to check range + } + return true; + } + /** - * Separates the row from the columns and returns an array of three Strings. The first element - * is the sheet name. Only the first element may be null. The second element in is the column - * name still in ALPHA-26 number format. The third element is the row. - */ - private static String[] separateRefParts(String reference) { - - int plingPos = reference.lastIndexOf(SHEET_NAME_DELIMITER); - String sheetName = parseSheetName(reference, plingPos); - int start = plingPos+1; + * Separates the row from the columns and returns an array of three Strings. The first element + * is the sheet name. Only the first element may be null. The second element in is the column + * name still in ALPHA-26 number format. The third element is the row. + */ + private static String[] separateRefParts(String reference) { + + int plingPos = reference.lastIndexOf(SHEET_NAME_DELIMITER); + String sheetName = parseSheetName(reference, plingPos); + int start = plingPos+1; - int length = reference.length(); + int length = reference.length(); - int loc = start; - // skip initial dollars - if (reference.charAt(loc)==ABSOLUTE_REFERENCE_MARKER) { - loc++; - } - // step over column name chars until first digit (or dollars) for row number. - for (; loc < length; loc++) { - char ch = reference.charAt(loc); - if (Character.isDigit(ch) || ch == ABSOLUTE_REFERENCE_MARKER) { - break; - } - } - return new String[] { - sheetName, - reference.substring(start,loc), - reference.substring(loc), - }; - } + int loc = start; + // skip initial dollars + if (reference.charAt(loc)==ABSOLUTE_REFERENCE_MARKER) { + loc++; + } + // step over column name chars until first digit (or dollars) for row number. + for (; loc < length; loc++) { + char ch = reference.charAt(loc); + if (Character.isDigit(ch) || ch == ABSOLUTE_REFERENCE_MARKER) { + break; + } + } + return new String[] { + sheetName, + reference.substring(start,loc), + reference.substring(loc), + }; + } - private static String parseSheetName(String reference, int indexOfSheetNameDelimiter) { - if(indexOfSheetNameDelimiter < 0) { - return null; - } - - boolean isQuoted = reference.charAt(0) == SPECIAL_NAME_DELIMITER; - if(!isQuoted) { - return reference.substring(0, indexOfSheetNameDelimiter); - } - int lastQuotePos = indexOfSheetNameDelimiter-1; - if(reference.charAt(lastQuotePos) != SPECIAL_NAME_DELIMITER) { - throw new RuntimeException("Mismatched quotes: (" + reference + ")"); - } + private static String parseSheetName(String reference, int indexOfSheetNameDelimiter) { + if(indexOfSheetNameDelimiter < 0) { + return null; + } + + boolean isQuoted = reference.charAt(0) == SPECIAL_NAME_DELIMITER; + if(!isQuoted) { + return reference.substring(0, indexOfSheetNameDelimiter); + } + int lastQuotePos = indexOfSheetNameDelimiter-1; + if(reference.charAt(lastQuotePos) != SPECIAL_NAME_DELIMITER) { + throw new RuntimeException("Mismatched quotes: (" + reference + ")"); + } - // TODO - refactor cell reference parsing logic to one place. - // Current known incarnations: - // FormulaParser.GetName() - // CellReference.parseSheetName() (here) - // AreaReference.separateAreaRefs() - // SheetNameFormatter.format() (inverse) - - StringBuffer sb = new StringBuffer(indexOfSheetNameDelimiter); - - for(int i=1; i D - */ - protected static String convertNumToColString(int col) { + /** + * Takes in a 0-based base-10 column and returns a ALPHA-26 + * representation. + * eg column #3 -> D + */ + protected static String convertNumToColString(int col) { // Excel counts column A as the 1st column, we // treat it as the 0th one int excelColNum = col + 1; @@ -387,36 +407,36 @@ public class CellReference { } return colRef; - } + } + + /** + * Example return values: + *
Input           Result 
Result 
"A", "1"true
"a", "111"true
"A", "65536"true
+ * + * + * + * + *
ResultComment
A1Cell reference without sheet
Sheet1!A1Standard sheet name
'O''Brien''s Sales'!A1' Sheet name with special characters
+ * @return the text representation of this cell reference as it would appear in a formula. + */ + public String formatAsString() { + StringBuffer sb = new StringBuffer(32); + if(_sheetName != null) { + SheetNameFormatter.appendFormat(sb, _sheetName); + sb.append(SHEET_NAME_DELIMITER); + } + appendCellReference(sb); + return sb.toString(); + } + + public String toString() { + StringBuffer sb = new StringBuffer(64); + sb.append(getClass().getName()).append(" ["); + sb.append(formatAsString()); + sb.append("]"); + return sb.toString(); + } - /** - * Example return values: - * - * - * - * - * - *
ResultComment
A1Cell reference without sheet
Sheet1!A1Standard sheet name
'O''Brien''s Sales'!A1' Sheet name with special characters
- * @return the text representation of this cell reference as it would appear in a formula. - */ - public String formatAsString() { - StringBuffer sb = new StringBuffer(32); - if(_sheetName != null) { - SheetNameFormatter.appendFormat(sb, _sheetName); - sb.append(SHEET_NAME_DELIMITER); - } - appendCellReference(sb); - return sb.toString(); - } - - public String toString() { - StringBuffer sb = new StringBuffer(64); - sb.append(getClass().getName()).append(" ["); - sb.append(formatAsString()); - sb.append("]"); - return sb.toString(); - } - /** * Returns the three parts of the cell reference, the * Sheet name (or null if none supplied), the 1 based @@ -433,18 +453,18 @@ public class CellReference { }; } - /** - * Appends cell reference with '$' markers for absolute values as required. - * Sheet name is not included. - */ - protected void appendCellReference(StringBuffer sb) { - if(_isColAbs) { - sb.append(ABSOLUTE_REFERENCE_MARKER); - } - sb.append( convertNumToColString(_colIndex)); - if(_isRowAbs) { - sb.append(ABSOLUTE_REFERENCE_MARKER); - } - sb.append(_rowIndex+1); - } + /** + * Appends cell reference with '$' markers for absolute values as required. + * Sheet name is not included. + */ + /* package */ void appendCellReference(StringBuffer sb) { + if(_isColAbs) { + sb.append(ABSOLUTE_REFERENCE_MARKER); + } + sb.append( convertNumToColString(_colIndex)); + if(_isRowAbs) { + sb.append(ABSOLUTE_REFERENCE_MARKER); + } + sb.append(_rowIndex+1); + } } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java index 7c03ebe7f..ddadf96eb 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFEvaluationWorkbook.java @@ -57,10 +57,6 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return convertToExternalSheetIndex(sheetIndex); } - public EvaluationName getName(int index) { - return new Name(_uBook.getNameAt(index), index, this); - } - public EvaluationName getName(String name) { for(int i=0; i < _uBook.getNumberOfNames(); i++) { String nameText = _uBook.getNameName(i); @@ -88,14 +84,15 @@ public final class XSSFEvaluationWorkbook implements FormulaRenderingWorkbook, E return _uBook.getSheetAt(sheetIndex); } - /** - * Doesn't do anything - returns the same index - * TODO - figure out if this is a ole2 specific thing, or - * if we need to do something proper here too! - */ - public Sheet getSheetByExternSheetIndex(int externSheetIndex) { - int sheetIndex = convertFromExternalSheetIndex(externSheetIndex); - return _uBook.getSheetAt(sheetIndex); + public ExternalSheet getExternalSheet(int externSheetIndex) { + // TODO Auto-generated method stub + return null; + } + public int getExternalSheetIndex(String workbookName, String sheetName) { + throw new RuntimeException("not implemented yet"); + } + public int getSheetIndex(String sheetName) { + return _uBook.getSheetIndex(sheetName); } public Workbook getWorkbook() { diff --git a/src/testcases/org/apache/poi/hssf/HSSFTests.java b/src/testcases/org/apache/poi/hssf/HSSFTests.java index aa5bf46d3..d5a491099 100644 --- a/src/testcases/org/apache/poi/hssf/HSSFTests.java +++ b/src/testcases/org/apache/poi/hssf/HSSFTests.java @@ -28,7 +28,7 @@ import org.apache.poi.hssf.model.AllModelTests; import org.apache.poi.hssf.record.AllRecordTests; import org.apache.poi.hssf.usermodel.AllUserModelTests; import org.apache.poi.hssf.util.AllHSSFUtilTests; -import org.apache.poi.ss.formula.TestEvaluationCache; +import org.apache.poi.ss.formula.AllSSFormulaTests; /** * Test Suite for all sub-packages of org.apache.poi.hssf
@@ -53,7 +53,7 @@ public final class HSSFTests { } suite.addTest(new TestSuite(TestEventRecordFactory.class)); suite.addTest(new TestSuite(TestModelFactory.class)); - suite.addTest(new TestSuite(TestEvaluationCache.class)); + suite.addTest(AllSSFormulaTests.suite()); return suite; } } diff --git a/src/testcases/org/apache/poi/hssf/data/multibookFormulaA.xls b/src/testcases/org/apache/poi/hssf/data/multibookFormulaA.xls new file mode 100644 index 000000000..b844fc1db Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/multibookFormulaA.xls differ diff --git a/src/testcases/org/apache/poi/hssf/data/multibookFormulaB.xls b/src/testcases/org/apache/poi/hssf/data/multibookFormulaB.xls new file mode 100644 index 000000000..3e4708d6c Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/multibookFormulaB.xls differ diff --git a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java index 016fa8398..53061a228 100644 --- a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java +++ b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java @@ -43,11 +43,13 @@ import org.apache.poi.hssf.record.formula.NumberPtg; import org.apache.poi.hssf.record.formula.PercentPtg; import org.apache.poi.hssf.record.formula.PowerPtg; import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.Ref3DPtg; import org.apache.poi.hssf.record.formula.RefPtg; import org.apache.poi.hssf.record.formula.StringPtg; import org.apache.poi.hssf.record.formula.SubtractPtg; import org.apache.poi.hssf.record.formula.UnaryMinusPtg; import org.apache.poi.hssf.record.formula.UnaryPlusPtg; +import org.apache.poi.hssf.usermodel.FormulaExtractor; import org.apache.poi.hssf.usermodel.HSSFCell; import org.apache.poi.hssf.usermodel.HSSFErrorConstants; import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook; @@ -553,14 +555,14 @@ public final class TestFormulaParser extends TestCase { Class[] expClss; - expClss = new Class[] { - RefPtg.class, + expClss = new Class[] { + RefPtg.class, AttrPtg.class, // tAttrIf - MissingArgPtg.class, + MissingArgPtg.class, AttrPtg.class, // tAttrSkip RefPtg.class, AttrPtg.class, // tAttrSkip - FuncVarPtg.class, + FuncVarPtg.class, }; confirmTokenClasses("if(A1, ,C1)", expClss); @@ -735,10 +737,10 @@ public final class TestFormulaParser extends TestCase { /** * Make sure that POI uses the right Func Ptg when encoding formulas. Functions with variable * number of args should get FuncVarPtg, functions with fixed args should get FuncPtg.

- * + * * Prior to the fix for bug 44675 POI would encode FuncVarPtg for all functions. In many cases - * Excel tolerates the wrong Ptg and evaluates the formula OK (e.g. SIN), but in some cases - * (e.g. COUNTIF) Excel fails to evaluate the formula, giving '#VALUE!' instead. + * Excel tolerates the wrong Ptg and evaluates the formula OK (e.g. SIN), but in some cases + * (e.g. COUNTIF) Excel fails to evaluate the formula, giving '#VALUE!' instead. */ public void testFuncPtgSelection() { @@ -777,7 +779,7 @@ public final class TestFormulaParser extends TestCase { parseFormula("round(3.14;2)"); throw new AssertionFailedError("Didn't get parse exception as expected"); } catch (RuntimeException e) { - FormulaParserTestHelper.confirmParseException(e, + FormulaParserTestHelper.confirmParseException(e, "Parse error near char 10 ';' in specified formula 'round(3.14;2)'. Expected ',' or ')'"); } @@ -785,11 +787,11 @@ public final class TestFormulaParser extends TestCase { parseFormula(" =2+2"); throw new AssertionFailedError("Didn't get parse exception as expected"); } catch (RuntimeException e) { - FormulaParserTestHelper.confirmParseException(e, + FormulaParserTestHelper.confirmParseException(e, "The specified formula ' =2+2' starts with an equals sign which is not allowed."); } } - + /** * this function name has a dot in it. */ @@ -798,8 +800,8 @@ public final class TestFormulaParser extends TestCase { Ptg[] ptgs; try { ptgs = parseFormula("error.type(A1)"); - - + + } catch (IllegalArgumentException e) { if (e.getMessage().equals("Invalid Formula cell reference: 'error'")) { throw new AssertionFailedError("Identified bug 45334"); @@ -811,7 +813,7 @@ public final class TestFormulaParser extends TestCase { FuncPtg funcPtg = (FuncPtg) ptgs[1]; assertEquals("ERROR.TYPE", funcPtg.getName()); } - + public void testNamedRangeThatLooksLikeCell() { HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet sheet = wb.createSheet("Sheet1"); @@ -838,35 +840,35 @@ public final class TestFormulaParser extends TestCase { cell.setCellFormula("count(pf1)"); throw new AssertionFailedError("Expected formula parse execption"); } catch (RuntimeException e) { - FormulaParserTestHelper.confirmParseException(e, + FormulaParserTestHelper.confirmParseException(e, "Specified named range 'pf1' does not exist in the current workbook."); } cell.setCellFormula("count(fp1)"); // plain cell ref, col is in range } - + public void testParseAreaRefHighRow_bug45358() { Ptg[] ptgs; AreaI aptg; - + HSSFWorkbook book = new HSSFWorkbook(); book.createSheet("Sheet1"); - + ptgs = HSSFFormulaParser.parse("Sheet1!A10:A40000", book); aptg = (AreaI) ptgs[0]; if (aptg.getLastRow() == -25537) { throw new AssertionFailedError("Identified bug 45358"); } assertEquals(39999, aptg.getLastRow()); - + ptgs = HSSFFormulaParser.parse("Sheet1!A10:A65536", book); aptg = (AreaI) ptgs[0]; assertEquals(65535, aptg.getLastRow()); - + // plain area refs should be ok too ptgs = parseFormula("A10:A65536"); aptg = (AreaI) ptgs[0]; assertEquals(65535, aptg.getLastRow()); - + } public void testParseArray() { Ptg[] ptgs; @@ -875,11 +877,71 @@ public final class TestFormulaParser extends TestCase { Ptg ptg0 = ptgs[0]; assertEquals(ArrayPtg.class, ptg0.getClass()); assertEquals("{1.0,2.0,2.0,#REF!;FALSE,3.0,3.0,2.0}", ptg0.toFormulaString()); - + ArrayPtg aptg = (ArrayPtg) ptg0; Object[][] values = aptg.getTokenArrayValues(); assertEquals(ErrorConstant.valueOf(HSSFErrorConstants.ERROR_REF), values[0][3]); assertEquals(Boolean.FALSE, values[1][0]); - } -} \ No newline at end of file + + public void testRangeOperator() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + HSSFCell cell = sheet.createRow(0).createCell(0); + + wb.setSheetName(0, "Sheet1"); + cell.setCellFormula("Sheet1!B$4:Sheet1!$C1"); // explicit range ':' operator + assertEquals("Sheet1!B$4:Sheet1!$C1", cell.getCellFormula()); + + cell.setCellFormula("Sheet1!B$4:$C1"); // plain area ref + assertEquals("Sheet1!B1:$C$4", cell.getCellFormula()); // note - area ref is normalised + + cell.setCellFormula("Sheet1!$C1...B$4"); // different syntax for plain area ref + assertEquals("Sheet1!B1:$C$4", cell.getCellFormula()); + + // with funny sheet name + wb.setSheetName(0, "A1...A2"); + cell.setCellFormula("A1...A2!B1"); + assertEquals("A1...A2!B1", cell.getCellFormula()); + } + + public void testBooleanNamedSheet() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet("true"); + HSSFCell cell = sheet.createRow(0).createCell(0); + cell.setCellFormula("'true'!B2"); + + assertEquals("'true'!B2", cell.getCellFormula()); + } + + public void testParseExternalWorkbookReference() { + HSSFWorkbook wbA = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaA.xls"); + HSSFCell cell = wbA.getSheetAt(0).getRow(0).getCell(0); + + // make sure formula in sample is as expected + assertEquals("[multibookFormulaB.xls]BSheet1!B1", cell.getCellFormula()); + Ptg[] expectedPtgs = FormulaExtractor.getPtgs(cell); + confirmSingle3DRef(expectedPtgs, 1); + + // now try (re-)parsing the formula + Ptg[] actualPtgs = HSSFFormulaParser.parse("[multibookFormulaB.xls]BSheet1!B1", wbA); + confirmSingle3DRef(actualPtgs, 1); // externalSheetIndex 1 -> BSheet1 + + // try parsing a formula pointing to a different external sheet + Ptg[] otherPtgs = HSSFFormulaParser.parse("[multibookFormulaB.xls]AnotherSheet!B1", wbA); + confirmSingle3DRef(otherPtgs, 0); // externalSheetIndex 0 -> AnotherSheet + + // try setting the same formula in a cell + cell.setCellFormula("[multibookFormulaB.xls]AnotherSheet!B1"); + assertEquals("[multibookFormulaB.xls]AnotherSheet!B1", cell.getCellFormula()); + } + private static void confirmSingle3DRef(Ptg[] ptgs, int expectedExternSheetIndex) { + assertEquals(1, ptgs.length); + Ptg ptg0 = ptgs[0]; + assertEquals(Ref3DPtg.class, ptg0.getClass()); + assertEquals(expectedExternSheetIndex, ((Ref3DPtg)ptg0).getExternSheetIndex()); + } + +} diff --git a/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java b/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java index 3727989ef..e2a61dde7 100644 --- a/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java +++ b/src/testcases/org/apache/poi/hssf/record/TestSupBookRecord.java @@ -78,7 +78,7 @@ public final class TestSupBookRecord extends TestCase { assertEquals( 34, record.getRecordSize() ); //sid+size+data - assertEquals("testURL", record.getURL().getString()); + assertEquals("testURL", record.getURL()); UnicodeString[] sheetNames = record.getSheetNames(); assertEquals(2, sheetNames.length); assertEquals("Sheet1", sheetNames[0].getString()); diff --git a/src/testcases/org/apache/poi/hssf/record/formula/TestSheetNameFormatter.java b/src/testcases/org/apache/poi/hssf/record/formula/TestSheetNameFormatter.java index 369c09583..b7c1664ee 100644 --- a/src/testcases/org/apache/poi/hssf/record/formula/TestSheetNameFormatter.java +++ b/src/testcases/org/apache/poi/hssf/record/formula/TestSheetNameFormatter.java @@ -20,16 +20,12 @@ package org.apache.poi.hssf.record.formula; import junit.framework.TestCase; /** - * Tests for SheetNameFormatter + * Tests for {@link SheetNameFormatter} * * @author Josh Micich */ public final class TestSheetNameFormatter extends TestCase { - public TestSheetNameFormatter(String testName) { - super(testName); - } - private static void confirmFormat(String rawSheetName, String expectedSheetNameEncoding) { assertEquals(expectedSheetNameEncoding, SheetNameFormatter.format(rawSheetName)); } @@ -55,6 +51,16 @@ public final class TestSheetNameFormatter extends TestCase { confirmFormat("TAXRETURN19980415", "TAXRETURN19980415"); } + public void testBooleanLiterals() { + confirmFormat("TRUE", "'TRUE'"); + confirmFormat("FALSE", "'FALSE'"); + confirmFormat("True", "'True'"); + confirmFormat("fAlse", "'fAlse'"); + + confirmFormat("Yes", "Yes"); + confirmFormat("No", "No"); + } + private static void confirmCellNameMatch(String rawSheetName, boolean expected) { assertEquals(expected, SheetNameFormatter.nameLooksLikePlainCellReference(rawSheetName)); } diff --git a/src/testcases/org/apache/poi/hssf/record/formula/eval/AllFormulaEvalTests.java b/src/testcases/org/apache/poi/hssf/record/formula/eval/AllFormulaEvalTests.java index 755106194..fe37a3c84 100755 --- a/src/testcases/org/apache/poi/hssf/record/formula/eval/AllFormulaEvalTests.java +++ b/src/testcases/org/apache/poi/hssf/record/formula/eval/AllFormulaEvalTests.java @@ -37,6 +37,7 @@ public class AllFormulaEvalTests { result.addTestSuite(TestFormulaBugs.class); result.addTestSuite(TestFormulasFromSpreadsheet.class); result.addTestSuite(TestPercentEval.class); + result.addTestSuite(TestRangeEval.class); result.addTestSuite(TestUnaryPlusEval.class); return result; } diff --git a/src/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulaBugs.java b/src/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulaBugs.java index 45b0f3a65..1b97905ed 100755 --- a/src/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulaBugs.java +++ b/src/testcases/org/apache/poi/hssf/record/formula/eval/TestFormulaBugs.java @@ -146,7 +146,7 @@ public final class TestFormulaBugs extends TestCase { throw new AssertionFailedError("Identified bug 42448"); } - assertEquals("SUMPRODUCT(A!C7:C67,B8:B68)/B69", cell.getCellFormula()); + assertEquals("SUMPRODUCT(A!C7:A!C67,B8:B68)/B69", cell.getCellFormula()); // might as well evaluate the sucker... diff --git a/src/testcases/org/apache/poi/hssf/record/formula/eval/TestRangeEval.java b/src/testcases/org/apache/poi/hssf/record/formula/eval/TestRangeEval.java new file mode 100644 index 000000000..e98517a73 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/record/formula/eval/TestRangeEval.java @@ -0,0 +1,95 @@ +/* ==================================================================== + 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.hssf.record.formula.eval; + +import org.apache.poi.hssf.record.formula.AreaI; +import org.apache.poi.hssf.record.formula.AreaI.OffsetArea; +import org.apache.poi.hssf.util.AreaReference; +import org.apache.poi.hssf.util.CellReference; + +import junit.framework.TestCase; + +/** + * Test for unary plus operator evaluator. + * + * @author Josh Micich + */ +public final class TestRangeEval extends TestCase { + + public void testPermutations() { + + confirm("B3", "D7", "B3:D7"); + confirm("B1", "B1", "B1:B1"); + + confirm("B7", "D3", "B3:D7"); + confirm("D3", "B7", "B3:D7"); + confirm("D7", "B3", "B3:D7"); + } + + private static void confirm(String refA, String refB, String expectedAreaRef) { + + Eval[] args = { + createRefEval(refA), + createRefEval(refB), + }; + AreaReference ar = new AreaReference(expectedAreaRef); + Eval result = RangeEval.instance.evaluate(args, 0, (short)0); + assertTrue(result instanceof AreaEval); + AreaEval ae = (AreaEval) result; + assertEquals(ar.getFirstCell().getRow(), ae.getFirstRow()); + assertEquals(ar.getLastCell().getRow(), ae.getLastRow()); + assertEquals(ar.getFirstCell().getCol(), ae.getFirstColumn()); + assertEquals(ar.getLastCell().getCol(), ae.getLastColumn()); + } + + private static Eval createRefEval(String refStr) { + CellReference cr = new CellReference(refStr); + return new MockRefEval(cr.getRow(), cr.getCol()); + + } + + private static final class MockRefEval extends RefEvalBase { + + public MockRefEval(int rowIndex, int columnIndex) { + super(rowIndex, columnIndex); + } + public ValueEval getInnerValueEval() { + throw new RuntimeException("not expected to be called during this test"); + } + public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, + int relLastColIx) { + AreaI area = new OffsetArea(getRow(), getColumn(), + relFirstRowIx, relLastRowIx, relFirstColIx, relLastColIx); + return new MockAreaEval(area); + } + } + + private static final class MockAreaEval extends AreaEvalBase { + + public MockAreaEval(AreaI ptg) { + super(ptg); + } + public ValueEval getRelativeValue(int relativeRowIndex, int relativeColumnIndex) { + throw new RuntimeException("not expected to be called during this test"); + } + public AreaEval offset(int relFirstRowIx, int relLastRowIx, int relFirstColIx, + int relLastColIx) { + throw new RuntimeException("not expected to be called during this test"); + } + } +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java index 397c1e52c..03f47d681 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java @@ -18,7 +18,6 @@ package org.apache.poi.hssf.usermodel; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Date; @@ -42,12 +41,8 @@ public final class TestFormulas extends TestCase { /** * Add 1+1 -- WHoohoo! */ + public void testBasicAddIntegers() { - public void testBasicAddIntegers() - throws Exception { - - File file = TempFile.createTempFile("testFormula",".xls"); - FileOutputStream out = new FileOutputStream(file); HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet s = wb.createSheet(); HSSFRow r = null; @@ -58,95 +53,78 @@ public final class TestFormulas extends TestCase { c = r.createCell(1); c.setCellFormula(1 + "+" + 1); - wb.write(out); - out.close(); - - FileInputStream in = new FileInputStream(file); - wb = new HSSFWorkbook(in); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); s = wb.getSheetAt(0); r = s.getRow(1); c = r.getCell(1); assertTrue("Formula is as expected",("1+1".equals(c.getCellFormula()))); - in.close(); } /** * Add various integers */ - - public void testAddIntegers() - throws Exception { + public void testAddIntegers() { binomialOperator("+"); } /** * Multiply various integers */ - - public void testMultplyIntegers() - throws Exception { + public void testMultplyIntegers() { binomialOperator("*"); } /** * Subtract various integers */ - public void testSubtractIntegers() - throws Exception { + public void testSubtractIntegers() { binomialOperator("-"); } /** * Subtract various integers */ - public void testDivideIntegers() - throws Exception { + public void testDivideIntegers() { binomialOperator("/"); } /** * Exponentialize various integers; */ - public void testPowerIntegers() - throws Exception { + public void testPowerIntegers() { binomialOperator("^"); } /** - * Concatinate two numbers 1&2 = 12 + * Concatenate two numbers 1&2 = 12 */ - public void testConcatIntegers() - throws Exception { + public void testConcatIntegers() { binomialOperator("&"); } /** * tests 1*2+3*4 */ - public void testOrderOfOperationsMultiply() - throws Exception { + public void testOrderOfOperationsMultiply() { orderTest("1*2+3*4"); } /** * tests 1*2+3^4 */ - public void testOrderOfOperationsPower() - throws Exception { + public void testOrderOfOperationsPower() { orderTest("1*2+3^4"); } /** * Tests that parenthesis are obeyed */ - public void testParenthesis() - throws Exception { + public void testParenthesis() { orderTest("(1*3)+2+(1+2)*(3^4)^5"); } - public void testReferencesOpr() - throws Exception { + public void testReferencesOpr() { String[] operation = new String[] { "+", "-", "*", "/", "^", "&" }; @@ -159,16 +137,12 @@ public final class TestFormulas extends TestCase { * Tests creating a file with floating point in a formula. * */ - public void testFloat() - throws Exception { + public void testFloat() { floatTest("*"); floatTest("/"); } - private void floatTest(String operator) - throws Exception { - File file = TempFile.createTempFile("testFormulaFloat",".xls"); - FileOutputStream out = new FileOutputStream(file); + private static void floatTest(String operator) { HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet s = wb.createSheet(); HSSFRow r = null; @@ -180,10 +154,10 @@ public final class TestFormulas extends TestCase { c = r.createCell(1); c.setCellFormula(""+Float.MIN_VALUE + operator + Float.MIN_VALUE); - for (short x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2) ) { + for (int x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2) ) { r = s.createRow(x); - for (short y = 1; y < 256 && y > 0; y= (short) (y +2)) { + for (int y = 1; y < 256 && y > 0; y= (short) (y +2)) { c = r.createCell(y); c.setCellFormula("" + x+"."+y + operator + y +"."+x); @@ -196,69 +170,48 @@ public final class TestFormulas extends TestCase { c = r.createCell(0); c.setCellFormula("" + Float.MAX_VALUE + operator + Float.MAX_VALUE); } - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); - out=null;wb=null; //otherwise we get out of memory error! - floatVerify(operator,file); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + floatVerify(operator, wb); } - private void floatVerify(String operator, File file) - throws Exception { + private static void floatVerify(String operator, HSSFWorkbook wb) { - FileInputStream in = new FileInputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(in); HSSFSheet s = wb.getSheetAt(0); - HSSFRow r = null; - HSSFCell c = null; - // dont know how to check correct result .. for the moment, we just verify that the file can be read. + // don't know how to check correct result .. for the moment, we just verify that the file can be read. - for (short x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2)) { - r = s.getRow(x); + for (int x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2)) { + HSSFRow r = s.getRow(x); - for (short y = 1; y < 256 && y > 0; y=(short)(y+2)) { + for (int y = 1; y < 256 && y > 0; y=(short)(y+2)) { - c = r.getCell(y); + HSSFCell c = r.getCell(y); assertTrue("got a formula",c.getCellFormula()!=null); assertTrue("loop Formula is as expected "+x+"."+y+operator+y+"."+x+"!="+c.getCellFormula(),( (""+x+"."+y+operator+y+"."+x).equals(c.getCellFormula()) )); - } } - - in.close(); - assertTrue("file exists",file.exists()); } - public void testAreaSum() - throws Exception { + public void testAreaSum() { areaFunctionTest("SUM"); } - public void testAreaAverage() - throws Exception { + public void testAreaAverage() { areaFunctionTest("AVERAGE"); } - public void testRefArraySum() - throws Exception { + public void testRefArraySum() { refArrayFunctionTest("SUM"); } - public void testAreaArraySum() - throws Exception { + public void testAreaArraySum() { refAreaArrayFunctionTest("SUM"); } - - - private void operationRefTest(String operator) - throws Exception { - File file = TempFile.createTempFile("testFormula",".xls"); - FileOutputStream out = new FileOutputStream(file); + private static void operationRefTest(String operator) { HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet s = wb.createSheet(); HSSFRow r = null; @@ -317,21 +270,16 @@ public final class TestFormulas extends TestCase { c.setCellFormula("" + "B1" + operator + "IV255"); } - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); - operationalRefVerify(operator,file); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + operationalRefVerify(operator, wb); } /** * Opens the sheet we wrote out by binomialOperator and makes sure the formulas * all match what we expect (x operator y) */ - private void operationalRefVerify(String operator, File file) - throws Exception { + private static void operationalRefVerify(String operator, HSSFWorkbook wb) { - FileInputStream in = new FileInputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(in); HSSFSheet s = wb.getSheetAt(0); HSSFRow r = null; HSSFCell c = null; @@ -345,46 +293,43 @@ public final class TestFormulas extends TestCase { )); - for (short x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2)) { + for (int x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2)) { r = s.getRow(x); - for (short y = 1; y < 256 && y > 0; y++) { + for (int y = 1; y < 256 && y > 0; y++) { - String ref=null; - String ref2=null; - short refx1=0; - short refy1=0; - short refx2=0; - short refy2=0; + int refx1; + int refy1; + int refx2; + int refy2; if (x +50 < Short.MAX_VALUE) { - refx1=(short)(x+50); - refx2=(short)(x+46); + refx1=x+50; + refx2=x+46; } else { - refx1=(short)(x-4); - refx2=(short)(x-3); + refx1=x-4; + refx2=x-3; } if (y+50 < 255) { - refy1=(short)(y+50); - refy2=(short)(y+49); + refy1=y+50; + refy2=y+49; } else { - refy1=(short)(y-4); - refy2=(short)(y-3); + refy1=y-4; + refy2=y-3; } c = r.getCell(y); CellReference cr= new CellReference(refx1, refy1, false, false); + String ref=cr.formatAsString(); ref=cr.formatAsString(); cr=new CellReference(refx2,refy2, false, false); - ref2=cr.formatAsString(); + String ref2=cr.formatAsString(); assertTrue("loop Formula is as expected "+ref+operator+ref2+"!="+c.getCellFormula(),( (""+ref+operator+ref2).equals(c.getCellFormula()) ) ); - - } } @@ -392,13 +337,7 @@ public final class TestFormulas extends TestCase { r = s.getRow(0); c = r.getCell(0); - assertTrue("maxval Formula is as expected",( - ("B1"+operator+"IV255").equals(c.getCellFormula()) - ) - ); - - in.close(); - assertTrue("file exists",file.exists()); + assertEquals("B1"+operator+"IV255", c.getCellFormula()); } @@ -406,10 +345,7 @@ public final class TestFormulas extends TestCase { /** * tests order wrting out == order writing in for a given formula */ - private void orderTest(String formula) - throws Exception { - File file = TempFile.createTempFile("testFormula",".xls"); - FileOutputStream out = new FileOutputStream(file); + private static void orderTest(String formula) { HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet s = wb.createSheet(); HSSFRow r = null; @@ -420,12 +356,7 @@ public final class TestFormulas extends TestCase { c = r.createCell(1); c.setCellFormula(formula); - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); - - FileInputStream in = new FileInputStream(file); - wb = new HSSFWorkbook(in); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); s = wb.getSheetAt(0); //get our minimum values @@ -434,8 +365,6 @@ public final class TestFormulas extends TestCase { assertTrue("minval Formula is as expected", formula.equals(c.getCellFormula()) ); - - in.close(); } /** @@ -443,10 +372,7 @@ public final class TestFormulas extends TestCase { * huge set of x operator y formulas. Next we call binomialVerify and verify * that they are all how we expect. */ - private void binomialOperator(String operator) - throws Exception { - File file = TempFile.createTempFile("testFormula",".xls"); - FileOutputStream out = new FileOutputStream(file); + private static void binomialOperator(String operator) { HSSFWorkbook wb = new HSSFWorkbook(); HSSFSheet s = wb.createSheet(); HSSFRow r = null; @@ -474,23 +400,15 @@ public final class TestFormulas extends TestCase { c = r.createCell(0); c.setCellFormula("" + Short.MAX_VALUE + operator + Short.MAX_VALUE); } - - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); - - binomialVerify(operator,file); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + binomialVerify(operator, wb); } /** * Opens the sheet we wrote out by binomialOperator and makes sure the formulas * all match what we expect (x operator y) */ - private void binomialVerify(String operator, File file) - throws Exception { - - FileInputStream in = new FileInputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(in); + private static void binomialVerify(String operator, HSSFWorkbook wb) { HSSFSheet s = wb.getSheetAt(0); HSSFRow r = null; HSSFCell c = null; @@ -502,10 +420,10 @@ public final class TestFormulas extends TestCase { ( ("1"+operator+"1").equals(c.getCellFormula()) )); - for (short x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2)) { + for (int x = 1; x < Short.MAX_VALUE && x > 0; x=(short)(x*2)) { r = s.getRow(x); - for (short y = 1; y < 256 && y > 0; y++) { + for (int y = 1; y < 256 && y > 0; y++) { c = r.getCell(y); @@ -513,8 +431,6 @@ public final class TestFormulas extends TestCase { (""+x+operator+y).equals(c.getCellFormula()) ) ); - - } } @@ -522,90 +438,62 @@ public final class TestFormulas extends TestCase { r = s.getRow(0); c = r.getCell(0); - assertTrue("maxval Formula is as expected",( (""+Short.MAX_VALUE+operator+Short.MAX_VALUE).equals(c.getCellFormula()) ) ); - - in.close(); - assertTrue("file exists",file.exists()); - } - - - - /** - * Writes a function then tests to see if its correct - * - */ - public void areaFunctionTest(String function) - throws Exception { - - File file = TempFile.createTempFile("testFormulaAreaFunction"+function,".xls"); - FileOutputStream out = new FileOutputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(); - HSSFSheet s = wb.createSheet(); - HSSFRow r = null; - HSSFCell c = null; - - - r = s.createRow(0); - - c = r.createCell(0); - c.setCellFormula(function+"(A2:A3)"); - - - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); - - FileInputStream in = new FileInputStream(file); - wb = new HSSFWorkbook(in); - s = wb.getSheetAt(0); - r = s.getRow(0); - c = r.getCell(0); - - assertTrue("function ="+function+"(A2:A3)", - ( (function+"(A2:A3)").equals((function+"(A2:A3)")) ) - ); - in.close(); } /** * Writes a function then tests to see if its correct - * */ - public void refArrayFunctionTest(String function) - throws Exception { + public static void areaFunctionTest(String function) { - File file = TempFile.createTempFile("testFormulaArrayFunction"+function,".xls"); - FileOutputStream out = new FileOutputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(); - HSSFSheet s = wb.createSheet(); - HSSFRow r = null; - HSSFCell c = null; + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet(); + HSSFRow r = null; + HSSFCell c = null; - r = s.createRow(0); + r = s.createRow(0); - c = r.createCell(0); - c.setCellFormula(function+"(A2,A3)"); + c = r.createCell(0); + c.setCellFormula(function+"(A2:A3)"); + + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + s = wb.getSheetAt(0); + r = s.getRow(0); + c = r.getCell(0); + + assertTrue("function ="+function+"(A2:A3)", + ( (function+"(A2:A3)").equals((function+"(A2:A3)")) ) + ); + } + + /** + * Writes a function then tests to see if its correct + */ + public void refArrayFunctionTest(String function) { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet(); + HSSFRow r = null; + HSSFCell c = null; - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); + r = s.createRow(0); - FileInputStream in = new FileInputStream(file); - wb = new HSSFWorkbook(in); - s = wb.getSheetAt(0); - r = s.getRow(0); - c = r.getCell(0); + c = r.createCell(0); + c.setCellFormula(function+"(A2,A3)"); - assertTrue("function ="+function+"(A2,A3)", - ( (function+"(A2,A3)").equals(c.getCellFormula()) ) - ); - in.close(); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + s = wb.getSheetAt(0); + r = s.getRow(0); + c = r.getCell(0); + + assertTrue("function ="+function+"(A2,A3)", + ( (function+"(A2,A3)").equals(c.getCellFormula()) ) + ); } @@ -613,294 +501,224 @@ public final class TestFormulas extends TestCase { * Writes a function then tests to see if its correct * */ - public void refAreaArrayFunctionTest(String function) - throws Exception { + public void refAreaArrayFunctionTest(String function) { - File file = TempFile.createTempFile("testFormulaAreaArrayFunction"+function,".xls"); - FileOutputStream out = new FileOutputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(); - HSSFSheet s = wb.createSheet(); - HSSFRow r = null; - HSSFCell c = null; + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet(); + HSSFRow r = null; + HSSFCell c = null; - r = s.createRow(0); + r = s.createRow(0); - c = r.createCell(0); - c.setCellFormula(function+"(A2:A4,B2:B4)"); - c=r.createCell(1); - c.setCellFormula(function+"($A$2:$A4,B$2:B4)"); + c = r.createCell(0); + c.setCellFormula(function+"(A2:A4,B2:B4)"); + c=r.createCell(1); + c.setCellFormula(function+"($A$2:$A4,B$2:B4)"); - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + s = wb.getSheetAt(0); + r = s.getRow(0); + c = r.getCell(0); - FileInputStream in = new FileInputStream(file); - wb = new HSSFWorkbook(in); - s = wb.getSheetAt(0); - r = s.getRow(0); - c = r.getCell(0); + assertTrue("function ="+function+"(A2:A4,B2:B4)", + ( (function+"(A2:A4,B2:B4)").equals(c.getCellFormula()) ) + ); - assertTrue("function ="+function+"(A2:A4,B2:B4)", - ( (function+"(A2:A4,B2:B4)").equals(c.getCellFormula()) ) - ); - - c=r.getCell(1); - assertTrue("function ="+function+"($A$2:$A4,B$2:B4)", - ( (function+"($A$2:$A4,B$2:B4)").equals(c.getCellFormula()) ) - ); - in.close(); + c=r.getCell(1); + assertTrue("function ="+function+"($A$2:$A4,B$2:B4)", + ( (function+"($A$2:$A4,B$2:B4)").equals(c.getCellFormula()) ) + ); } - public void testAbsRefs() throws Exception { - File file = TempFile.createTempFile("testFormulaAbsRef",".xls"); - FileOutputStream out = new FileOutputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(); - HSSFSheet s = wb.createSheet(); - HSSFRow r = null; - HSSFCell c = null; + public void testAbsRefs() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet(); + HSSFRow r; + HSSFCell c; + r = s.createRow(0); + c = r.createCell(0); + c.setCellFormula("A3+A2"); + c=r.createCell(1); + c.setCellFormula("$A3+$A2"); + c=r.createCell(2); + c.setCellFormula("A$3+A$2"); + c=r.createCell(3); + c.setCellFormula("$A$3+$A$2"); + c=r.createCell(4); + c.setCellFormula("SUM($A$3,$A$2)"); - r = s.createRow(0); - - c = r.createCell(0); - c.setCellFormula("A3+A2"); - c=r.createCell(1); - c.setCellFormula("$A3+$A2"); - c=r.createCell(2); - c.setCellFormula("A$3+A$2"); - c=r.createCell(3); - c.setCellFormula("$A$3+$A$2"); - c=r.createCell(4); - c.setCellFormula("SUM($A$3,$A$2)"); - - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); - - FileInputStream in = new FileInputStream(file); - wb = new HSSFWorkbook(in); - s = wb.getSheetAt(0); - r = s.getRow(0); - c = r.getCell(0); - assertTrue("A3+A2", ("A3+A2").equals(c.getCellFormula())); - c = r.getCell(1); - assertTrue("$A3+$A2", ("$A3+$A2").equals(c.getCellFormula())); - c = r.getCell(2); - assertTrue("A$3+A$2", ("A$3+A$2").equals(c.getCellFormula())); - c = r.getCell(3); - assertTrue("$A$3+$A$2", ("$A$3+$A$2").equals(c.getCellFormula())); - c = r.getCell(4); - assertTrue("SUM($A$3,$A$2)", ("SUM($A$3,$A$2)").equals(c.getCellFormula())); - in.close(); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); + s = wb.getSheetAt(0); + r = s.getRow(0); + c = r.getCell(0); + assertTrue("A3+A2", ("A3+A2").equals(c.getCellFormula())); + c = r.getCell(1); + assertTrue("$A3+$A2", ("$A3+$A2").equals(c.getCellFormula())); + c = r.getCell(2); + assertTrue("A$3+A$2", ("A$3+A$2").equals(c.getCellFormula())); + c = r.getCell(3); + assertTrue("$A$3+$A$2", ("$A$3+$A$2").equals(c.getCellFormula())); + c = r.getCell(4); + assertTrue("SUM($A$3,$A$2)", ("SUM($A$3,$A$2)").equals(c.getCellFormula())); } - public void testSheetFunctions() - throws IOException - { - File file = TempFile.createTempFile("testSheetFormula",".xls"); - FileOutputStream out = new FileOutputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(); - HSSFSheet s = wb.createSheet("A"); - HSSFRow r = null; - HSSFCell c = null; - r = s.createRow(0); - c = r.createCell(0);c.setCellValue(1); - c = r.createCell(1);c.setCellValue(2); + public void testSheetFunctions() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet("A"); + HSSFRow r = null; + HSSFCell c = null; + r = s.createRow(0); + c = r.createCell(0);c.setCellValue(1); + c = r.createCell(1);c.setCellValue(2); - s = wb.createSheet("B"); - r = s.createRow(0); - c=r.createCell(0); c.setCellFormula("AVERAGE(A!A1:B1)"); - c=r.createCell(1); c.setCellFormula("A!A1+A!B1"); - c=r.createCell(2); c.setCellFormula("A!$A$1+A!$B1"); - wb.write(out); - out.close(); + s = wb.createSheet("B"); + r = s.createRow(0); + c=r.createCell(0); c.setCellFormula("AVERAGE(A!A1:B1)"); + c=r.createCell(1); c.setCellFormula("A!A1+A!B1"); + c=r.createCell(2); c.setCellFormula("A!$A$1+A!$B1"); - assertTrue("file exists",file.exists()); + wb = HSSFTestDataSamples.writeOutAndReadBack(wb); - FileInputStream in = new FileInputStream(file); - wb = new HSSFWorkbook(in); - s = wb.getSheet("B"); - r = s.getRow(0); - c = r.getCell(0); - assertTrue("expected: AVERAGE(A!A1:B1) got: "+c.getCellFormula(), ("AVERAGE(A!A1:B1)").equals(c.getCellFormula())); - c = r.getCell(1); - assertTrue("expected: A!A1+A!B1 got: "+c.getCellFormula(), ("A!A1+A!B1").equals(c.getCellFormula())); - in.close(); + s = wb.getSheet("B"); + r = s.getRow(0); + c = r.getCell(0); + assertTrue("expected: AVERAGE(A!A1:B1) got: "+c.getCellFormula(), ("AVERAGE(A!A1:B1)").equals(c.getCellFormula())); + c = r.getCell(1); + assertTrue("expected: A!A1+A!B1 got: "+c.getCellFormula(), ("A!A1+A!B1").equals(c.getCellFormula())); } public void testRVAoperands() throws Exception { - File file = TempFile.createTempFile("testFormulaRVA",".xls"); - FileOutputStream out = new FileOutputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(); - HSSFSheet s = wb.createSheet(); - HSSFRow r = null; - HSSFCell c = null; + File file = TempFile.createTempFile("testFormulaRVA",".xls"); + FileOutputStream out = new FileOutputStream(file); + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet(); + HSSFRow r = null; + HSSFCell c = null; - r = s.createRow(0); + r = s.createRow(0); - c = r.createCell(0); - c.setCellFormula("A3+A2"); - c=r.createCell(1); - c.setCellFormula("AVERAGE(A3,A2)"); - c=r.createCell(2); - c.setCellFormula("ROW(A3)"); - c=r.createCell(3); - c.setCellFormula("AVERAGE(A2:A3)"); - c=r.createCell(4); - c.setCellFormula("POWER(A2,A3)"); - c=r.createCell(5); - c.setCellFormula("SIN(A2)"); + c = r.createCell(0); + c.setCellFormula("A3+A2"); + c=r.createCell(1); + c.setCellFormula("AVERAGE(A3,A2)"); + c=r.createCell(2); + c.setCellFormula("ROW(A3)"); + c=r.createCell(3); + c.setCellFormula("AVERAGE(A2:A3)"); + c=r.createCell(4); + c.setCellFormula("POWER(A2,A3)"); + c=r.createCell(5); + c.setCellFormula("SIN(A2)"); - c=r.createCell(6); - c.setCellFormula("SUM(A2:A3)"); + c=r.createCell(6); + c.setCellFormula("SUM(A2:A3)"); - c=r.createCell(7); - c.setCellFormula("SUM(A2,A3)"); + c=r.createCell(7); + c.setCellFormula("SUM(A2,A3)"); - r = s.createRow(1);c=r.createCell(0); c.setCellValue(2.0); - r = s.createRow(2);c=r.createCell(0); c.setCellValue(3.0); + r = s.createRow(1);c=r.createCell(0); c.setCellValue(2.0); + r = s.createRow(2);c=r.createCell(0); c.setCellValue(3.0); - wb.write(out); - out.close(); - assertTrue("file exists",file.exists()); + wb.write(out); + out.close(); + assertTrue("file exists",file.exists()); } - public void testStringFormulas() - throws IOException - { - File file = TempFile.createTempFile("testStringFormula",".xls"); - FileOutputStream out = new FileOutputStream(file); - HSSFWorkbook wb = new HSSFWorkbook(); - HSSFSheet s = wb.createSheet("A"); - HSSFRow r = null; - HSSFCell c = null; - r = s.createRow(0); - c=r.createCell(1); c.setCellFormula("UPPER(\"abc\")"); - c=r.createCell(2); c.setCellFormula("LOWER(\"ABC\")"); - c=r.createCell(3); c.setCellFormula("CONCATENATE(\" my \",\" name \")"); + public void testStringFormulas() { + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet("A"); + HSSFRow r = null; + HSSFCell c = null; + r = s.createRow(0); + c=r.createCell(1); c.setCellFormula("UPPER(\"abc\")"); + c=r.createCell(2); c.setCellFormula("LOWER(\"ABC\")"); + c=r.createCell(3); c.setCellFormula("CONCATENATE(\" my \",\" name \")"); - wb.write(out); - out.close(); + HSSFTestDataSamples.writeOutAndReadBack(wb); - wb = openSample("StringFormulas.xls"); - s = wb.getSheetAt(0); - r = s.getRow(0); - c = r.getCell(0); - assertTrue("expected: UPPER(\"xyz\") got "+c.getCellFormula(), ("UPPER(\"xyz\")").equals(c.getCellFormula())); - //c = r.getCell((short)1); - //assertTrue("expected: A!A1+A!B1 got: "+c.getCellFormula(), ("A!A1+A!B1").equals(c.getCellFormula())); + wb = openSample("StringFormulas.xls"); + s = wb.getSheetAt(0); + r = s.getRow(0); + c = r.getCell(0); + assertEquals("UPPER(\"xyz\")", c.getCellFormula()); + } + + public void testLogicalFormulas() { + + HSSFWorkbook wb = new HSSFWorkbook(); + HSSFSheet s = wb.createSheet("A"); + HSSFRow r = null; + HSSFCell c = null; + r = s.createRow(0); + c=r.createCell(1); c.setCellFormula("IF(A10)); - - File nestedIf = TempFile.createTempFile("testNestedIfFormula",".xls"); - out = new FileOutputStream(nestedIf); wb = new HSSFWorkbook(); s = wb.createSheet("testSheet1"); r = null; @@ -939,17 +751,10 @@ public final class TestFormulas extends TestCase { formulaCell.setCellFormula("IF(A1=B1,AVERAGE(A1:B1),AVERAGE(A2:B2))"); - - wb.write(out); - out.close(); - assertTrue("file exists", nestedIf.exists()); - - assertTrue("length of nestedIf file is zero", (nestedIf.length()>0)); + HSSFTestDataSamples.writeOutAndReadBack(wb); } - public void testSumIf() - throws IOException - { + public void testSumIf() { String function ="SUMIF(A1:A5,\">4000\",B1:B5)"; HSSFWorkbook wb = openSample("sumifformula.xls"); @@ -960,40 +765,34 @@ public final class TestFormulas extends TestCase { assertEquals(function, c.getCellFormula()); - File file = TempFile.createTempFile("testSumIfFormula",".xls"); - FileOutputStream out = new FileOutputStream(file); wb = new HSSFWorkbook(); s = wb.createSheet(); r = s.createRow(0); - c=r.createCell(0); c.setCellValue((double)1000); - c=r.createCell(1); c.setCellValue((double)1); + c=r.createCell(0); c.setCellValue(1000); + c=r.createCell(1); c.setCellValue(1); r = s.createRow(1); - c=r.createCell(0); c.setCellValue((double)2000); - c=r.createCell(1); c.setCellValue((double)2); + c=r.createCell(0); c.setCellValue(2000); + c=r.createCell(1); c.setCellValue(2); r = s.createRow(2); - c=r.createCell(0); c.setCellValue((double)3000); - c=r.createCell(1); c.setCellValue((double)3); + c=r.createCell(0); c.setCellValue(3000); + c=r.createCell(1); c.setCellValue(3); r = s.createRow(3); - c=r.createCell(0); c.setCellValue((double)4000); - c=r.createCell(1); c.setCellValue((double)4); + c=r.createCell(0); c.setCellValue(4000); + c=r.createCell(1); c.setCellValue(4); r = s.createRow(4); - c=r.createCell(0); c.setCellValue((double)5000); - c=r.createCell(1); c.setCellValue((double)5); + c=r.createCell(0); c.setCellValue(5000); + c=r.createCell(1); c.setCellValue(5); r = s.getRow(0); c=r.createCell(2); c.setCellFormula(function); - wb.write(out); - out.close(); - - assertTrue("sumif file doesnt exists", (file.exists())); - assertTrue("sumif == 0 bytes", file.length() > 0); + HSSFTestDataSamples.writeOutAndReadBack(wb); } public void testSquareMacro() { @@ -1064,7 +863,7 @@ public final class TestFormulas extends TestCase { /** Unknown Ptg 3D*/ public void test27272_2() throws Exception { HSSFWorkbook wb = openSample("27272_2.xls"); - assertEquals("Reference for named range ", "'LOAD.POD_HISTORIES'!#REF!",wb.getNameAt(0).getReference()); + assertEquals("Reference for named range ", "LOAD.POD_HISTORIES!#REF!",wb.getNameAt(0).getReference()); File outF = File.createTempFile("bug27272_2",".xls"); wb.write(new FileOutputStream(outF)); System.out.println("Open "+outF.getAbsolutePath()+" in Excel"); @@ -1085,8 +884,4 @@ public final class TestFormulas extends TestCase { assertEquals("DZ2*2", wb.getSheetAt(0).getRow(1).getCell(128).toString()); assertEquals("B32770*2", wb.getSheetAt(0).getRow(32768).getCell(1).toString()); } - - public static void main(String [] args) { - junit.textui.TestRunner.run(TestFormulas.class); - } } diff --git a/src/testcases/org/apache/poi/ss/formula/AllSSFormulaTests.java b/src/testcases/org/apache/poi/ss/formula/AllSSFormulaTests.java new file mode 100644 index 000000000..dd1360f76 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/AllSSFormulaTests.java @@ -0,0 +1,34 @@ +/* ==================================================================== + 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.formula; + +import junit.framework.Test; +import junit.framework.TestSuite; +/** + * Test suite for org.apache.poi.ss.formula + * + * @author Josh Micich + */ +public final class AllSSFormulaTests { + public static Test suite() { + TestSuite result = new TestSuite(AllSSFormulaTests.class.getName()); + result.addTestSuite(TestEvaluationCache.class); + result.addTestSuite(TestWorkbookEvaluator.class); + return result; + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java new file mode 100644 index 000000000..0d3e93ee7 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/TestWorkbookEvaluator.java @@ -0,0 +1,154 @@ +/* ==================================================================== + 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.formula; + +import junit.framework.TestCase; + +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.record.formula.AreaErrPtg; +import org.apache.poi.hssf.record.formula.AttrPtg; +import org.apache.poi.hssf.record.formula.DeletedArea3DPtg; +import org.apache.poi.hssf.record.formula.DeletedRef3DPtg; +import org.apache.poi.hssf.record.formula.IntPtg; +import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.RefErrorPtg; +import org.apache.poi.hssf.record.formula.eval.ErrorEval; +import org.apache.poi.hssf.record.formula.eval.NumberEval; +import org.apache.poi.hssf.record.formula.eval.ValueEval; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; + +/** + * Tests {@link WorkbookEvaluator}. + * + * @author Josh Micich + */ +public class TestWorkbookEvaluator extends TestCase { + + /** + * Make sure that the evaluator can directly handle tAttrSum (instead of relying on re-parsing + * the whole formula which converts tAttrSum to tFuncVar("SUM") ) + */ + public void testAttrSum() { + + Ptg[] ptgs = { + new IntPtg(42), + AttrPtg.SUM, + }; + + ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null); + assertEquals(42, ((NumberEval)result).getNumberValue(), 0.0); + } + + /** + * Make sure that the evaluator can directly handle (deleted) ref error tokens + * (instead of relying on re-parsing the whole formula which converts these + * to the error constant #REF! ) + */ + public void testRefErr() { + + confirmRefErr(new RefErrorPtg()); + confirmRefErr(new AreaErrPtg()); + confirmRefErr(new DeletedRef3DPtg(0)); + confirmRefErr(new DeletedArea3DPtg(0)); + } + private static void confirmRefErr(Ptg ptg) { + Ptg[] ptgs = { + ptg, + }; + + ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null); + assertEquals(ErrorEval.REF_INVALID, result); + } + + /** + * Make sure that the evaluator can directly handle tAttrSum (instead of relying on re-parsing + * the whole formula which converts tAttrSum to tFuncVar("SUM") ) + */ + public void testMemFunc() { + + Ptg[] ptgs = { + new IntPtg(42), + AttrPtg.SUM, + }; + + ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null); + assertEquals(42, ((NumberEval)result).getNumberValue(), 0.0); + } + + + public void testEvaluateMultipleWorkbooks() { + HSSFWorkbook wbA = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaA.xls"); + HSSFWorkbook wbB = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaB.xls"); + + HSSFFormulaEvaluator evaluatorA = new HSSFFormulaEvaluator(wbA); + HSSFFormulaEvaluator evaluatorB = new HSSFFormulaEvaluator(wbB); + + // Hook up the workbook evaluators to enable evaluation of formulas across books + String[] bookNames = { "multibookFormulaA.xls", "multibookFormulaB.xls", }; + HSSFFormulaEvaluator[] evaluators = { evaluatorA, evaluatorB, }; + HSSFFormulaEvaluator.setupEnvironment(bookNames, evaluators); + + HSSFCell cell; + + HSSFSheet aSheet1 = wbA.getSheetAt(0); + HSSFSheet bSheet1 = wbB.getSheetAt(0); + + // Simple case - single link from wbA to wbB + confirmFormula(wbA, 0, 0, 0, "[multibookFormulaB.xls]BSheet1!B1"); + cell = aSheet1.getRow(0).getCell(0); + confirmEvaluation(35, evaluatorA, cell); + + + // more complex case - back link into wbA + // [wbA]ASheet1!A2 references (among other things) [wbB]BSheet1!B2 + confirmFormula(wbA, 0, 1, 0, "[multibookFormulaB.xls]BSheet1!$B$2+2*A3"); + // [wbB]BSheet1!B2 references (among other things) [wbA]AnotherSheet!A1:B2 + confirmFormula(wbB, 0, 1, 1, "SUM([multibookFormulaA.xls]AnotherSheet!$A$1:$B$2)+B3"); + + cell = aSheet1.getRow(1).getCell(0); + confirmEvaluation(264, evaluatorA, cell); + + // change [wbB]BSheet1!B3 (from 50 to 60) + bSheet1.getRow(2).getCell(1).setCellValue(60); + evaluatorB.setCachedPlainValue(bSheet1, 2, 1, new NumberEval(60)); + confirmEvaluation(274, evaluatorA, cell); + + // change [wbA]ASheet1!A3 (from 100 to 80) + aSheet1.getRow(2).getCell(0).setCellValue(80); + evaluatorA.setCachedPlainValue(aSheet1, 2, 0, new NumberEval(80)); + confirmEvaluation(234, evaluatorA, cell); + + // change [wbA]AnotherSheet!A1 (from 2 to 3) + wbA.getSheetAt(1).getRow(0).getCell(0).setCellValue(3); + evaluatorA.setCachedPlainValue(wbA.getSheetAt(1), 0, 0, new NumberEval(3)); + confirmEvaluation(235, evaluatorA, cell); + } + + private static void confirmEvaluation(double expectedValue, HSSFFormulaEvaluator fe, HSSFCell cell) { + assertEquals(expectedValue, fe.evaluate(cell).getNumberValue(), 0.0); + } + + private static void confirmFormula(HSSFWorkbook wb, int sheetIndex, int rowIndex, int columnIndex, + String expectedFormula) { + HSSFCell cell = wb.getSheetAt(sheetIndex).getRow(rowIndex).getCell(columnIndex); + assertEquals(expectedFormula, cell.getCellFormula()); + } +}