diff options
author | Josh Micich <josh@apache.org> | 2008-09-29 20:09:09 +0000 |
---|---|---|
committer | Josh Micich <josh@apache.org> | 2008-09-29 20:09:09 +0000 |
commit | 8bfd1494141e2fac61069bb3431189565f8c6a39 (patch) | |
tree | c1e4032f4be206d4167e5828f35e3593f04d97b6 /src/java/org | |
parent | 7c8d6be5bce576e94054a6808da509e3db3c1ebc (diff) | |
download | poi-8bfd1494141e2fac61069bb3431189565f8c6a39.tar.gz poi-8bfd1494141e2fac61069bb3431189565f8c6a39.zip |
Merged revisions 699178,699487,699489,699761 via svnmerge from
https://svn.apache.org/repos/asf/poi/trunk
........
r699178 | josh | 2008-09-25 21:49:20 -0700 (Thu, 25 Sep 2008) | 1 line
Changed HSSFEvaluationWorkbook to avoid re-parsing cell formulas during execution. (working towards fix for bug 45865)
........
r699487 | josh | 2008-09-26 13:25:45 -0700 (Fri, 26 Sep 2008) | 1 line
Fix formula parser to properly support the range operator. Small fixes to parsing of sheet names and full column references.
........
r699489 | josh | 2008-09-26 13:32:06 -0700 (Fri, 26 Sep 2008) | 1 line
Code cleanup in junit
........
r699761 | josh | 2008-09-27 19:04:31 -0700 (Sat, 27 Sep 2008) | 1 line
Bug 45865 - modified Formula Parser/Evaluator to handle cross-worksheet formulas
........
git-svn-id: https://svn.apache.org/repos/asf/poi/branches/ooxml@700234 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'src/java/org')
29 files changed, 1444 insertions, 738 deletions
diff --git a/src/java/org/apache/poi/hssf/model/LinkTable.java b/src/java/org/apache/poi/hssf/model/LinkTable.java index 2caab0fb56..998712e5e8 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 dbf7ecf7e1..0728d0e29f 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<records.size() && - !done; i++) { - Record r = records.get(i); - if(r instanceof ExtendedFormatRecord) { - } else if(r instanceof StyleRecord) { - StyleRecord sr = (StyleRecord)r; - if(sr.getIndex() == xfIndex) { - return sr; - } - } else { - done = true; - } - } - return null; + // Style records always follow after + // the ExtendedFormat records + boolean done = false; + for(int i=records.getXfpos(); i<records.size() && + !done; i++) { + Record r = records.get(i); + if(r instanceof ExtendedFormatRecord) { + } else if(r instanceof StyleRecord) { + StyleRecord sr = (StyleRecord)r; + if(sr.getIndex() == xfIndex) { + return sr; + } + } else { + done = true; + } + } + return null; } /** * Creates a new StyleRecord, for the given Extended @@ -785,29 +786,29 @@ public final class Workbook implements Model { * records collection */ public StyleRecord createStyleRecord(int xfIndex) { - // Style records always follow after - // the ExtendedFormat records - StyleRecord newSR = new StyleRecord(); - newSR.setIndex((short)xfIndex); - - // Find the spot - int addAt = -1; - for(int i=records.getXfpos(); i<records.size() && - addAt == -1; i++) { - Record r = records.get(i); - if(r instanceof ExtendedFormatRecord || - r instanceof StyleRecord) { - // Keep going - } else { - addAt = i; - } - } - if(addAt == -1) { - throw new IllegalStateException("No XF Records found!"); - } - records.add(addAt, newSR); - - return newSR; + // Style records always follow after + // the ExtendedFormat records + StyleRecord newSR = new StyleRecord(); + newSR.setIndex((short)xfIndex); + + // Find the spot + int addAt = -1; + for(int i=records.getXfpos(); i<records.size() && + addAt == -1; i++) { + Record r = records.get(i); + if(r instanceof ExtendedFormatRecord || + r instanceof StyleRecord) { + // Keep going + } else { + addAt = i; + } + } + if(addAt == -1) { + throw new IllegalStateException("No XF Records found!"); + } + records.add(addAt, newSR); + + return newSR; } /** @@ -1914,8 +1915,7 @@ public final class Workbook implements Model { */ public String findSheetNameFromExternSheet(int externSheetIndex){ - int indexToSheet = linkTable.getIndexToSheet(externSheetIndex); - + int indexToSheet = linkTable.getIndexToInternalSheet(externSheetIndex); if (indexToSheet < 0) { // TODO - what does '-1' mean here? //error check, bail out gracefully! @@ -1927,6 +1927,13 @@ public final class Workbook implements Model { } return getSheetName(indexToSheet); } + public ExternalSheet getExternalSheet(int externSheetIndex) { + String[] extNames = linkTable.getExternalBookAndSheetName(externSheetIndex); + if (extNames == null) { + return null; + } + return new ExternalSheet(extNames[0], extNames[1]); + } /** * Finds the sheet index for a particular external sheet number. @@ -1944,9 +1951,14 @@ public final class Workbook implements Model { * @return index to extern sheet */ public short checkExternSheet(int sheetNumber){ - return getOrCreateLinkTable().checkExternSheet(sheetNumber); + return (short)getOrCreateLinkTable().checkExternSheet(sheetNumber); } + public int getExternalSheetIndex(String workbookName, String sheetName) { + return getOrCreateLinkTable().getExternalSheetIndex(workbookName, sheetName); + } + + /** gets the total number of names * @return number of names */ diff --git a/src/java/org/apache/poi/hssf/record/ExternSheetRecord.java b/src/java/org/apache/poi/hssf/record/ExternSheetRecord.java index c4f00581b2..64f86c3f12 100644 --- a/src/java/org/apache/poi/hssf/record/ExternSheetRecord.java +++ b/src/java/org/apache/poi/hssf/record/ExternSheetRecord.java @@ -250,10 +250,13 @@ public class ExternSheetRecord extends Record { return _list.size() - 1; } - public int getRefIxForSheet(int sheetIndex) { + public int getRefIxForSheet(int externalBookIndex, int sheetIndex) { int nItems = _list.size(); for (int i = 0; i < nItems; i++) { RefSubRecord ref = getRef(i); + if (ref.getExtBookIndex() != externalBookIndex) { + continue; + } if (ref.getFirstSheetIndex() == sheetIndex && ref.getLastSheetIndex() == sheetIndex) { return i; } diff --git a/src/java/org/apache/poi/hssf/record/SupBookRecord.java b/src/java/org/apache/poi/hssf/record/SupBookRecord.java index c75e2db894..b4e46c6fdc 100644 --- a/src/java/org/apache/poi/hssf/record/SupBookRecord.java +++ b/src/java/org/apache/poi/hssf/record/SupBookRecord.java @@ -221,8 +221,33 @@ public final class SupBookRecord extends Record { { return sid; } - public UnicodeString getURL() { - return field_2_encoded_url; + public String getURL() { + String encodedUrl = field_2_encoded_url.getString(); + switch(encodedUrl.charAt(0)) { + case 0: // Reference to an empty workbook name + return encodedUrl.substring(1); // will this just be empty string? + case 1: // encoded file name + return decodeFileName(encodedUrl); + case 2: // Self-referential external reference + return encodedUrl.substring(1); + + } + return encodedUrl; + } + private static String decodeFileName(String encodedUrl) { + return encodedUrl.substring(1); + // TODO the following special characters may appear in the rest of the string, and need to get interpreted + /* see "MICROSOFT OFFICE EXCEL 97-2007 BINARY FILE FORMAT SPECIFICATION" + chVolume 1 + chSameVolume 2 + chDownDir 3 + chUpDir 4 + chLongVolume 5 + chStartupDir 6 + chAltStartupDir 7 + chLibDir 8 + + */ } public UnicodeString[] getSheetNames() { return (UnicodeString[]) field_3_sheet_names.clone(); diff --git a/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java b/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java index 9a538cf70d..58cdc5b275 100644 --- a/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/Area3DPtg.java @@ -18,8 +18,9 @@ package org.apache.poi.hssf.record.formula; import org.apache.poi.hssf.record.RecordInputStream; -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 Area3DPtg extends AreaPtgBase implements WorkbookDependentFormula { +public final class Area3DPtg extends AreaPtgBase implements WorkbookDependentFormula, ExternSheetReferenceToken { public final static byte sid = 0x3b; private final static int SIZE = 11; // 10 + 1 for Ptg @@ -76,8 +77,8 @@ public final class Area3DPtg extends AreaPtgBase implements WorkbookDependentFor return SIZE; } - public short getExternSheetIndex() { - return (short)field_1_index_extern_sheet; + public int getExternSheetIndex() { + return field_1_index_extern_sheet; } public void setExternSheetIndex(int index) { diff --git a/src/java/org/apache/poi/hssf/record/formula/AreaI.java b/src/java/org/apache/poi/hssf/record/formula/AreaI.java index 477d32f677..319a9819c9 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AreaI.java +++ b/src/java/org/apache/poi/hssf/record/formula/AreaI.java @@ -50,10 +50,10 @@ public interface AreaI { public OffsetArea(int baseRow, int baseColumn, int relFirstRowIx, int relLastRowIx, int relFirstColIx, int relLastColIx) { - _firstRow = baseRow + relFirstRowIx; - _lastRow = baseRow + relLastRowIx; - _firstColumn = baseColumn + relFirstColIx; - _lastColumn = baseColumn + relLastColIx; + _firstRow = baseRow + Math.min(relFirstRowIx, relLastRowIx); + _lastRow = baseRow + Math.max(relFirstRowIx, relLastRowIx); + _firstColumn = baseColumn + Math.min(relFirstColIx, relLastColIx); + _lastColumn = baseColumn + Math.max(relFirstColIx, relLastColIx); } public int getFirstColumn() { @@ -72,5 +72,4 @@ public interface AreaI { return _lastRow; } } - }
\ No newline at end of file diff --git a/src/java/org/apache/poi/hssf/record/formula/AreaPtgBase.java b/src/java/org/apache/poi/hssf/record/formula/AreaPtgBase.java index 7bfefc5526..a97ecd4f03 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AreaPtgBase.java +++ b/src/java/org/apache/poi/hssf/record/formula/AreaPtgBase.java @@ -30,248 +30,264 @@ import org.apache.poi.util.LittleEndian; * @author Jason Height (jheight at chariot dot net dot au) */ public abstract class AreaPtgBase extends OperandPtg implements AreaI { - /** - * TODO - (May-2008) fix subclasses of AreaPtg 'AreaN~' which are used in shared formulas. - * see similar comment in ReferencePtg - */ - protected final RuntimeException notImplemented() { - return new RuntimeException("Coding Error: This method should never be called. This ptg should be converted"); - } + /** + * TODO - (May-2008) fix subclasses of AreaPtg 'AreaN~' which are used in shared formulas. + * see similar comment in ReferencePtg + */ + protected final RuntimeException notImplemented() { + return new RuntimeException("Coding Error: This method should never be called. This ptg should be converted"); + } - /** zero based, unsigned 16 bit */ - private int field_1_first_row; - /** zero based, unsigned 16 bit */ - private int field_2_last_row; - /** zero based, unsigned 8 bit */ - private int field_3_first_column; - /** zero based, unsigned 8 bit */ - private int field_4_last_column; - - private final static BitField rowRelative = BitFieldFactory.getInstance(0x8000); - private final static BitField colRelative = BitFieldFactory.getInstance(0x4000); - private final static BitField columnMask = BitFieldFactory.getInstance(0x3FFF); + /** zero based, unsigned 16 bit */ + private int field_1_first_row; + /** zero based, unsigned 16 bit */ + private int field_2_last_row; + /** zero based, unsigned 8 bit */ + private int field_3_first_column; + /** zero based, unsigned 8 bit */ + private int field_4_last_column; + + private final static BitField rowRelative = BitFieldFactory.getInstance(0x8000); + private final static BitField colRelative = BitFieldFactory.getInstance(0x4000); + private final static BitField columnMask = BitFieldFactory.getInstance(0x3FFF); - protected AreaPtgBase() { - // do nothing - } - - protected AreaPtgBase(String arearef) { - AreaReference ar = new AreaReference(arearef); - CellReference firstCell = ar.getFirstCell(); - CellReference lastCell = ar.getLastCell(); - setFirstRow(firstCell.getRow()); - setFirstColumn(firstCell.getCol()); - setLastRow(lastCell.getRow()); - setLastColumn(lastCell.getCol()); - setFirstColRelative(!firstCell.isColAbsolute()); - setLastColRelative(!lastCell.isColAbsolute()); - setFirstRowRelative(!firstCell.isRowAbsolute()); - setLastRowRelative(!lastCell.isRowAbsolute()); - } - - protected AreaPtgBase(int firstRow, int lastRow, int firstColumn, int lastColumn, - boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { - - checkColumnBounds(firstColumn); - checkColumnBounds(lastColumn); - checkRowBounds(firstRow); - checkRowBounds(lastRow); - setFirstRow(firstRow); - setLastRow(lastRow); - setFirstColumn(firstColumn); - setLastColumn(lastColumn); - setFirstRowRelative(firstRowRelative); - setLastRowRelative(lastRowRelative); - setFirstColRelative(firstColRelative); - setLastColRelative(lastColRelative); - } + protected AreaPtgBase() { + // do nothing + } + + protected AreaPtgBase(String arearef) { + AreaReference ar = new AreaReference(arearef); + CellReference firstCell = ar.getFirstCell(); + CellReference lastCell = ar.getLastCell(); + setFirstRow(firstCell.getRow()); + setFirstColumn(firstCell.getCol()); + setLastRow(lastCell.getRow()); + setLastColumn(lastCell.getCol()); + setFirstColRelative(!firstCell.isColAbsolute()); + setLastColRelative(!lastCell.isColAbsolute()); + setFirstRowRelative(!firstCell.isRowAbsolute()); + setLastRowRelative(!lastCell.isRowAbsolute()); + } + + protected AreaPtgBase(int firstRow, int lastRow, int firstColumn, int lastColumn, + boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { + + checkColumnBounds(firstColumn); + checkColumnBounds(lastColumn); + checkRowBounds(firstRow); + checkRowBounds(lastRow); + + if (lastRow > 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 <code>true</code> if the last row relative, else - * <code>false</code> - */ - 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 <code>true</code> if the last row relative, else + * <code>false</code> + */ + 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 34558ebdf8..701a7aee74 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 <tt>SpaceType</tt> * @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 412c110d3a..dadbcb8702 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 261e98ce8b..053057925f 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 ace857da1e..fd822c3986 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 <code>true</code> 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 0000000000..9398108fdc --- /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 d82a9d1144..0b6a954499 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 d7a3207383..2f30d15c81 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 d6575642ee..6a0b0e2d3a 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 0ddb0764ee..2e31b2a215 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 d6bb9b7bfb..6857c4bc1b 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.<br/> */ 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 0000000000..c62d2f182d --- /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.<p/> + * + * 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; i<nItems; i++) { + String wbName = workbookNames[i]; + WorkbookEvaluator wbEval = evaluators[i]; + if (m.containsKey(wbName)) { + throw new IllegalArgumentException("Duplicate workbook name '" + wbName + "'"); + } + if (uniqueEvals.containsKey(wbEval)) { + String msg = "Attempted to register same workbook under names '" + + uniqueEvals.get(wbEval) + "' and '" + wbName + "'"; + throw new IllegalArgumentException(msg); + } + uniqueEvals.put(wbEval, wbName); + m.put(wbName, wbEval); + } + unhookOldEnvironments(evaluators); + hookNewEnvironment(evaluators, this); + _unhooked = false; + _evaluators = evaluators; + _evaluatorsByName = m; + } + + private static void hookNewEnvironment(WorkbookEvaluator[] evaluators, CollaboratingWorkbooksEnvironment env) { + + // All evaluators will need to share the same cache. + // but the cache takes an optional evaluation listener. + int nItems = evaluators.length; + IEvaluationListener evalListener = evaluators[0].getEvaluationListener(); + // make sure that all evaluators have the same listener + for(int i=0; i<nItems; i++) { + if(evalListener != evaluators[i].getEvaluationListener()) { + // This would be very complex to support + throw new RuntimeException("Workbook evaluators must all have the same evaluation listener"); + } + } + EvaluationCache cache = new EvaluationCache(evalListener); + + for(int i=0; i<nItems; i++) { + evaluators[i].attachToEnvironment(env, cache); + } + + } + private void unhookOldEnvironments(WorkbookEvaluator[] evaluators) { + Set oldEnvs = new HashSet(); + for(int i=0; i<evaluators.length; i++) { + oldEnvs.add(evaluators[i].getEnvironment()); + } + CollaboratingWorkbooksEnvironment[] oldCWEs = new CollaboratingWorkbooksEnvironment[oldEnvs.size()]; + oldEnvs.toArray(oldCWEs); + for (int i = 0; i < oldCWEs.length; i++) { + oldCWEs[i].unhook(); + } + } + + /** + * + */ + private void unhook() { + if (_evaluators.length < 1) { + return; + } + for (int i = 0; i < _evaluators.length; i++) { + _evaluators[i].detachFromEnvironment(); + } + _unhooked = true; + } + + public WorkbookEvaluator getWorkbookEvaluator(String workbookName) { + if (_unhooked) { + throw new IllegalStateException("This environment has been unhooked"); + } + WorkbookEvaluator result = (WorkbookEvaluator) _evaluatorsByName.get(workbookName); + if (result == null) { + StringBuffer sb = new StringBuffer(256); + sb.append("Could not resolve external workbook name '").append(workbookName).append("'."); + if (_evaluators.length < 1) { + sb.append(" Workbook environment has not been set up."); + } else { + sb.append(" The following workbook names are valid: ("); + Iterator i = _evaluatorsByName.keySet().iterator(); + int count=0; + while(i.hasNext()) { + if (count++>0) { + 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 fdc933f6fa..b0c34fd78a 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 02917d82a4..6cf938fe9f 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 <code>null</code> 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 0000000000..09262a1320 --- /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 <br/> + * + * 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 df2f8c282f..1398399ff4 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.<br/> @@ -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.<br/> * 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)); - } - - private Ptg parseNameOrReference(String name) { - - AreaReference areaRef = parseArea(name); - if (areaRef != null) { - // will happen if dots are used instead of colon - return new AreaPtg(areaRef.formatAsString()); + if (!iden.isQuoted()) { + String name = iden.getName(); + if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) { + return new ParseNode(new BoolPtg(name.toUpperCase())); + } } + return parseRangeExpression(iden); + } - if (look == ':' || look == '.') { // this is a AreaReference + private ParseNode parseRangeExpression(Identifier iden) { + Ptg ptgA = parseNameOrCellRef(iden); + if (look == ':') { GetChar(); - - while (look == '.') { // formulas can have . or .. or ... instead of : - 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 <code>null</code> 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; - String first = name; - String second = parseIdentifier(); - return new AreaPtg(first+":"+second); + 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 == '!') { - 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); + GetChar(); + // 3-D ref + // this code assumes iden is a sheetName + // TODO - handle <book name> ! <named range name> + int externIdx = getExternalSheetIndex(iden.getName()); + String secondIden = parseUnquotedIdentifier(); + AreaReference areaRef = parseArea(secondIden); + if (areaRef == null) { + return new Ref3DPtg(secondIden, externIdx); } - return new Ref3DPtg(first, externIdx); - } - if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) { - return new BoolPtg(name.toUpperCase()); + // 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()); + } // 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 <code>null</code> 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 69431c2c22..e9be7d1d39 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 ac95f4da0f..c9b95f6b1c 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.<br/>
@@ -29,6 +30,10 @@ import org.apache.poi.hssf.record.formula.NameXPtg; */
public interface FormulaRenderingWorkbook {
+ /**
+ * @return <code>null</code> 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 79087e4d6d..91226bd53b 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 eaa57c114f..09c93723f9 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 b41e2c2c3f..9c02ca9f9e 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 <tt>NumberEval</tt>, <tt>StringEval</tt>, <tt>BoolEval</tt>, * <tt>BlankEval</tt> or <tt>ErrorEval</tt>. Never <code>null</code>. */ - 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 ae2c9192f0..30d92ebcf4 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 e5005db5d4..23b4c806e8 100644 --- a/src/java/org/apache/poi/ss/util/CellReference.java +++ b/src/java/org/apache/poi/ss/util/CellReference.java @@ -34,22 +34,29 @@ 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 - } - - public CellReference(int pRow, int pCol) { - this(pRow, pCol & 0xFFFF, false, false); - } - public CellReference(int pRow, short pCol) { - this(pRow, (int)pCol, false, false); - } + /** + * 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, 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) { + this(pRow, pCol, false, false); + } + public CellReference(int pRow, short pCol) { + this(pRow, pCol & 0xFFFF, false, false); + } - public int getRow(){return _rowIndex;} - public short getCol(){return (short) _colIndex;} - public boolean isRowAbsolute(){return _isRowAbs;} - public boolean isColAbsolute(){return _isColAbs;} - /** - * @return possibly <code>null</code> 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; - } + 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; + } - /** - * 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 <code>null</code> 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 <tt>NameType</tt> - */ - 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 <tt>NameType</tt> + */ + 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 { * <blockquote><table border="0" cellpadding="1" cellspacing="0" * summary="Notable cases."> * <tr><th>Input </th> - * <th>Result </th></tr> + * <th>Result </th></tr> * <tr><td>"A", "1"</td><td>true</td></tr> * <tr><td>"a", "111"</td><td>true</td></tr> * <tr><td>"A", "65536"</td><td>true</td></tr> @@ -257,23 +270,13 @@ public class CellReference { * @return <code>true</code> 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<lastQuotePos; i++) { // Note boundaries - skip outer quotes - char ch = reference.charAt(i); - if(ch != SPECIAL_NAME_DELIMITER) { - sb.append(ch); - continue; - } - if(i < lastQuotePos) { - if(reference.charAt(i+1) == SPECIAL_NAME_DELIMITER) { - // two consecutive quotes is the escape sequence for a single one - i++; // skip this and keep parsing the special name - sb.append(ch); - continue; - } - } - throw new RuntimeException("Bad sheet name quote escaping: (" + reference + ")"); - } - return sb.toString(); - } + // 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<lastQuotePos; i++) { // Note boundaries - skip outer quotes + char ch = reference.charAt(i); + if(ch != SPECIAL_NAME_DELIMITER) { + sb.append(ch); + continue; + } + if(i < lastQuotePos) { + if(reference.charAt(i+1) == SPECIAL_NAME_DELIMITER) { + // two consecutive quotes is the escape sequence for a single one + i++; // skip this and keep parsing the special name + sb.append(ch); + continue; + } + } + throw new RuntimeException("Bad sheet name quote escaping: (" + reference + ")"); + } + return sb.toString(); + } - /** - * Takes in a 0-based base-10 column and returns a ALPHA-26 - * representation. - * eg column #3 -> 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: + * <table border="0" cellpadding="1" cellspacing="0" summary="Example return values"> + * <tr><th align='left'>Result</th><th align='left'>Comment</th></tr> + * <tr><td>A1</td><td>Cell reference without sheet</td></tr> + * <tr><td>Sheet1!A1</td><td>Standard sheet name</td></tr> + * <tr><td>'O''Brien''s Sales'!A1' </td><td>Sheet name with special characters</td></tr> + * </table> + * @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: - * <table border="0" cellpadding="1" cellspacing="0" summary="Example return values"> - * <tr><th align='left'>Result</th><th align='left'>Comment</th></tr> - * <tr><td>A1</td><td>Cell reference without sheet</td></tr> - * <tr><td>Sheet1!A1</td><td>Standard sheet name</td></tr> - * <tr><td>'O''Brien''s Sales'!A1' </td><td>Sheet name with special characters</td></tr> - * </table> - * @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); + } } |