Fix formula parser to properly support the range operator. Small fixes to parsing of sheet names and full column references.

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@699487 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Josh Micich 2008-09-26 20:25:45 +00:00
parent 3e317e0747
commit b22f939c79
10 changed files with 548 additions and 346 deletions

View File

@ -30,248 +30,264 @@ import org.apache.poi.util.LittleEndian;
* @author Jason Height (jheight at chariot dot net dot au) * @author Jason Height (jheight at chariot dot net dot au)
*/ */
public abstract class AreaPtgBase extends OperandPtg implements AreaI { public abstract class AreaPtgBase extends OperandPtg implements AreaI {
/** /**
* TODO - (May-2008) fix subclasses of AreaPtg 'AreaN~' which are used in shared formulas. * TODO - (May-2008) fix subclasses of AreaPtg 'AreaN~' which are used in shared formulas.
* see similar comment in ReferencePtg * see similar comment in ReferencePtg
*/ */
protected final RuntimeException notImplemented() { protected final RuntimeException notImplemented() {
return new RuntimeException("Coding Error: This method should never be called. This ptg should be converted"); return new RuntimeException("Coding Error: This method should never be called. This ptg should be converted");
} }
/** zero based, unsigned 16 bit */ /** zero based, unsigned 16 bit */
private int field_1_first_row; private int field_1_first_row;
/** zero based, unsigned 16 bit */ /** zero based, unsigned 16 bit */
private int field_2_last_row; private int field_2_last_row;
/** zero based, unsigned 8 bit */ /** zero based, unsigned 8 bit */
private int field_3_first_column; private int field_3_first_column;
/** zero based, unsigned 8 bit */ /** zero based, unsigned 8 bit */
private int field_4_last_column; private int field_4_last_column;
private final static BitField rowRelative = BitFieldFactory.getInstance(0x8000); private final static BitField rowRelative = BitFieldFactory.getInstance(0x8000);
private final static BitField colRelative = BitFieldFactory.getInstance(0x4000); private final static BitField colRelative = BitFieldFactory.getInstance(0x4000);
private final static BitField columnMask = BitFieldFactory.getInstance(0x3FFF); private final static BitField columnMask = BitFieldFactory.getInstance(0x3FFF);
protected AreaPtgBase() { protected AreaPtgBase() {
// do nothing // do nothing
} }
protected AreaPtgBase(String arearef) { protected AreaPtgBase(String arearef) {
AreaReference ar = new AreaReference(arearef); AreaReference ar = new AreaReference(arearef);
CellReference firstCell = ar.getFirstCell(); CellReference firstCell = ar.getFirstCell();
CellReference lastCell = ar.getLastCell(); CellReference lastCell = ar.getLastCell();
setFirstRow(firstCell.getRow()); setFirstRow(firstCell.getRow());
setFirstColumn(firstCell.getCol()); setFirstColumn(firstCell.getCol());
setLastRow(lastCell.getRow()); setLastRow(lastCell.getRow());
setLastColumn(lastCell.getCol()); setLastColumn(lastCell.getCol());
setFirstColRelative(!firstCell.isColAbsolute()); setFirstColRelative(!firstCell.isColAbsolute());
setLastColRelative(!lastCell.isColAbsolute()); setLastColRelative(!lastCell.isColAbsolute());
setFirstRowRelative(!firstCell.isRowAbsolute()); setFirstRowRelative(!firstCell.isRowAbsolute());
setLastRowRelative(!lastCell.isRowAbsolute()); setLastRowRelative(!lastCell.isRowAbsolute());
} }
protected AreaPtgBase(int firstRow, int lastRow, int firstColumn, int lastColumn, protected AreaPtgBase(int firstRow, int lastRow, int firstColumn, int lastColumn,
boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) { boolean firstRowRelative, boolean lastRowRelative, boolean firstColRelative, boolean lastColRelative) {
checkColumnBounds(firstColumn); checkColumnBounds(firstColumn);
checkColumnBounds(lastColumn); checkColumnBounds(lastColumn);
checkRowBounds(firstRow); checkRowBounds(firstRow);
checkRowBounds(lastRow); checkRowBounds(lastRow);
setFirstRow(firstRow);
setLastRow(lastRow);
setFirstColumn(firstColumn);
setLastColumn(lastColumn);
setFirstRowRelative(firstRowRelative);
setLastRowRelative(lastRowRelative);
setFirstColRelative(firstColRelative);
setLastColRelative(lastColRelative);
}
private static void checkColumnBounds(int colIx) { if (lastRow > firstRow) {
if((colIx & 0x0FF) != colIx) { setFirstRow(firstRow);
throw new IllegalArgumentException("colIx (" + colIx + ") is out of range"); setLastRow(lastRow);
} setFirstRowRelative(firstRowRelative);
} setLastRowRelative(lastRowRelative);
private static void checkRowBounds(int rowIx) { } else {
if((rowIx & 0x0FFFF) != rowIx) { setFirstRow(lastRow);
throw new IllegalArgumentException("rowIx (" + rowIx + ") is out of range"); setLastRow(firstRow);
} setFirstRowRelative(lastRowRelative);
} setLastRowRelative(firstRowRelative);
}
protected final void readCoordinates(RecordInputStream in) { if (lastColumn > firstColumn) {
field_1_first_row = in.readUShort(); setFirstColumn(firstColumn);
field_2_last_row = in.readUShort(); setLastColumn(lastColumn);
field_3_first_column = in.readUShort(); setFirstColRelative(firstColRelative);
field_4_last_column = in.readUShort(); setLastColRelative(lastColRelative);
} } else {
protected final void writeCoordinates(byte[] array, int offset) { setFirstColumn(lastColumn);
LittleEndian.putUShort(array, offset + 0, field_1_first_row); setLastColumn(firstColumn);
LittleEndian.putUShort(array, offset + 2, field_2_last_row); setFirstColRelative(lastColRelative);
LittleEndian.putUShort(array, offset + 4, field_3_first_column); setLastColRelative(firstColRelative);
LittleEndian.putUShort(array, offset + 6, field_4_last_column); }
} }
/** private static void checkColumnBounds(int colIx) {
* @return the first row in the area if((colIx & 0x0FF) != colIx) {
*/ throw new IllegalArgumentException("colIx (" + colIx + ") is out of range");
public final int getFirstRow() { }
return field_1_first_row; }
} 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) {
* sets the first row field_1_first_row = in.readUShort();
* @param rowIx number (0-based) field_2_last_row = in.readUShort();
*/ field_3_first_column = in.readUShort();
public final void setFirstRow(int rowIx) { field_4_last_column = in.readUShort();
checkRowBounds(rowIx); }
field_1_first_row = rowIx; 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 last row in the range (x2 in x1,y1-x2,y2) * @return the first row in the area
*/ */
public final int getLastRow() { public final int getFirstRow() {
return field_2_last_row; return field_1_first_row;
} }
/** /**
* @param rowIx last row number in the area * sets the first row
*/ * @param rowIx number (0-based)
public final void setLastRow(int rowIx) { */
checkRowBounds(rowIx); public final void setFirstRow(int rowIx) {
field_2_last_row = rowIx; checkRowBounds(rowIx);
} field_1_first_row = rowIx;
}
/** /**
* @return the first column number in the area. * @return last row in the range (x2 in x1,y1-x2,y2)
*/ */
public final int getFirstColumn() { public final int getLastRow() {
return columnMask.getValue(field_3_first_column); return field_2_last_row;
} }
/** /**
* @return the first column number + the options bit settings unstripped * @param rowIx last row number in the area
*/ */
public final short getFirstColumnRaw() { public final void setLastRow(int rowIx) {
return (short) field_3_first_column; // TODO checkRowBounds(rowIx);
} field_2_last_row = rowIx;
}
/** /**
* @return whether or not the first row is a relative reference or not. * @return the first column number in the area.
*/ */
public final boolean isFirstRowRelative() { public final int getFirstColumn() {
return rowRelative.isSet(field_3_first_column); return columnMask.getValue(field_3_first_column);
} }
/** /**
* sets the first row to relative or not * @return the first column number + the options bit settings unstripped
* @param rel is relative or not. */
*/ public final short getFirstColumnRaw() {
public final void setFirstRowRelative(boolean rel) { return (short) field_3_first_column; // TODO
field_3_first_column=rowRelative.setBoolean(field_3_first_column,rel); }
}
/** /**
* @return isrelative first column to relative or not * @return whether or not the first row is a relative reference or not.
*/ */
public final boolean isFirstColRelative() { public final boolean isFirstRowRelative() {
return colRelative.isSet(field_3_first_column); return rowRelative.isSet(field_3_first_column);
} }
/** /**
* set whether the first column is relative * sets the first row to relative or not
*/ * @param rel is relative or not.
public final void setFirstColRelative(boolean rel) { */
field_3_first_column=colRelative.setBoolean(field_3_first_column,rel); public final void setFirstRowRelative(boolean rel) {
} field_3_first_column=rowRelative.setBoolean(field_3_first_column,rel);
}
/** /**
* set the first column in the area * @return isrelative first column to relative or not
*/ */
public final void setFirstColumn(int colIx) { public final boolean isFirstColRelative() {
checkColumnBounds(colIx); return colRelative.isSet(field_3_first_column);
field_3_first_column=columnMask.setValue(field_3_first_column, colIx); }
}
/** /**
* set the first column irrespective of the bitmasks * set whether the first column is relative
*/ */
public final void setFirstColumnRaw(int column) { public final void setFirstColRelative(boolean rel) {
field_3_first_column = column; field_3_first_column=colRelative.setBoolean(field_3_first_column,rel);
} }
/** /**
* @return lastcolumn in the area * set the first column in the area
*/ */
public final int getLastColumn() { public final void setFirstColumn(int colIx) {
return columnMask.getValue(field_4_last_column); checkColumnBounds(colIx);
} field_3_first_column=columnMask.setValue(field_3_first_column, colIx);
}
/** /**
* @return last column and bitmask (the raw field) * set the first column irrespective of the bitmasks
*/ */
public final short getLastColumnRaw() { public final void setFirstColumnRaw(int column) {
return (short) field_4_last_column; field_3_first_column = column;
} }
/** /**
* @return last row relative or not * @return lastcolumn in the area
*/ */
public final boolean isLastRowRelative() { public final int getLastColumn() {
return rowRelative.isSet(field_4_last_column); return columnMask.getValue(field_4_last_column);
} }
/** /**
* set whether the last row is relative or not * @return last column and bitmask (the raw field)
* @param rel <code>true</code> if the last row relative, else */
* <code>false</code> public final short getLastColumnRaw() {
*/ return (short) field_4_last_column;
public final void setLastRowRelative(boolean rel) { }
field_4_last_column=rowRelative.setBoolean(field_4_last_column,rel);
}
/** /**
* @return lastcol relative or not * @return last row relative or not
*/ */
public final boolean isLastColRelative() { public final boolean isLastRowRelative() {
return colRelative.isSet(field_4_last_column); return rowRelative.isSet(field_4_last_column);
} }
/** /**
* set whether the last column should be relative or not * set whether the last row is relative or not
*/ * @param rel <code>true</code> if the last row relative, else
public final void setLastColRelative(boolean rel) { * <code>false</code>
field_4_last_column=colRelative.setBoolean(field_4_last_column,rel); */
} public final void setLastRowRelative(boolean rel) {
field_4_last_column=rowRelative.setBoolean(field_4_last_column,rel);
}
/** /**
* set the last column in the area * @return lastcol relative or not
*/ */
public final void setLastColumn(int colIx) { public final boolean isLastColRelative() {
checkColumnBounds(colIx); return colRelative.isSet(field_4_last_column);
field_4_last_column=columnMask.setValue(field_4_last_column, colIx); }
}
/** /**
* set the last column irrespective of the bitmasks * set whether the last column should be relative or not
*/ */
public final void setLastColumnRaw(short column) { public final void setLastColRelative(boolean rel) {
field_4_last_column = column; field_4_last_column=colRelative.setBoolean(field_4_last_column,rel);
} }
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(); * set the last column in the area
} */
return topLeft.formatAsString() + ":" + botRight.formatAsString(); public final void setLastColumn(int colIx) {
} checkColumnBounds(colIx);
field_4_last_column=columnMask.setValue(field_4_last_column, colIx);
}
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());
public byte getDefaultOperandClass() { if(AreaReference.isWholeColumnReference(topLeft, botRight)) {
return Ptg.CLASS_REF; return (new AreaReference(topLeft, botRight)).formatAsString();
} }
return topLeft.formatAsString() + ":" + botRight.formatAsString();
}
public String toFormulaString() {
return formatReferenceAsString();
}
public byte getDefaultOperandClass() {
return Ptg.CLASS_REF;
}
} }

View File

@ -101,13 +101,27 @@ public final class SheetNameFormatter {
return true; 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; 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 * @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 * 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) { /* package */ static boolean isSpecialChar(char ch) {
// note - Character.isJavaIdentifierPart() would allow dollars '$' // note - Character.isJavaIdentifierPart() would allow dollars '$'
@ -115,7 +129,8 @@ public final class SheetNameFormatter {
return false; return false;
} }
switch(ch) { switch(ch) {
case '_': // underscore is ok case '.': // dot is OK
case '_': // underscore is OK
return false; return false;
case '\n': case '\n':
case '\r': case '\r':

View File

@ -90,7 +90,7 @@ public final class AreaReference {
for(int i=refPart.length()-1; i>=0; i--) { for(int i=refPart.length()-1; i>=0; i--) {
int ch = refPart.charAt(i); int ch = refPart.charAt(i);
if (ch == '$' && i==0) { if (ch == '$' && i==0) {
continue; continue;
} }
if (ch < 'A' || ch > 'Z') { if (ch < 'A' || ch > 'Z') {
return false; return false;
@ -101,10 +101,48 @@ public final class AreaReference {
/** /**
* Creates an area ref from a pair of Cell References. * Creates an area ref from a pair of Cell References.
* Also normalises such that the top-left
*/ */
public AreaReference(CellReference topLeft, CellReference botRight) { public AreaReference(CellReference topLeft, CellReference botRight) {
_firstCell = topLeft; boolean swapRows = topLeft.getRow() > botRight.getRow();
_lastCell = botRight; 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; _isSingleCell = false;
} }

View File

@ -34,6 +34,7 @@ public final class CellReference {
public static final class NameType { public static final class NameType {
public static final int CELL = 1; public static final int CELL = 1;
public static final int NAMED_RANGE = 2; public static final int NAMED_RANGE = 2;
public static final int COLUMN = 3;
public static final int BAD_CELL_OR_NAMED_RANGE = -1; public static final int BAD_CELL_OR_NAMED_RANGE = -1;
} }
/** The character ($) that signifies a row or column value is absolute instead of relative */ /** The character ($) that signifies a row or column value is absolute instead of relative */
@ -44,10 +45,16 @@ public final class CellReference {
private static final char SPECIAL_NAME_DELIMITER = '\''; 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 * Matches a run of one or more letters followed by a run of one or more digits.
* run of digits is group 2. Each group may optionally be prefixed with a single '$'. * 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]+)"); 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 * Named range names must start with a letter or underscore. Subsequent characters may include
* digits or dot. (They can even end in dot). * digits or dot. (They can even end in dot).
@ -203,7 +210,7 @@ public final class CellReference {
// named range name // named range name
// This behaviour is a little weird. For example, "IW123" 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 // 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. // dependent. In BIFF12, "IW123" is not a valid named range name, but in BIFF8 it is.
if (str.indexOf(ABSOLUTE_REFERENCE_MARKER) >= 0) { if (str.indexOf(ABSOLUTE_REFERENCE_MARKER) >= 0) {
// Of course, named range names cannot have '$' // Of course, named range names cannot have '$'
return NameType.BAD_CELL_OR_NAMED_RANGE; return NameType.BAD_CELL_OR_NAMED_RANGE;
@ -212,11 +219,17 @@ public final class CellReference {
} }
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()) { if (!NAMED_RANGE_NAME_PATTERN.matcher(str).matches()) {
return NameType.BAD_CELL_OR_NAMED_RANGE; return NameType.BAD_CELL_OR_NAMED_RANGE;
} }
return NameType.NAMED_RANGE; return NameType.NAMED_RANGE;
} }
@ -257,23 +270,13 @@ public final class CellReference {
* @return <code>true</code> if the row and col parameters are within range of a BIFF8 spreadsheet. * @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) { public static boolean cellReferenceIsWithinRange(String colStr, String rowStr) {
int numberOfLetters = colStr.length(); if (!isColumnWithnRange(colStr)) {
if(numberOfLetters > BIFF8_LAST_COLUMN_TEXT_LEN) { return false;
// "Sheet1" case etc
return false; // that was easy
} }
int nDigits = rowStr.length(); int nDigits = rowStr.length();
if(nDigits > BIFF8_LAST_ROW_TEXT_LEN) { if(nDigits > BIFF8_LAST_ROW_TEXT_LEN) {
return false; 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) { if(nDigits == BIFF8_LAST_ROW_TEXT_LEN) {
// ASCII comparison is valid if digit count is same // ASCII comparison is valid if digit count is same
@ -288,6 +291,23 @@ public final class CellReference {
return true; 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 * 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 * is the sheet name. Only the first element may be null. The second element in is the column

View File

@ -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.PercentPtg;
import org.apache.poi.hssf.record.formula.PowerPtg; import org.apache.poi.hssf.record.formula.PowerPtg;
import org.apache.poi.hssf.record.formula.Ptg; 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.Ref3DPtg;
import org.apache.poi.hssf.record.formula.RefPtg; import org.apache.poi.hssf.record.formula.RefPtg;
import org.apache.poi.hssf.record.formula.StringPtg; import org.apache.poi.hssf.record.formula.StringPtg;
@ -81,6 +82,33 @@ import org.apache.poi.hssf.util.CellReference.NameType;
* @author Josh Micich * @author Josh Micich
*/ */
public final class FormulaParser { 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/> * Specific exception thrown when a supplied formula does not parse properly.<br/>
@ -176,23 +204,23 @@ public final class FormulaParser {
} }
/** Recognize an Alpha Character */ /** Recognize an Alpha Character */
private boolean IsAlpha(char c) { private static boolean IsAlpha(char c) {
return Character.isLetter(c) || c == '$' || c=='_'; return Character.isLetter(c) || c == '$' || c=='_';
} }
/** Recognize a Decimal Digit */ /** Recognize a Decimal Digit */
private boolean IsDigit(char c) { private static boolean IsDigit(char c) {
return Character.isDigit(c); return Character.isDigit(c);
} }
/** Recognize an Alphanumeric */ /** Recognize an Alphanumeric */
private boolean IsAlNum(char c) { private static boolean IsAlNum(char c) {
return (IsAlpha(c) || IsDigit(c)); return IsAlpha(c) || IsDigit(c);
} }
/** Recognize White Space */ /** Recognize White Space */
private boolean IsWhite( char c) { private static boolean IsWhite( char c) {
return (c ==' ' || c== TAB); return c ==' ' || c== TAB;
} }
/** Skip Over Leading White Space */ /** Skip Over Leading White Space */
@ -213,7 +241,13 @@ public final class FormulaParser {
} }
GetChar(); 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/> * 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 * 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 * may return a value like "A1..B2", in which case the caller must convert it to
* an area reference like "A1:B2" * an area reference like "A1:B2"
*/ */
private String parseIdentifier() { private Identifier parseIdentifier() {
StringBuffer Token = new StringBuffer(); StringBuffer sb = new StringBuffer();
if (!IsAlpha(look) && look != '\'') { if (!IsAlpha(look) && look != '\'') {
throw expected("Name"); throw expected("Name");
} }
if(look == '\'') boolean isQuoted = look == '\'';
{ if(isQuoted) {
Match('\''); Match('\'');
boolean done = look == '\''; boolean done = look == '\'';
while(!done) while(!done) {
{ sb.append(look);
Token.append(look);
GetChar(); GetChar();
if(look == '\'') if(look == '\'')
{ {
@ -240,17 +273,15 @@ public final class FormulaParser {
done = look != '\''; done = look != '\'';
} }
} }
} } else {
else
{
// allow for any sequence of dots and identifier chars // allow for any sequence of dots and identifier chars
// special case of two consecutive dots is best treated in the calling code // special case of two consecutive dots is best treated in the calling code
while (IsAlNum(look) || look == '.') { while (IsAlNum(look) || look == '.') {
Token.append(look); sb.append(look);
GetChar(); GetChar();
} }
} }
return Token.toString(); return new Identifier(sb.toString(), isQuoted);
} }
/** Get a Number */ /** Get a Number */
@ -265,72 +296,112 @@ public final class FormulaParser {
} }
private ParseNode parseFunctionReferenceOrName() { private ParseNode parseFunctionReferenceOrName() {
String name = parseIdentifier(); Identifier iden = parseIdentifier();
if (look == '('){ if (look == '('){
//This is a function //This is a function
return function(name); return function(iden.getName());
} }
return new ParseNode(parseNameOrReference(name)); if (!iden.isQuoted()) {
String name = iden.getName();
if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) {
return new ParseNode(new BoolPtg(name.toUpperCase()));
}
}
return parseRangeExpression(iden);
} }
private Ptg parseNameOrReference(String name) { private ParseNode parseRangeExpression(Identifier iden) {
Ptg ptgA = parseNameOrCellRef(iden);
if (look == ':') {
GetChar();
Identifier iden2 = parseIdentifier();
Ptg ptgB = parseNameOrCellRef(iden2);
Ptg simplified = reduceRangeExpression(ptgA, ptgB);
if (simplified == null) {
ParseNode[] children = {
new ParseNode(ptgA),
new ParseNode(ptgB),
};
return new ParseNode(RangePtg.instance, children);
}
return new ParseNode(simplified);
}
return new ParseNode(ptgA);
}
/**
*
* "A1", "B3" -> "A1:B3"
* "sheet1!A1", "B3" -> "sheet1!A1:B3"
*
* @return <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;
if (ptgA instanceof RefPtg) {
RefPtg refA = (RefPtg) ptgA;
return new AreaPtg(refA.getRow(), refB.getRow(), refA.getColumn(), refB.getColumn(),
refA.isRowRelative(), refB.isRowRelative(), refA.isColRelative(), refB.isColRelative());
}
if (ptgA instanceof Ref3DPtg) {
Ref3DPtg refA = (Ref3DPtg) ptgA;
return new Area3DPtg(refA.getRow(), refB.getRow(), refA.getColumn(), refB.getColumn(),
refA.isRowRelative(), refB.isRowRelative(), refA.isColRelative(), refB.isColRelative(),
refA.getExternSheetIndex());
}
// Note - other operand types (like AreaPtg) which probably can't evaluate
// do not cause validation errors at parse time
return null;
}
private Ptg parseNameOrCellRef(Identifier iden) {
if (look == '!') {
GetChar();
// 3-D ref
// this code assumes iden is a sheetName
// TODO - handle <book name> ! <named range name>
int externIdx = book.getExternalSheetIndex(iden.getName());
String secondIden = parseUnquotedIdentifier();
AreaReference areaRef = parseArea(secondIden);
if (areaRef == null) {
return new Ref3DPtg(secondIden, externIdx);
}
// will happen if dots are used instead of colon
return new Area3DPtg(areaRef.formatAsString(), externIdx);
}
String name = iden.getName();
AreaReference areaRef = parseArea(name); AreaReference areaRef = parseArea(name);
if (areaRef != null) { if (areaRef != null) {
// will happen if dots are used instead of colon // will happen if dots are used instead of colon
return new AreaPtg(areaRef.formatAsString()); return new AreaPtg(areaRef.formatAsString());
} }
if (look == ':' || look == '.') { // this is a AreaReference
GetChar();
while (look == '.') { // formulas can have . or .. or ... instead of :
GetChar();
}
String first = name;
String second = parseIdentifier();
return new AreaPtg(first+":"+second);
}
if (look == '!') {
Match('!');
String sheetName = name;
String first = parseIdentifier();
int externIdx = book.getExternalSheetIndex(sheetName);
areaRef = parseArea(name);
if (areaRef != null) {
// will happen if dots are used instead of colon
return new Area3DPtg(areaRef.formatAsString(), externIdx);
}
if (look == ':') {
Match(':');
String second=parseIdentifier();
if (look == '!') {
//The sheet name was included in both of the areas. Only really
//need it once
Match('!');
String third=parseIdentifier();
if (!sheetName.equals(second))
throw new RuntimeException("Unhandled double sheet reference.");
return new Area3DPtg(first+":"+third,externIdx);
}
return new Area3DPtg(first+":"+second,externIdx);
}
return new Ref3DPtg(first, externIdx);
}
if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) {
return new BoolPtg(name.toUpperCase());
}
// This can be either a cell ref or a named range // This can be either a cell ref or a named range
// Try to spot which it is
int nameType = CellReference.classifyCellReference(name); int nameType = CellReference.classifyCellReference(name);
if (nameType == NameType.CELL) { if (nameType == NameType.CELL) {
return new RefPtg(name); 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) { if (nameType != NameType.NAMED_RANGE) {
new FormulaParseException("Name '" + name new FormulaParseException("Name '" + name
+ "' does not look like a cell reference or named range"); + "' does not look like a cell reference or named range");
@ -662,7 +733,7 @@ public final class FormulaParser {
} }
private Boolean parseBooleanLiteral() { private Boolean parseBooleanLiteral() {
String iden = parseIdentifier(); String iden = parseUnquotedIdentifier();
if ("TRUE".equalsIgnoreCase(iden)) { if ("TRUE".equalsIgnoreCase(iden)) {
return Boolean.TRUE; return Boolean.TRUE;
} }
@ -720,7 +791,7 @@ public final class FormulaParser {
private int parseErrorLiteral() { private int parseErrorLiteral() {
Match('#'); Match('#');
String part1 = parseIdentifier().toUpperCase(); String part1 = parseUnquotedIdentifier().toUpperCase();
switch(part1.charAt(0)) { switch(part1.charAt(0)) {
case 'V': case 'V':

View File

@ -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.AbstractFunctionPtg;
import org.apache.poi.hssf.record.formula.ControlPtg; 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.ValueOperatorPtg;
import org.apache.poi.hssf.record.formula.Ptg; import org.apache.poi.hssf.record.formula.Ptg;
@ -115,6 +116,10 @@ final class OperandClassTransformer {
return; return;
} }
if (children.length > 0) { 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"); throw new IllegalStateException("Node should not have any children");
} }

View File

@ -880,6 +880,37 @@ public final class TestFormulaParser extends TestCase {
Object[][] values = aptg.getTokenArrayValues(); Object[][] values = aptg.getTokenArrayValues();
assertEquals(ErrorConstant.valueOf(HSSFErrorConstants.ERROR_REF), values[0][3]); assertEquals(ErrorConstant.valueOf(HSSFErrorConstants.ERROR_REF), values[0][3]);
assertEquals(Boolean.FALSE, values[1][0]); assertEquals(Boolean.FALSE, values[1][0]);
}
public void testRangeOperator() {
HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet sheet = wb.createSheet();
HSSFCell cell = sheet.createRow(0).createCell(0);
wb.setSheetName(0, "Sheet1");
cell.setCellFormula("Sheet1!B$4:Sheet1!$C1"); // explicit range ':' operator
assertEquals("Sheet1!B$4:Sheet1!$C1", cell.getCellFormula());
cell.setCellFormula("Sheet1!B$4:$C1"); // plain area ref
assertEquals("Sheet1!B1:$C$4", cell.getCellFormula()); // note - area ref is normalised
cell.setCellFormula("Sheet1!$C1...B$4"); // different syntax for plain area ref
assertEquals("Sheet1!B1:$C$4", cell.getCellFormula());
// with funny sheet name
wb.setSheetName(0, "A1...A2");
cell.setCellFormula("A1...A2!B1");
assertEquals("A1...A2!B1", cell.getCellFormula());
}
public void testBooleanNamedSheet() {
HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet sheet = wb.createSheet("true");
HSSFCell cell = sheet.createRow(0).createCell(0);
cell.setCellFormula("'true'!B2");
assertEquals("'true'!B2", cell.getCellFormula());
} }
} }

View File

@ -20,16 +20,12 @@ package org.apache.poi.hssf.record.formula;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
* Tests for SheetNameFormatter * Tests for {@link SheetNameFormatter}
* *
* @author Josh Micich * @author Josh Micich
*/ */
public final class TestSheetNameFormatter extends TestCase { public final class TestSheetNameFormatter extends TestCase {
public TestSheetNameFormatter(String testName) {
super(testName);
}
private static void confirmFormat(String rawSheetName, String expectedSheetNameEncoding) { private static void confirmFormat(String rawSheetName, String expectedSheetNameEncoding) {
assertEquals(expectedSheetNameEncoding, SheetNameFormatter.format(rawSheetName)); assertEquals(expectedSheetNameEncoding, SheetNameFormatter.format(rawSheetName));
} }
@ -55,6 +51,16 @@ public final class TestSheetNameFormatter extends TestCase {
confirmFormat("TAXRETURN19980415", "TAXRETURN19980415"); confirmFormat("TAXRETURN19980415", "TAXRETURN19980415");
} }
public void testBooleanLiterals() {
confirmFormat("TRUE", "'TRUE'");
confirmFormat("FALSE", "'FALSE'");
confirmFormat("True", "'True'");
confirmFormat("fAlse", "'fAlse'");
confirmFormat("Yes", "Yes");
confirmFormat("No", "No");
}
private static void confirmCellNameMatch(String rawSheetName, boolean expected) { private static void confirmCellNameMatch(String rawSheetName, boolean expected) {
assertEquals(expected, SheetNameFormatter.nameLooksLikePlainCellReference(rawSheetName)); assertEquals(expected, SheetNameFormatter.nameLooksLikePlainCellReference(rawSheetName));
} }

View File

@ -146,7 +146,7 @@ public final class TestFormulaBugs extends TestCase {
throw new AssertionFailedError("Identified bug 42448"); throw new AssertionFailedError("Identified bug 42448");
} }
assertEquals("SUMPRODUCT(A!C7:C67,B8:B68)/B69", cell.getCellFormula()); assertEquals("SUMPRODUCT(A!C7:A!C67,B8:B68)/B69", cell.getCellFormula());
// might as well evaluate the sucker... // might as well evaluate the sucker...

View File

@ -1064,7 +1064,7 @@ public final class TestFormulas extends TestCase {
/** Unknown Ptg 3D*/ /** Unknown Ptg 3D*/
public void test27272_2() throws Exception { public void test27272_2() throws Exception {
HSSFWorkbook wb = openSample("27272_2.xls"); HSSFWorkbook wb = openSample("27272_2.xls");
assertEquals("Reference for named range ", "'LOAD.POD_HISTORIES'!#REF!",wb.getNameAt(0).getReference()); assertEquals("LOAD.POD_HISTORIES!#REF!", wb.getNameAt(0).getReference());
File outF = File.createTempFile("bug27272_2",".xls"); File outF = File.createTempFile("bug27272_2",".xls");
wb.write(new FileOutputStream(outF)); wb.write(new FileOutputStream(outF));
System.out.println("Open "+outF.getAbsolutePath()+" in Excel"); System.out.println("Open "+outF.getAbsolutePath()+" in Excel");