<!-- Don't forget to update status.xml too! -->
<release version="3.2-alpha1" date="2008-??-??">
+ <action dev="POI-DEVELOPERS" type="add">45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas</action>
<action dev="POI-DEVELOPERS" type="add">Optimised the FormulaEvaluator to take cell dependencies into account</action>
<action dev="POI-DEVELOPERS" type="add">16936 - Initial support for whole-row cell styling</action>
<action dev="POI-DEVELOPERS" type="add">Update hssf.extractor.ExcelExtractor to optionally output blank cells too</action>
<!-- Don't forget to update changes.xml too! -->
<changes>
<release version="3.2-alpha1" date="2008-??-??">
+ <action dev="POI-DEVELOPERS" type="add">45865 modified Formula Parser/Evaluator to handle cross-worksheet formulas</action>
<action dev="POI-DEVELOPERS" type="add">Optimised the FormulaEvaluator to take cell dependencies into account</action>
<action dev="POI-DEVELOPERS" type="add">16936 - Initial support for whole-row cell styling</action>
<action dev="POI-DEVELOPERS" type="add">Update hssf.extractor.ExcelExtractor to optionally output blank cells too</action>
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;
/**
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];
}
return ExternSheetRecord.combine(esrs);
}
- public LinkTable(short numberOfSheets, WorkbookRecordList workbookRecordList) {
+ public LinkTable(int numberOfSheets, WorkbookRecordList workbookRecordList) {
_workbookRecordList = workbookRecordList;
_definedNames = new ArrayList();
_externalBookBlocks = new ExternalBookBlock[] {
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);
}
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);
}
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;
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;
* @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);
}
* 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
* 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;
}
/**
*/
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!
}
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.
* @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
*/
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;
}
{
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();
package org.apache.poi.hssf.record.formula;
import org.apache.poi.hssf.record.RecordInputStream;
-import org.apache.poi.hssf.usermodel.HSSFWorkbook;
-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;
/**
* @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
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) {
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
}
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);
import org.apache.poi.hssf.record.RecordInputStream;
import org.apache.poi.hssf.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;
/**
* @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
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;
}
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();
int sheetIndex = _uBook.getSheetIndex(sheetName);\r
return _iBook.checkExternSheet(sheetIndex);\r
}\r
+ public int getExternalSheetIndex(String workbookName, String sheetName) {\r
+ return _iBook.getExternalSheetIndex(workbookName, sheetName);\r
+ }\r
\r
public EvaluationName getName(int index) {\r
return new Name(_iBook.getNameRecord(index), index);\r
public int getSheetIndex(HSSFSheet sheet) {\r
return _uBook.getSheetIndex(sheet);\r
}\r
+ public int getSheetIndex(String sheetName) {\r
+ return _uBook.getSheetIndex(sheetName);\r
+ }\r
\r
public String getSheetName(int sheetIndex) {\r
return _uBook.getSheetName(sheetIndex);\r
}\r
public int convertFromExternSheetIndex(int externSheetIndex) {\r
return _iBook.getSheetIndexFromExternSheetIndex(externSheetIndex);\r
-}\r
+ }\r
\r
+ public ExternalSheet getExternalSheet(int externSheetIndex) {\r
+ return _iBook.getExternalSheet(externSheetIndex);\r
+ }\r
+ \r
public HSSFWorkbook getWorkbook() {\r
return _uBook;\r
}\r
import org.apache.poi.hssf.record.formula.eval.NumberEval;\r
import org.apache.poi.hssf.record.formula.eval.StringEval;\r
import org.apache.poi.hssf.record.formula.eval.ValueEval;\r
+import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment;\r
import org.apache.poi.ss.formula.WorkbookEvaluator;\r
\r
/**\r
public HSSFFormulaEvaluator(HSSFWorkbook workbook) {\r
_bookEvaluator = new WorkbookEvaluator(HSSFEvaluationWorkbook.create(workbook));\r
}\r
+ \r
+ /**\r
+ * Coordinates several formula evaluators together so that formulas that involve external\r
+ * references can be evaluated.\r
+ * @param workbookNames the simple file names used to identify the workbooks in formulas\r
+ * with external links (for example "MyData.xls" as used in a formula "[MyData.xls]Sheet1!A1")\r
+ * @param evaluators all evaluators for the full set of workbooks required by the formulas. \r
+ */\r
+ public static void setupEnvironment(String[] workbookNames, HSSFFormulaEvaluator[] evaluators) {\r
+ WorkbookEvaluator[] wbEvals = new WorkbookEvaluator[evaluators.length];\r
+ for (int i = 0; i < wbEvals.length; i++) {\r
+ wbEvals[i] = evaluators[i]._bookEvaluator;\r
+ }\r
+ CollaboratingWorkbooksEnvironment.setup(workbookNames, wbEvals);\r
+ }\r
\r
/**\r
* Does nothing\r
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;
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() {
* @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() {
--- /dev/null
+/* ====================================================================
+ 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;
+ }
+}
+ 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();
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) {
* @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];
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;
*/\r
public interface EvaluationWorkbook {\r
String getSheetName(int sheetIndex);\r
+ /**\r
+ * @return -1 if the specified sheet is from a different book\r
+ */\r
int getSheetIndex(HSSFSheet sheet);\r
+ int getSheetIndex(String sheetName);\r
\r
HSSFSheet getSheet(int sheetIndex);\r
\r
+ /**\r
+ * @return <code>null</code> if externSheetIndex refers to a sheet inside the current workbook\r
+ */\r
+ ExternalSheet getExternalSheet(int externSheetIndex);\r
int convertFromExternSheetIndex(int externSheetIndex);\r
EvaluationName getName(NamePtg namePtg);\r
String resolveNameXText(NameXPtg ptg);\r
Ptg[] getFormulaTokens(HSSFCell cell);\r
+ \r
+ class ExternalSheet {\r
+ private final String _workbookName;\r
+ private final String _sheetName;\r
+\r
+ public ExternalSheet(String workbookName, String sheetName) {\r
+ _workbookName = workbookName;\r
+ _sheetName = sheetName;\r
+ }\r
+ public String getWorkbookName() {\r
+ return _workbookName;\r
+ }\r
+ public String getSheetName() {\r
+ return _sheetName;\r
+ }\r
+ }\r
}\r
--- /dev/null
+/* ====================================================================
+ 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();
+}
*/
private Identifier parseIdentifier() {
StringBuffer sb = new StringBuffer();
- if (!IsAlpha(look) && look != '\'') {
+ if (!IsAlpha(look) && look != '\'' && look != '[') {
throw expected("Name");
}
boolean isQuoted = look == '\'';
} 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 == '.') {
+ while (IsAlNum(look) || look == '.' || look == '[' || look == ']') {
sb.append(look);
GetChar();
}
// 3-D ref
// this code assumes iden is a sheetName
// TODO - handle <book name> ! <named range name>
- int externIdx = book.getExternalSheetIndex(iden.getName());
+ int externIdx = getExternalSheetIndex(iden.getName());
String secondIden = parseUnquotedIdentifier();
AreaReference areaRef = parseArea(secondIden);
if (areaRef == null) {
+ 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
Match('}');
return arrayNode;
}
- if (IsAlpha(look) || look == '\''){
+ if (IsAlpha(look) || look == '\'' || look == '['){
return parseFunctionReferenceOrName();
}
// else - assume number
*/\r
EvaluationName getName(String name);\r
\r
- int getExternalSheetIndex(String sheetName);\r
NameXPtg getNameXPtg(String name);\r
+\r
+ /**\r
+ * gets the externSheet index for a sheet from this workbook\r
+ */\r
+ int getExternalSheetIndex(String sheetName);\r
+ /**\r
+ * gets the externSheet index for a sheet from an external workbook\r
+ * @param workbookName e.g. "Budget.xls"\r
+ * @param sheetName a name of a sheet in that workbook\r
+ */\r
+ int getExternalSheetIndex(String workbookName, String sheetName);\r
}\r
\r
import org.apache.poi.hssf.record.formula.NamePtg;\r
import org.apache.poi.hssf.record.formula.NameXPtg;\r
+import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet;\r
\r
/**\r
* Abstracts a workbook for the purpose of converting formula to text.<br/>\r
*/\r
public interface FormulaRenderingWorkbook {\r
\r
+ /**\r
+ * @return <code>null</code> if externSheetIndex refers to a sheet inside the current workbook\r
+ */\r
+ ExternalSheet getExternalSheet(int externSheetIndex);\r
String getSheetNameByExternSheet(int externSheetIndex);\r
String resolveNameXText(NameXPtg nameXPtg);\r
String getNameText(NamePtg namePtg);\r
import org.apache.poi.hssf.usermodel.HSSFRow;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.util.CellReference;
+import org.apache.poi.ss.formula.EvaluationWorkbook.ExternalSheet;
/**
* Evaluates formula cells.<p/>
*
* @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);
_evaluationListener = evaluationListener;
_cache = new EvaluationCache(evaluationListener);
_sheetIndexesBySheet = new IdentityHashMap();
+ _collaboratingWorkbookEnvironment = CollaboratingWorkbooksEnvironment.EMPTY;
}
/**
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
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);
}
/**
*/
public void notifySetFormula(HSSFSheet 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(HSSFSheet 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();
public ValueEval evaluate(HSSFCell 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));
}
}
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
* 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;
}
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);
} 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);
}
import org.apache.poi.hssf.record.formula.PercentPtg;
import org.apache.poi.hssf.record.formula.PowerPtg;
import org.apache.poi.hssf.record.formula.Ptg;
+import org.apache.poi.hssf.record.formula.Ref3DPtg;
import org.apache.poi.hssf.record.formula.RefPtg;
import org.apache.poi.hssf.record.formula.StringPtg;
import org.apache.poi.hssf.record.formula.SubtractPtg;
import org.apache.poi.hssf.record.formula.UnaryMinusPtg;
import org.apache.poi.hssf.record.formula.UnaryPlusPtg;
+import org.apache.poi.hssf.usermodel.FormulaExtractor;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.hssf.usermodel.HSSFErrorConstants;
import org.apache.poi.hssf.usermodel.HSSFEvaluationWorkbook;
assertEquals("'true'!B2", cell.getCellFormula());
}
+
+ public void testParseExternalWorkbookReference() {
+ HSSFWorkbook wbA = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaA.xls");
+ HSSFCell cell = wbA.getSheetAt(0).getRow(0).getCell(0);
+
+ // make sure formula in sample is as expected
+ assertEquals("[multibookFormulaB.xls]BSheet1!B1", cell.getCellFormula());
+ Ptg[] expectedPtgs = FormulaExtractor.getPtgs(cell);
+ confirmSingle3DRef(expectedPtgs, 1);
+
+ // now try (re-)parsing the formula
+ Ptg[] actualPtgs = HSSFFormulaParser.parse("[multibookFormulaB.xls]BSheet1!B1", wbA);
+ confirmSingle3DRef(actualPtgs, 1); // externalSheetIndex 1 -> BSheet1
+
+ // try parsing a formula pointing to a different external sheet
+ Ptg[] otherPtgs = HSSFFormulaParser.parse("[multibookFormulaB.xls]AnotherSheet!B1", wbA);
+ confirmSingle3DRef(otherPtgs, 0); // externalSheetIndex 0 -> AnotherSheet
+
+ // try setting the same formula in a cell
+ cell.setCellFormula("[multibookFormulaB.xls]AnotherSheet!B1");
+ assertEquals("[multibookFormulaB.xls]AnotherSheet!B1", cell.getCellFormula());
+ }
+ private static void confirmSingle3DRef(Ptg[] ptgs, int expectedExternSheetIndex) {
+ assertEquals(1, ptgs.length);
+ Ptg ptg0 = ptgs[0];
+ assertEquals(Ref3DPtg.class, ptg0.getClass());
+ assertEquals(expectedExternSheetIndex, ((Ref3DPtg)ptg0).getExternSheetIndex());
+ }
+
}
assertEquals( 34, record.getRecordSize() ); //sid+size+data
- assertEquals("testURL", record.getURL().getString());
+ assertEquals("testURL", record.getURL());
UnicodeString[] sheetNames = record.getSheetNames();
assertEquals(2, sheetNames.length);
assertEquals("Sheet1", sheetNames[0].getString());
import junit.framework.TestCase;
+import org.apache.poi.hssf.HSSFTestDataSamples;
import org.apache.poi.hssf.record.formula.AreaErrPtg;
import org.apache.poi.hssf.record.formula.AttrPtg;
import org.apache.poi.hssf.record.formula.DeletedArea3DPtg;
import org.apache.poi.hssf.record.formula.eval.ErrorEval;
import org.apache.poi.hssf.record.formula.eval.NumberEval;
import org.apache.poi.hssf.record.formula.eval.ValueEval;
+import org.apache.poi.hssf.usermodel.HSSFCell;
+import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
/**
- * Tests {@link WorkbookEvaluator}.
+ * Tests {@link WorkbookEvaluator}.
*
* @author Josh Micich
*/
public class TestWorkbookEvaluator extends TestCase {
-
+
/**
* Make sure that the evaluator can directly handle tAttrSum (instead of relying on re-parsing
- * the whole formula which converts tAttrSum to tFuncVar("SUM") )
+ * the whole formula which converts tAttrSum to tFuncVar("SUM") )
*/
public void testAttrSum() {
-
+
Ptg[] ptgs = {
new IntPtg(42),
AttrPtg.SUM,
};
-
+
ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null);
assertEquals(42, ((NumberEval)result).getNumberValue(), 0.0);
}
-
+
/**
* Make sure that the evaluator can directly handle (deleted) ref error tokens
- * (instead of relying on re-parsing the whole formula which converts these
- * to the error constant #REF! )
+ * (instead of relying on re-parsing the whole formula which converts these
+ * to the error constant #REF! )
*/
public void testRefErr() {
-
+
confirmRefErr(new RefErrorPtg());
confirmRefErr(new AreaErrPtg());
confirmRefErr(new DeletedRef3DPtg(0));
Ptg[] ptgs = {
ptg,
};
-
+
ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null);
assertEquals(ErrorEval.REF_INVALID, result);
}
-
+
/**
* Make sure that the evaluator can directly handle tAttrSum (instead of relying on re-parsing
- * the whole formula which converts tAttrSum to tFuncVar("SUM") )
+ * the whole formula which converts tAttrSum to tFuncVar("SUM") )
*/
public void testMemFunc() {
-
+
Ptg[] ptgs = {
new IntPtg(42),
AttrPtg.SUM,
};
-
+
ValueEval result = new WorkbookEvaluator(null).evaluateFormula(0, 0, 0, ptgs, null);
assertEquals(42, ((NumberEval)result).getNumberValue(), 0.0);
}
-
-
+
+
+ public void testEvaluateMultipleWorkbooks() {
+ HSSFWorkbook wbA = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaA.xls");
+ HSSFWorkbook wbB = HSSFTestDataSamples.openSampleWorkbook("multibookFormulaB.xls");
+
+ HSSFFormulaEvaluator evaluatorA = new HSSFFormulaEvaluator(wbA);
+ HSSFFormulaEvaluator evaluatorB = new HSSFFormulaEvaluator(wbB);
+
+ // Hook up the workbook evaluators to enable evaluation of formulas across books
+ String[] bookNames = { "multibookFormulaA.xls", "multibookFormulaB.xls", };
+ HSSFFormulaEvaluator[] evaluators = { evaluatorA, evaluatorB, };
+ HSSFFormulaEvaluator.setupEnvironment(bookNames, evaluators);
+
+ HSSFCell cell;
+
+ HSSFSheet aSheet1 = wbA.getSheetAt(0);
+ HSSFSheet bSheet1 = wbB.getSheetAt(0);
+
+ // Simple case - single link from wbA to wbB
+ confirmFormula(wbA, 0, 0, 0, "[multibookFormulaB.xls]BSheet1!B1");
+ cell = aSheet1.getRow(0).getCell(0);
+ confirmEvaluation(35, evaluatorA, cell);
+
+
+ // more complex case - back link into wbA
+ // [wbA]ASheet1!A2 references (among other things) [wbB]BSheet1!B2
+ confirmFormula(wbA, 0, 1, 0, "[multibookFormulaB.xls]BSheet1!$B$2+2*A3");
+ // [wbB]BSheet1!B2 references (among other things) [wbA]AnotherSheet!A1:B2
+ confirmFormula(wbB, 0, 1, 1, "SUM([multibookFormulaA.xls]AnotherSheet!$A$1:$B$2)+B3");
+
+ cell = aSheet1.getRow(1).getCell(0);
+ confirmEvaluation(264, evaluatorA, cell);
+
+ // change [wbB]BSheet1!B3 (from 50 to 60)
+ bSheet1.getRow(2).getCell(1).setCellValue(60);
+ evaluatorB.setCachedPlainValue(bSheet1, 2, 1, new NumberEval(60));
+ confirmEvaluation(274, evaluatorA, cell);
+
+ // change [wbA]ASheet1!A3 (from 100 to 80)
+ aSheet1.getRow(2).getCell(0).setCellValue(80);
+ evaluatorA.setCachedPlainValue(aSheet1, 2, 0, new NumberEval(80));
+ confirmEvaluation(234, evaluatorA, cell);
+
+ // change [wbA]AnotherSheet!A1 (from 2 to 3)
+ wbA.getSheetAt(1).getRow(0).getCell(0).setCellValue(3);
+ evaluatorA.setCachedPlainValue(wbA.getSheetAt(1), 0, 0, new NumberEval(3));
+ confirmEvaluation(235, evaluatorA, cell);
+ }
+
+ private static void confirmEvaluation(double expectedValue, HSSFFormulaEvaluator fe, HSSFCell cell) {
+ assertEquals(expectedValue, fe.evaluate(cell).getNumberValue(), 0.0);
+ }
+
+ private static void confirmFormula(HSSFWorkbook wb, int sheetIndex, int rowIndex, int columnIndex,
+ String expectedFormula) {
+ HSSFCell cell = wb.getSheetAt(sheetIndex).getRow(rowIndex).getCell(columnIndex);
+ assertEquals(expectedFormula, cell.getCellFormula());
+ }
}