|
|
@@ -28,9 +28,8 @@ import org.apache.poi.hssf.record.SharedFormulaRecord; |
|
|
|
import org.apache.poi.hssf.record.SharedValueRecordBase; |
|
|
|
import org.apache.poi.hssf.record.TableRecord; |
|
|
|
import org.apache.poi.hssf.record.formula.ExpPtg; |
|
|
|
import org.apache.poi.hssf.record.formula.TblPtg; |
|
|
|
import org.apache.poi.hssf.util.CellRangeAddress8Bit; |
|
|
|
import org.apache.poi.hssf.util.CellReference; |
|
|
|
import org.apache.poi.ss.util.CellReference; |
|
|
|
|
|
|
|
/** |
|
|
|
* Manages various auxiliary records while constructing a |
|
|
@@ -45,26 +44,38 @@ import org.apache.poi.hssf.util.CellReference; |
|
|
|
*/ |
|
|
|
public final class SharedValueManager { |
|
|
|
|
|
|
|
// This class should probably be generalised to handle array and table groups too |
|
|
|
private static final class SharedValueGroup { |
|
|
|
private final SharedValueRecordBase _svr; |
|
|
|
private FormulaRecordAggregate[] _frAggs; |
|
|
|
private static final class SharedFormulaGroup { |
|
|
|
private final SharedFormulaRecord _sfr; |
|
|
|
private final FormulaRecordAggregate[] _frAggs; |
|
|
|
private int _numberOfFormulas; |
|
|
|
/** |
|
|
|
* Coordinates of the first cell having a formula that uses this shared formula. |
|
|
|
* This is often <i>but not always</i> the top left cell in the range covered by |
|
|
|
* {@link #_sfr} |
|
|
|
*/ |
|
|
|
private final CellReference _firstCell; |
|
|
|
|
|
|
|
public SharedValueGroup(SharedValueRecordBase svr) { |
|
|
|
_svr = svr; |
|
|
|
int width = svr.getLastColumn() - svr.getFirstColumn() + 1; |
|
|
|
int height = svr.getLastRow() - svr.getFirstRow() + 1; |
|
|
|
public SharedFormulaGroup(SharedFormulaRecord sfr, CellReference firstCell) { |
|
|
|
if (!sfr.isInRange(firstCell.getRow(), firstCell.getCol())) { |
|
|
|
throw new IllegalArgumentException("First formula cell " + firstCell.formatAsString() |
|
|
|
+ " is not shared formula range " + sfr.getRange().toString() + "."); |
|
|
|
} |
|
|
|
_sfr = sfr; |
|
|
|
_firstCell = firstCell; |
|
|
|
int width = sfr.getLastColumn() - sfr.getFirstColumn() + 1; |
|
|
|
int height = sfr.getLastRow() - sfr.getFirstRow() + 1; |
|
|
|
_frAggs = new FormulaRecordAggregate[width * height]; |
|
|
|
_numberOfFormulas = 0; |
|
|
|
} |
|
|
|
|
|
|
|
public void add(FormulaRecordAggregate agg) { |
|
|
|
if (_numberOfFormulas == 0) { |
|
|
|
if (_firstCell.getRow() != agg.getRow() || _firstCell.getCol() != agg.getColumn()) { |
|
|
|
throw new IllegalStateException("shared formula coding error"); |
|
|
|
} |
|
|
|
} |
|
|
|
if (_numberOfFormulas >= _frAggs.length) { |
|
|
|
// this probably shouldn't occur - problems with sample file "15228.xls" |
|
|
|
FormulaRecordAggregate[] temp = new FormulaRecordAggregate[_numberOfFormulas*2]; |
|
|
|
System.arraycopy(_frAggs, 0, temp, 0, _frAggs.length); |
|
|
|
_frAggs = temp; |
|
|
|
throw new RuntimeException("Too many formula records for shared formula group"); |
|
|
|
} |
|
|
|
_frAggs[_numberOfFormulas++] = agg; |
|
|
|
} |
|
|
@@ -75,49 +86,55 @@ public final class SharedValueManager { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public SharedValueRecordBase getSVR() { |
|
|
|
return _svr; |
|
|
|
public SharedFormulaRecord getSFR() { |
|
|
|
return _sfr; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Note - Sometimes the first formula in a group is not present (because the range |
|
|
|
* is sparsely populated), so this method can return <code>true</code> for a cell |
|
|
|
* that is not the top-left corner of the range. |
|
|
|
* @return <code>true</code> if this is the first formula cell in the group |
|
|
|
*/ |
|
|
|
public boolean isFirstMember(FormulaRecordAggregate agg) { |
|
|
|
return agg == _frAggs[0]; |
|
|
|
} |
|
|
|
public final String toString() { |
|
|
|
StringBuffer sb = new StringBuffer(64); |
|
|
|
sb.append(getClass().getName()).append(" ["); |
|
|
|
sb.append(_svr.getRange().toString()); |
|
|
|
sb.append(_sfr.getRange().toString()); |
|
|
|
sb.append("]"); |
|
|
|
return sb.toString(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Note - the 'first cell' of a shared formula group is not always the top-left cell |
|
|
|
* of the enclosing range. |
|
|
|
* @return <code>true</code> if the specified coordinates correspond to the 'first cell' |
|
|
|
* of this shared formula group. |
|
|
|
*/ |
|
|
|
public boolean isFirstCell(int row, int column) { |
|
|
|
return _firstCell.getRow() == row && _firstCell.getCol() == column; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public static final SharedValueManager EMPTY = new SharedValueManager( |
|
|
|
new SharedFormulaRecord[0], new ArrayRecord[0], new TableRecord[0]); |
|
|
|
new SharedFormulaRecord[0], new CellReference[0], new ArrayRecord[0], new TableRecord[0]); |
|
|
|
private final ArrayRecord[] _arrayRecords; |
|
|
|
private final TableRecord[] _tableRecords; |
|
|
|
private final Map _groupsBySharedFormulaRecord; |
|
|
|
private final Map<SharedFormulaRecord, SharedFormulaGroup> _groupsBySharedFormulaRecord; |
|
|
|
/** cached for optimization purposes */ |
|
|
|
private SharedValueGroup[] _groups; |
|
|
|
private SharedFormulaGroup[] _groups; |
|
|
|
|
|
|
|
private SharedValueManager(SharedFormulaRecord[] sharedFormulaRecords, |
|
|
|
ArrayRecord[] arrayRecords, TableRecord[] tableRecords) { |
|
|
|
CellReference[] firstCells, ArrayRecord[] arrayRecords, TableRecord[] tableRecords) { |
|
|
|
int nShF = sharedFormulaRecords.length; |
|
|
|
if (nShF != firstCells.length) { |
|
|
|
throw new IllegalArgumentException("array sizes don't match: " + nShF + "!=" + firstCells.length + "."); |
|
|
|
} |
|
|
|
_arrayRecords = arrayRecords; |
|
|
|
_tableRecords = tableRecords; |
|
|
|
Map m = new HashMap(sharedFormulaRecords.length * 3 / 2); |
|
|
|
for (int i = 0; i < sharedFormulaRecords.length; i++) { |
|
|
|
Map<SharedFormulaRecord, SharedFormulaGroup> m = new HashMap<SharedFormulaRecord, SharedFormulaGroup>(nShF * 3 / 2); |
|
|
|
for (int i = 0; i < nShF; i++) { |
|
|
|
SharedFormulaRecord sfr = sharedFormulaRecords[i]; |
|
|
|
m.put(sfr, new SharedValueGroup(sfr)); |
|
|
|
m.put(sfr, new SharedFormulaGroup(sfr, firstCells[i])); |
|
|
|
} |
|
|
|
_groupsBySharedFormulaRecord = m; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* @param firstCells |
|
|
|
* @param recs list of sheet records (possibly contains records for other parts of the Excel file) |
|
|
|
* @param startIx index of first row/cell record for current sheet |
|
|
|
* @param endIx one past index of last row/cell record for current sheet. It is important |
|
|
@@ -125,11 +142,11 @@ public final class SharedValueManager { |
|
|
|
* sheet (which could happen if endIx is chosen poorly). (see bug 44449) |
|
|
|
*/ |
|
|
|
public static SharedValueManager create(SharedFormulaRecord[] sharedFormulaRecords, |
|
|
|
ArrayRecord[] arrayRecords, TableRecord[] tableRecords) { |
|
|
|
if (sharedFormulaRecords.length + arrayRecords.length + tableRecords.length < 1) { |
|
|
|
CellReference[] firstCells, ArrayRecord[] arrayRecords, TableRecord[] tableRecords) { |
|
|
|
if (sharedFormulaRecords.length + firstCells.length + arrayRecords.length + tableRecords.length < 1) { |
|
|
|
return EMPTY; |
|
|
|
} |
|
|
|
return new SharedValueManager(sharedFormulaRecords, arrayRecords, tableRecords); |
|
|
|
return new SharedValueManager(sharedFormulaRecords, firstCells, arrayRecords, tableRecords); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
@@ -139,68 +156,30 @@ public final class SharedValueManager { |
|
|
|
*/ |
|
|
|
public SharedFormulaRecord linkSharedFormulaRecord(CellReference firstCell, FormulaRecordAggregate agg) { |
|
|
|
|
|
|
|
SharedValueGroup result = findGroup(getGroups(), firstCell); |
|
|
|
SharedFormulaGroup result = findFormulaGroup(getGroups(), firstCell); |
|
|
|
result.add(agg); |
|
|
|
return (SharedFormulaRecord) result.getSVR(); |
|
|
|
return result.getSFR(); |
|
|
|
} |
|
|
|
|
|
|
|
private static SharedValueGroup findGroup(SharedValueGroup[] groups, CellReference firstCell) { |
|
|
|
private static SharedFormulaGroup findFormulaGroup(SharedFormulaGroup[] groups, CellReference firstCell) { |
|
|
|
int row = firstCell.getRow(); |
|
|
|
int column = firstCell.getCol(); |
|
|
|
// Traverse the list of shared formulas and try to find the correct one for us |
|
|
|
|
|
|
|
// perhaps this could be optimised to some kind of binary search |
|
|
|
for (int i = 0; i < groups.length; i++) { |
|
|
|
SharedValueGroup svg = groups[i]; |
|
|
|
if (svg.getSVR().isFirstCell(row, column)) { |
|
|
|
SharedFormulaGroup svg = groups[i]; |
|
|
|
if (svg.isFirstCell(row, column)) { |
|
|
|
return svg; |
|
|
|
} |
|
|
|
} |
|
|
|
// else - no SharedFormulaRecord was found with the specified firstCell. |
|
|
|
// This is unusual, but one sample file exhibits the anomaly: "ex45046-21984.xls" |
|
|
|
// Excel seems to handle the problem OK, and doesn't even correct it. Perhaps POI should. |
|
|
|
|
|
|
|
// search for shared formula by range |
|
|
|
SharedValueGroup result = null; |
|
|
|
for (int i = 0; i < groups.length; i++) { |
|
|
|
SharedValueGroup svg = groups[i]; |
|
|
|
if (svg.getSVR().isInRange(row, column)) { |
|
|
|
if (result != null) { |
|
|
|
// This happens in sample file "15228.xls" |
|
|
|
if (sharedFormulasAreSame(result, svg)) { |
|
|
|
// hopefully this is OK - just use the first one since they are the same |
|
|
|
// not quite |
|
|
|
// TODO - fix file "15228.xls" so it opens in Excel after rewriting with POI |
|
|
|
} else { |
|
|
|
throw new RuntimeException("This cell is in the range of more than one distinct shared formula"); |
|
|
|
} |
|
|
|
} else { |
|
|
|
result = svg; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
if (result == null) { |
|
|
|
throw new RuntimeException("Failed to find a matching shared formula record"); |
|
|
|
} |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Handles the ugly situation (seen in example "15228.xls") where a shared formula cell is |
|
|
|
* covered by more than one shared formula range, but the formula cell's {@link ExpPtg} |
|
|
|
* doesn't identify any of them. |
|
|
|
* @return <code>true</code> if the underlying shared formulas are the same |
|
|
|
*/ |
|
|
|
private static boolean sharedFormulasAreSame(SharedValueGroup grpA, SharedValueGroup grpB) { |
|
|
|
// safe to cast here because this findGroup() is never called for ARRAY or TABLE formulas |
|
|
|
SharedFormulaRecord sfrA = (SharedFormulaRecord) grpA.getSVR(); |
|
|
|
SharedFormulaRecord sfrB = (SharedFormulaRecord) grpB.getSVR(); |
|
|
|
return sfrA.isFormulaSame(sfrB); |
|
|
|
// TODO - fix file "15228.xls" so it opens in Excel after rewriting with POI |
|
|
|
throw new RuntimeException("Failed to find a matching shared formula record"); |
|
|
|
} |
|
|
|
|
|
|
|
private SharedValueGroup[] getGroups() { |
|
|
|
private SharedFormulaGroup[] getGroups() { |
|
|
|
if (_groups == null) { |
|
|
|
SharedValueGroup[] groups = new SharedValueGroup[_groupsBySharedFormulaRecord.size()]; |
|
|
|
SharedFormulaGroup[] groups = new SharedFormulaGroup[_groupsBySharedFormulaRecord.size()]; |
|
|
|
_groupsBySharedFormulaRecord.values().toArray(groups); |
|
|
|
Arrays.sort(groups, SVGComparator); // make search behaviour more deterministic |
|
|
|
_groups = groups; |
|
|
@@ -208,11 +187,11 @@ public final class SharedValueManager { |
|
|
|
return _groups; |
|
|
|
} |
|
|
|
|
|
|
|
private static final Comparator SVGComparator = new Comparator() { |
|
|
|
private static final Comparator<SharedFormulaGroup> SVGComparator = new Comparator<SharedFormulaGroup>() { |
|
|
|
|
|
|
|
public int compare(Object a, Object b) { |
|
|
|
CellRangeAddress8Bit rangeA = ((SharedValueGroup)a).getSVR().getRange(); |
|
|
|
CellRangeAddress8Bit rangeB = ((SharedValueGroup)b).getSVR().getRange(); |
|
|
|
public int compare(SharedFormulaGroup a, SharedFormulaGroup b) { |
|
|
|
CellRangeAddress8Bit rangeA = a.getSFR().getRange(); |
|
|
|
CellRangeAddress8Bit rangeB = b.getSFR().getRange(); |
|
|
|
|
|
|
|
int cmp; |
|
|
|
cmp = rangeA.getFirstRow() - rangeB.getFirstRow(); |
|
|
@@ -228,44 +207,57 @@ public final class SharedValueManager { |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* The {@link SharedValueRecordBase} record returned by this method |
|
|
|
* @param firstCell the cell coordinates as read from the {@link ExpPtg} or {@link TblPtg} |
|
|
|
* of the current formula. Note - this is usually not the same as the cell coordinates |
|
|
|
* of the formula's cell. |
|
|
|
* Gets the {@link SharedValueRecordBase} record if it should be encoded immediately after the |
|
|
|
* formula record contained in the specified {@link FormulaRecordAggregate} agg. Note - the |
|
|
|
* shared value record always appears after the first formula record in the group. For arrays |
|
|
|
* and tables the first formula is always the in the top left cell. However, since shared |
|
|
|
* formula groups can be sparse and/or overlap, the first formula may not actually be in the |
|
|
|
* top left cell. |
|
|
|
* |
|
|
|
* @return the SHRFMLA, TABLE or ARRAY record for this formula cell, if it is the first cell of a |
|
|
|
* table or array region. <code>null</code> if |
|
|
|
* @return the SHRFMLA, TABLE or ARRAY record for the formula cell, if it is the first cell of |
|
|
|
* a table or array region. <code>null</code> if the formula cell is not shared/array/table, |
|
|
|
* or if the specified formula is not the the first in the group. |
|
|
|
*/ |
|
|
|
public SharedValueRecordBase getRecordForFirstCell(CellReference firstCell, FormulaRecordAggregate agg) { |
|
|
|
public SharedValueRecordBase getRecordForFirstCell(FormulaRecordAggregate agg) { |
|
|
|
CellReference firstCell = agg.getFormulaRecord().getFormula().getExpReference(); |
|
|
|
// perhaps this could be optimised by consulting the (somewhat unreliable) isShared flag |
|
|
|
// and/or distinguishing between tExp and tTbl. |
|
|
|
if (firstCell == null) { |
|
|
|
// not a shared/array/table formula |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
int row = firstCell.getRow(); |
|
|
|
int column = firstCell.getCol(); |
|
|
|
boolean isTopLeft = agg.getRow() == row && agg.getColumn() == column; |
|
|
|
if (isTopLeft) { |
|
|
|
for (int i = 0; i < _tableRecords.length; i++) { |
|
|
|
TableRecord tr = _tableRecords[i]; |
|
|
|
if (tr.isFirstCell(row, column)) { |
|
|
|
return tr; |
|
|
|
} |
|
|
|
if (agg.getRow() != row || agg.getColumn() != column) { |
|
|
|
// not the first formula cell in the group |
|
|
|
return null; |
|
|
|
} |
|
|
|
SharedFormulaGroup[] groups = getGroups(); |
|
|
|
for (int i = 0; i < groups.length; i++) { |
|
|
|
// note - logic for finding correct shared formula group is slightly |
|
|
|
// more complicated since the first cell |
|
|
|
SharedFormulaGroup sfg = groups[i]; |
|
|
|
if (sfg.isFirstCell(row, column)) { |
|
|
|
return sfg.getSFR(); |
|
|
|
} |
|
|
|
for (int i = 0; i < _arrayRecords.length; i++) { |
|
|
|
ArrayRecord ar = _arrayRecords[i]; |
|
|
|
if (ar.isFirstCell(row, column)) { |
|
|
|
return ar; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Since arrays and tables cannot be sparse (all cells in range participate) |
|
|
|
// The first cell will be the top left in the range. So we can match the |
|
|
|
// ARRAY/TABLE record directly. |
|
|
|
|
|
|
|
for (int i = 0; i < _tableRecords.length; i++) { |
|
|
|
TableRecord tr = _tableRecords[i]; |
|
|
|
if (tr.isFirstCell(row, column)) { |
|
|
|
return tr; |
|
|
|
} |
|
|
|
} else { |
|
|
|
// Since arrays and tables cannot be sparse (all cells in range participate) |
|
|
|
// no need to search arrays and tables if agg is not the top left cell |
|
|
|
} |
|
|
|
SharedValueGroup[] groups = getGroups(); |
|
|
|
for (int i = 0; i < groups.length; i++) { |
|
|
|
SharedValueGroup svg = groups[i]; |
|
|
|
SharedValueRecordBase svr = svg.getSVR(); |
|
|
|
if (svr.isFirstCell(row, column)) { |
|
|
|
if (svg.isFirstMember(agg)) { |
|
|
|
return svr; |
|
|
|
} |
|
|
|
return null; |
|
|
|
for (int i = 0; i < _arrayRecords.length; i++) { |
|
|
|
ArrayRecord ar = _arrayRecords[i]; |
|
|
|
if (ar.isFirstCell(row, column)) { |
|
|
|
return ar; |
|
|
|
} |
|
|
|
} |
|
|
|
return null; |
|
|
@@ -276,7 +268,7 @@ public final class SharedValueManager { |
|
|
|
* to plain unshared formulas |
|
|
|
*/ |
|
|
|
public void unlink(SharedFormulaRecord sharedFormulaRecord) { |
|
|
|
SharedValueGroup svg = (SharedValueGroup) _groupsBySharedFormulaRecord.remove(sharedFormulaRecord); |
|
|
|
SharedFormulaGroup svg = _groupsBySharedFormulaRecord.remove(sharedFormulaRecord); |
|
|
|
_groups = null; // be sure to reset cached value |
|
|
|
if (svg == null) { |
|
|
|
throw new IllegalStateException("Failed to find formulas for shared formula"); |