From 527b4c7fc5265804b855dfaabc13a3c41bcba001 Mon Sep 17 00:00:00 2001
From: Josh Micich
- * Primarily used by test cases when testing for specific parsing exceptions.
-1
means that
- * the scope of the name will be ignored and the parser will match names only by name
- *
- * @return array of parsed tokens
- * @throws FormulaParseException if the formula is unparsable
- */
- public static Ptg[] parse(String formula, FormulaParsingWorkbook workbook, int formulaType, int sheetIndex) {
- FormulaParser fp = new FormulaParser(formula, workbook, sheetIndex);
- fp.parse();
- return fp.getRPNPtg(formulaType);
- }
+ /**
+ * Lookahead Character.
+ * gets value '\0' when the input string is exhausted
+ */
+ private char look;
- /** Read New Character From Input Stream */
- private void GetChar() {
- // Check to see if we've walked off the end of the string.
- if (_pointer > _formulaLength) {
- throw new RuntimeException("too far");
- }
- if (_pointer < _formulaLength) {
- look=_formulaString.charAt(_pointer);
- } else {
- // Just return if so and reset 'look' to something to keep
- // SkipWhitespace from spinning
- look = (char)0;
- }
- _pointer++;
- //System.out.println("Got char: "+ look);
- }
+ private FormulaParsingWorkbook _book;
- /** Report What Was Expected */
- private RuntimeException expected(String s) {
- String msg;
-
- if (look == '=' && _formulaString.substring(0, _pointer-1).trim().length() < 1) {
- msg = "The specified formula '" + _formulaString
- + "' starts with an equals sign which is not allowed.";
- } else {
- msg = "Parse error near char " + (_pointer-1) + " '" + look + "'"
- + " in specified formula '" + _formulaString + "'. Expected "
- + s;
- }
- return new FormulaParseException(msg);
- }
-
- /** Recognize an Alpha Character */
- private static boolean IsAlpha(char c) {
- return Character.isLetter(c) || c == '$' || c=='_';
- }
-
- /** Recognize a Decimal Digit */
- private static boolean IsDigit(char c) {
- return Character.isDigit(c);
- }
-
- /** Recognize an Alphanumeric */
- private static boolean IsAlNum(char c) {
- return IsAlpha(c) || IsDigit(c);
- }
-
- /** Recognize White Space */
- private static boolean IsWhite( char c) {
- return c ==' ' || c== TAB;
- }
-
- /** Skip Over Leading White Space */
- private void SkipWhite() {
- while (IsWhite(look)) {
- GetChar();
- }
- }
-
- /**
- * Consumes the next input character if it is equal to the one specified otherwise throws an
- * unchecked exception. This method does not consume whitespace (before or after the
- * matched character).
- */
- private void Match(char x) {
- if (look != x) {
- throw expected("'" + x + "'");
- }
- 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.null
if the range expression cannot / shouldn't be reduced.
- */
- private static Ptg reduceRangeExpression(Ptg ptgA, Ptg ptgB) {
- if (!(ptgB instanceof RefPtg)) {
- // only when second ref is simple 2-D ref can the range
- // expression be converted to an area ref
- return null;
- }
- RefPtg refB = (RefPtg) ptgB;
-
- if (ptgA instanceof RefPtg) {
- RefPtg refA = (RefPtg) ptgA;
- return new AreaPtg(refA.getRow(), refB.getRow(), refA.getColumn(), refB.getColumn(),
- refA.isRowRelative(), refB.isRowRelative(), refA.isColRelative(), refB.isColRelative());
- }
- if (ptgA instanceof Ref3DPtg) {
- Ref3DPtg refA = (Ref3DPtg) ptgA;
- return new Area3DPtg(refA.getRow(), refB.getRow(), refA.getColumn(), refB.getColumn(),
- refA.isRowRelative(), refB.isRowRelative(), refA.isColRelative(), refB.isColRelative(),
- refA.getExternSheetIndex());
- }
- // Note - other operand types (like AreaPtg) which probably can't evaluate
- // do not cause validation errors at parse time
- return null;
- }
-
- private Ptg parseNameOrCellRef(Identifier iden) {
-
- if (look == '!') {
- GetChar();
- // 3-D ref
- // this code assumes iden is a sheetName
- // TODO - handle -1
means that
+ * the scope of the name will be ignored and the parser will match names only by name
+ *
+ * @return array of parsed tokens
+ * @throws FormulaParseException if the formula is unparsable
+ */
+ public static Ptg[] parse(String formula, FormulaParsingWorkbook workbook, int formulaType, int sheetIndex) {
+ FormulaParser fp = new FormulaParser(formula, workbook, sheetIndex);
+ fp.parse();
+ return fp.getRPNPtg(formulaType);
+ }
- /**
- * @return true
if the specified name is a valid cell reference
- */
- private static boolean isValidCellReference(String str) {
- return CellReference.classifyCellReference(str) == NameType.CELL;
- }
+ /** Read New Character From Input Stream */
+ private void GetChar() {
+ // Check to see if we've walked off the end of the string.
+ if (_pointer > _formulaLength) {
+ throw new RuntimeException("too far");
+ }
+ if (_pointer < _formulaLength) {
+ look=_formulaString.charAt(_pointer);
+ } else {
+ // Just return if so and reset 'look' to something to keep
+ // SkipWhitespace from spinning
+ look = (char)0;
+ }
+ _pointer++;
+ //System.out.println("Got char: "+ look);
+ }
+ private void resetPointer(int ptr) {
+ _pointer = ptr;
+ if (_pointer <= _formulaLength) {
+ look=_formulaString.charAt(_pointer-1);
+ } else {
+ // Just return if so and reset 'look' to something to keep
+ // SkipWhitespace from spinning
+ look = (char)0;
+ }
+ }
+
+ /** Report What Was Expected */
+ private RuntimeException expected(String s) {
+ String msg;
+
+ if (look == '=' && _formulaString.substring(0, _pointer-1).trim().length() < 1) {
+ msg = "The specified formula '" + _formulaString
+ + "' starts with an equals sign which is not allowed.";
+ } else {
+ msg = "Parse error near char " + (_pointer-1) + " '" + look + "'"
+ + " in specified formula '" + _formulaString + "'. Expected "
+ + s;
+ }
+ return new FormulaParseException(msg);
+ }
+
+ /** Recognize an Alpha Character */
+ private static boolean IsAlpha(char c) {
+ return Character.isLetter(c) || c == '$' || c=='_';
+ }
+
+ /** Recognize a Decimal Digit */
+ private static boolean IsDigit(char c) {
+ return Character.isDigit(c);
+ }
+
+ /** Recognize White Space */
+ private static boolean IsWhite( char c) {
+ return c ==' ' || c== TAB;
+ }
+
+ /** Skip Over Leading White Space */
+ private void SkipWhite() {
+ while (IsWhite(look)) {
+ GetChar();
+ }
+ }
+
+ /**
+ * Consumes the next input character if it is equal to the one specified otherwise throws an
+ * unchecked exception. This method does not consume whitespace (before or after the
+ * matched character).
+ */
+ private void Match(char x) {
+ if (look != x) {
+ throw expected("'" + x + "'");
+ }
+ GetChar();
+ }
+
+ /** Get a Number */
+ private String GetNum() {
+ StringBuffer value = new StringBuffer();
+
+ while (IsDigit(this.look)){
+ value.append(this.look);
+ GetChar();
+ }
+ return value.length() == 0 ? null : value.toString();
+ }
+
+ private ParseNode parseRangeExpression() {
+ ParseNode result = parseRangeable();
+ boolean hasRange = false;
+ while (look == ':') {
+ int pos = _pointer;
+ GetChar();
+ ParseNode nextPart = parseRangeable();
+ // Note - no range simplification here. An expr like "A1:B2:C3:D4:E5" should be
+ // grouped into area ref pairs like: "(A1:B2):(C3:D4):E5"
+ // Furthermore, Excel doesn't seem to simplify
+ // expressions like "Sheet1!A1:Sheet1:B2" into "Sheet1!A1:B2"
+
+ checkValidRangeOperand("LHS", pos, result);
+ checkValidRangeOperand("RHS", pos, nextPart);
+
+ ParseNode[] children = { result, nextPart, };
+ result = new ParseNode(RangePtg.instance, children);
+ hasRange = true;
+ }
+ if (hasRange) {
+ return augmentWithMemPtg(result);
+ }
+ return result;
+ }
+
+ private static ParseNode augmentWithMemPtg(ParseNode root) {
+ Ptg memPtg;
+ if (needsMemFunc(root)) {
+ memPtg = new MemFuncPtg(root.getEncodedSize());
+ } else {
+ memPtg = new MemAreaPtg(root.getEncodedSize());
+ }
+ return new ParseNode(memPtg, root);
+ }
+ /**
+ * From OOO doc: "Whenever one operand of the reference subexpression is a function,
+ * a defined name, a 3D reference, or an external reference (and no error occurs),
+ * a tMemFunc token is used"
+ *
+ */
+ private static boolean needsMemFunc(ParseNode root) {
+ Ptg token = root.getToken();
+ if (token instanceof AbstractFunctionPtg) {
+ return true;
+ }
+ if (token instanceof ExternSheetReferenceToken) { // 3D refs
+ return true;
+ }
+ if (token instanceof NamePtg || token instanceof NameXPtg) { // 3D refs
+ return true;
+ }
+
+ if (token instanceof OperationPtg || token instanceof ParenthesisPtg) {
+ // expect RangePtg, but perhaps also UnionPtg, IntersectionPtg etc
+ for(ParseNode child : root.getChildren()) {
+ if (needsMemFunc(child)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ if (token instanceof OperandPtg) {
+ return false;
+ }
+ if (token instanceof OperationPtg) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param currentParsePosition used to format a potential error message
+ */
+ private static void checkValidRangeOperand(String sideName, int currentParsePosition, ParseNode pn) {
+ if (!isValidRangeOperand(pn)) {
+ throw new FormulaParseException("The " + sideName
+ + " of the range operator ':' at position "
+ + currentParsePosition + " is not a proper reference.");
+ }
+ }
+
+ /**
+ * @return false
if sub-expression represented the specified ParseNode definitely
+ * cannot appear on either side of the range (':') operator
+ */
+ private static boolean isValidRangeOperand(ParseNode a) {
+ Ptg tkn = a.getToken();
+ // Note - order is important for these instance-of checks
+ if (tkn instanceof OperandPtg) {
+ // notably cell refs and area refs
+ return true;
+ }
+
+ // next 2 are special cases of OperationPtg
+ if (tkn instanceof AbstractFunctionPtg) {
+ AbstractFunctionPtg afp = (AbstractFunctionPtg) tkn;
+ byte returnClass = afp.getDefaultOperandClass();
+ return Ptg.CLASS_REF == returnClass;
+ }
+ if (tkn instanceof ValueOperatorPtg) {
+ return false;
+ }
+ if (tkn instanceof OperationPtg) {
+ return true;
+ }
+
+ // one special case of ControlPtg
+ if (tkn instanceof ParenthesisPtg) {
+ // parenthesis Ptg should have only one child
+ return isValidRangeOperand(a.getChildren()[0]);
+ }
+
+ // one special case of ScalarConstantPtg
+ if (tkn == ErrPtg.REF_INVALID) {
+ return true;
+ }
+
+ // All other ControlPtgs and ScalarConstantPtgs cannot be used with ':'
+ return false;
+ }
+
+ /**
+ * Parses area refs (things which could be the operand of ':') and simple factors
+ * Examples
+ * + * A$1 + * $A$1 : $B1 + * A1 ....... C2 + * Sheet1 !$A1 + * a..b!A1 + * 'my sheet'!A1 + * .my.sheet!A1 + * my.named..range. + * foo.bar(123.456, "abc") + * 123.456 + * "abc" + * true + *+ * + */ + private ParseNode parseRangeable() { + SkipWhite(); + int savePointer = _pointer; + SheetIdentifier sheetIden = parseSheetName(); + if (sheetIden == null) { + resetPointer(savePointer); + } else { + SkipWhite(); + savePointer = _pointer; + } + + SimpleRangePart part1 = parseSimpleRangePart(); + if (part1 == null) { + if (sheetIden != null) { + throw new FormulaParseException("Cell reference expected after sheet name at index " + + _pointer + "."); + } + return parseNonRange(savePointer); + } + boolean whiteAfterPart1 = IsWhite(look); + if (whiteAfterPart1) { + SkipWhite(); + } + + if (look == ':') { + int colonPos = _pointer; + GetChar(); + SkipWhite(); + SimpleRangePart part2 = parseSimpleRangePart(); + if (part2 != null && !part1.isCompatibleForArea(part2)) { + // second part is not compatible with an area ref e.g. S!A1:S!B2 + // where S might be a sheet name (that looks like a column name) + + part2 = null; + } + if (part2 == null) { + // second part is not compatible with an area ref e.g. A1:OFFSET(B2, 1, 2) + // reset and let caller use explicit range operator + resetPointer(colonPos); + if (!part1.isCell()) { + String prefix; + if (sheetIden == null) { + prefix = ""; + } else { + prefix = "'" + sheetIden.getSheetIdentifier().getName() + '!'; + } + throw new FormulaParseException(prefix + part1.getRep() + "' is not a proper reference."); + } + return createAreaRefParseNode(sheetIden, part1, part2); + } + return createAreaRefParseNode(sheetIden, part1, part2); + } + + if (look == '.') { + GetChar(); + int dotCount = 1; + while (look =='.') { + dotCount ++; + GetChar(); + } + boolean whiteBeforePart2 = IsWhite(look); + + SkipWhite(); + SimpleRangePart part2 = parseSimpleRangePart(); + String part1And2 = _formulaString.substring(savePointer-1, _pointer-1); + if (part2 == null) { + if (sheetIden != null) { + throw new FormulaParseException("Complete area reference expected after sheet name at index " + + _pointer + "."); + } + return parseNonRange(savePointer); + } - /** - * Note - Excel function names are 'case aware but not case sensitive'. This method may end - * up creating a defined name record in the workbook if the specified name is not an internal - * Excel function, and has not been encountered before. - * - * @param name case preserved function name (as it was entered/appeared in the formula). - */ - private ParseNode function(String name) { - Ptg nameToken = null; - if(!AbstractFunctionPtg.isBuiltInFunctionName(name)) { - // user defined function - // in the token tree, the name is more or less the first argument + if (whiteAfterPart1 || whiteBeforePart2) { + if (part1.isRowOrColumn() || part2.isRowOrColumn()) { + // "A .. B" not valid syntax for "A:B" + // and there's no other valid expression that fits this grammar + throw new FormulaParseException("Dotted range (full row or column) expression '" + + part1And2 + "' must not contain whitespace."); + } + return createAreaRefParseNode(sheetIden, part1, part2); + } - EvaluationName hName = _book.getName(name, _sheetIndex); - if (hName == null) { + if (dotCount == 1 && part1.isRow() && part2.isRow()) { + // actually, this is looking more like a number + return parseNonRange(savePointer); + } - nameToken = _book.getNameXPtg(name); - if (nameToken == null) { - throw new FormulaParseException("Name '" + name - + "' is completely unknown in the current workbook"); - } - } else { - if (!hName.isFunctionName()) { - throw new FormulaParseException("Attempt to use name '" + name - + "' as a function, but defined name in workbook does not refer to a function"); - } + if (part1.isRowOrColumn() || part2.isRowOrColumn()) { + if (dotCount != 2) { + throw new FormulaParseException("Dotted range (full row or column) expression '" + part1And2 + + "' must have exactly 2 dots."); + } + } + return createAreaRefParseNode(sheetIden, part1, part2); + } + if (part1.isCell() && isValidCellReference(part1.getRep())) { + return createAreaRefParseNode(sheetIden, part1, null); + } + if (sheetIden != null) { + throw new FormulaParseException("Second part of cell reference expected after sheet name at index " + + _pointer + "."); + } - // calls to user-defined functions within the workbook - // get a Name token which points to a defined name record - nameToken = hName.createPtg(); - } - } + return parseNonRange(savePointer); + } - Match('('); - ParseNode[] args = Arguments(); - Match(')'); - return getFunction(name, nameToken, args); - } - /** - * Generates the variable function ptg for the formula. - *
- * For IF Formulas, additional PTGs are added to the tokens
- * @param name a {@link NamePtg} or {@link NameXPtg} or null
- * @param numArgs
- * @return Ptg a null is returned if we're in an IF formula, it needs extreme manipulation and is handled in this function
- */
- private ParseNode getFunction(String name, Ptg namePtg, ParseNode[] args) {
+ /**
+ * Parses simple factors that are not primitive ranges or range components
+ * i.e. '!', ':'(and equiv '...') do not appear
+ * Examples
+ *
+ * my.named...range. + * foo.bar(123.456, "abc") + * 123.456 + * "abc" + * true + *+ */ + private ParseNode parseNonRange(int savePointer) { + resetPointer(savePointer); - FunctionMetadata fm = FunctionMetadataRegistry.getFunctionByName(name.toUpperCase()); - int numArgs = args.length; - if(fm == null) { - if (namePtg == null) { - throw new IllegalStateException("NamePtg must be supplied for external functions"); - } - // must be external function - ParseNode[] allArgs = new ParseNode[numArgs+1]; - allArgs[0] = new ParseNode(namePtg); - System.arraycopy(args, 0, allArgs, 1, numArgs); - return new ParseNode(new FuncVarPtg(name, (byte)(numArgs+1)), allArgs); - } + if (Character.isDigit(look)) { + return new ParseNode(parseNumber()); + } + if (look == '"') { + return new ParseNode(new StringPtg(parseStringLiteral())); + } + // from now on we can only be dealing with non-quoted identifiers + // which will either be named ranges or functions + StringBuilder sb = new StringBuilder(); - if (namePtg != null) { - throw new IllegalStateException("NamePtg no applicable to internal functions"); - } - boolean isVarArgs = !fm.hasFixedArgsLength(); - int funcIx = fm.getIndex(); - if (funcIx == FunctionMetadataRegistry.FUNCTION_INDEX_SUM && args.length == 1) { - // Excel encodes the sum of a single argument as tAttrSum - // POI does the same for consistency, but this is not critical - return new ParseNode(AttrPtg.getSumSingle(), args); - // The code below would encode tFuncVar(SUM) which seems to do no harm - } - validateNumArgs(args.length, fm); + if (!Character.isLetter(look)) { + throw expected("number, string, or defined name"); + } + while (isValidDefinedNameChar(look)) { + sb.append(look); + GetChar(); + } + SkipWhite(); + String name = sb.toString(); + if (look == '(') { + return function(name); + } + if (name.equalsIgnoreCase("TRUE") || name.equalsIgnoreCase("FALSE")) { + return new ParseNode(new BoolPtg(name.toUpperCase())); + } + if (_book == null) { + // Only test cases omit the book (expecting it not to be needed) + throw new IllegalStateException("Need book to evaluate name '" + name + "'"); + } + EvaluationName evalName = _book.getName(name, _sheetIndex); + if (evalName == null) { + throw new FormulaParseException("Specified named range '" + + name + "' does not exist in the current workbook."); + } + if (evalName.isRange()) { + return new ParseNode(evalName.createPtg()); + } + // TODO - what about NameX ? + throw new FormulaParseException("Specified name '" + + name + "' is not a range as expected."); + } - AbstractFunctionPtg retval; - if(isVarArgs) { - retval = new FuncVarPtg(name, (byte)numArgs); - } else { - retval = new FuncPtg(funcIx); - } - return new ParseNode(retval, args); - } + /** + * + * @return
true
if the specified character may be used in a defined name
+ */
+ private static boolean isValidDefinedNameChar(char ch) {
+ if (Character.isLetterOrDigit(ch)) {
+ return true;
+ }
+ switch (ch) {
+ case '.':
+ case '_':
+ case '?':
+ case '\\': // of all things
+ return true;
+ }
+ return false;
+ }
- private void validateNumArgs(int numArgs, FunctionMetadata fm) {
- if(numArgs < fm.getMinParams()) {
- String msg = "Too few arguments to function '" + fm.getName() + "'. ";
- if(fm.hasFixedArgsLength()) {
- msg += "Expected " + fm.getMinParams();
- } else {
- msg += "At least " + fm.getMinParams() + " were expected";
- }
- msg += " but got " + numArgs + ".";
- throw new FormulaParseException(msg);
- }
- if(numArgs > fm.getMaxParams()) {
- String msg = "Too many arguments to function '" + fm.getName() + "'. ";
- if(fm.hasFixedArgsLength()) {
- msg += "Expected " + fm.getMaxParams();
- } else {
- msg += "At most " + fm.getMaxParams() + " were expected";
- }
- msg += " but got " + numArgs + ".";
- throw new FormulaParseException(msg);
- }
- }
+ /**
+ *
+ * @param sheetIden may be null
+ * @param part1
+ * @param part2 may be null
+ */
+ private ParseNode createAreaRefParseNode(SheetIdentifier sheetIden, SimpleRangePart part1,
+ SimpleRangePart part2) throws FormulaParseException {
- private static boolean isArgumentDelimiter(char ch) {
- return ch == ',' || ch == ')';
- }
+ int extIx;
+ if (sheetIden == null) {
+ extIx = Integer.MIN_VALUE;
+ } else {
+ String sName = sheetIden.getSheetIdentifier().getName();
+ if (sheetIden.getBookName() == null) {
+ extIx = _book.getExternalSheetIndex(sName);
+ } else {
+ extIx = _book.getExternalSheetIndex(sheetIden.getBookName(), sName);
+ }
+ }
+ Ptg ptg;
+ if (part2 == null) {
+ CellReference cr = part1.getCellReference();
+ if (sheetIden == null) {
+ ptg = new RefPtg(cr);
+ } else {
+ ptg = new Ref3DPtg(cr, extIx);
+ }
+ } else {
+ AreaReference areaRef = createAreaRef(part1, part2);
- /** get arguments to a function */
- private ParseNode[] Arguments() {
- //average 2 args per function
- Listnull
(and leaves {@link #_pointer} unchanged if a proper range part does not parse out
+ */
+ private SimpleRangePart parseSimpleRangePart() {
+ int ptr = _pointer-1; // TODO avoid StringIndexOutOfBounds
+ boolean hasDigits = false;
+ boolean hasLetters = false;
+ while (ptr < _formulaLength) {
+ char ch = _formulaString.charAt(ptr);
+ if (Character.isDigit(ch)) {
+ hasDigits = true;
+ } else if (Character.isLetter(ch)) {
+ hasLetters = true;
+ } else if (ch =='$') {
+ //
+ } else {
+ break;
+ }
+ ptr++;
+ }
+ if (ptr <= _pointer-1) {
+ return null;
+ }
+ String rep = _formulaString.substring(_pointer-1, ptr);
+ if (!CELL_REF_PATTERN.matcher(rep).matches()) {
+ return null;
+ }
+ // Check range bounds against grid max
+ if (hasLetters && hasDigits) {
+ if (!isValidCellReference(rep)) {
+ return null;
+ }
+ } else if (hasLetters) {
+ if (!CellReference.isColumnWithnRange(rep.replace("$", ""))) {
+ return null;
+ }
+ } else if (hasDigits) {
+ int i;
+ try {
+ i = Integer.parseInt(rep.replace("$", ""));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ if (i<1 || i>65536) {
+ return null;
+ }
+ } else {
+ // just dollars ? can this happen?
+ return null;
+ }
+
+
+ resetPointer(ptr+1); // stepping forward
+ return new SimpleRangePart(rep, hasLetters, hasDigits);
+ }
+
+
+ /**
+ * A1, $A1, A$1, $A$1, A, 1
+ */
+ private static final class SimpleRangePart {
+ private enum Type {
+ CELL, ROW, COLUMN;
+
+ public static Type get(boolean hasLetters, boolean hasDigits) {
+ if (hasLetters) {
+ return hasDigits ? CELL : COLUMN;
+ }
+ if (!hasDigits) {
+ throw new IllegalArgumentException("must have either letters or numbers");
+ }
+ return ROW;
+ }
+ }
+
+ private final Type _type;
+ private final String _rep;
+
+ public SimpleRangePart(String rep, boolean hasLetters, boolean hasNumbers) {
+ _rep = rep;
+ _type = Type.get(hasLetters, hasNumbers);
+ }
+
+ public boolean isCell() {
+ return _type == Type.CELL;
+ }
+
+ public boolean isRowOrColumn() {
+ return _type != Type.CELL;
+ }
+
+ public CellReference getCellReference() {
+ if (_type != Type.CELL) {
+ throw new IllegalStateException("Not applicable to this type");
+ }
+ return new CellReference(_rep);
+ }
+
+ public boolean isColumn() {
+ return _type == Type.COLUMN;
+ }
+
+ public boolean isRow() {
+ return _type == Type.ROW;
+ }
+
+ public String getRep() {
+ return _rep;
+ }
+
+ /**
+ * @return true
if the two range parts can be combined in an
+ * {@link AreaPtg} ( Note - the explicit range operator (:) may still be valid
+ * when this method returns false
)
+ */
+ public boolean isCompatibleForArea(SimpleRangePart part2) {
+ return _type == part2._type;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(64);
+ sb.append(getClass().getName()).append(" [");
+ sb.append(_rep);
+ sb.append("]");
+ return sb.toString();
+ }
+ }
+
+ /**
+ * Note - caller should reset {@link #_pointer} upon null
result
+ * @param iden identifier prefix (if unquoted, it is terminated at first dot)
+ * @return The sheet name as an identifier null
if '!' is not found in the right place
+ */
+ private SheetIdentifier parseSheetName() {
+
+ String bookName;
+ if (look == '[') {
+ StringBuilder sb = new StringBuilder();
+ GetChar();
+ while (look != ']') {
+ sb.append(look);
+ GetChar();
+ }
+ GetChar();
+ bookName = sb.toString();
+ } else {
+ bookName = null;
+ }
+
+ if (look == '\'') {
+ StringBuffer sb = new StringBuffer();
+
+ Match('\'');
+ boolean done = look == '\'';
+ while(!done) {
+ sb.append(look);
+ GetChar();
+ if(look == '\'')
+ {
+ Match('\'');
+ done = look != '\'';
+ }
+ }
+
+ Identifier iden = new Identifier(sb.toString(), true);
+ // quoted identifier - can't concatenate anything more
+ SkipWhite();
+ if (look == '!') {
+ GetChar();
+ return new SheetIdentifier(bookName, iden);
+ }
+ return null;
+ }
+
+ // unquoted sheet names must start with underscore or a letter
+ if (look =='_' || Character.isLetter(look)) {
+ StringBuilder sb = new StringBuilder();
+ // can concatenate idens with dots
+ while (isUnquotedSheetNameChar(look)) {
+ sb.append(look);
+ GetChar();
+ }
+ SkipWhite();
+ if (look == '!') {
+ GetChar();
+ return new SheetIdentifier(bookName, new Identifier(sb.toString(), false));
+ }
+ return null;
+ }
+ return null;
+ }
+
+ /**
+ * very similar to {@link SheetNameFormatter#isSpecialChar(char)}
+ */
+ private static boolean isUnquotedSheetNameChar(char ch) {
+ if(Character.isLetterOrDigit(ch)) {
+ return true;
+ }
+ switch(ch) {
+ case '.': // dot is OK
+ case '_': // underscore is OK
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * @return true
if the specified name is a valid cell reference
+ */
+ private static boolean isValidCellReference(String str) {
+ return CellReference.classifyCellReference(str) == NameType.CELL;
+ }
+
+
+ /**
+ * Note - Excel function names are 'case aware but not case sensitive'. This method may end
+ * up creating a defined name record in the workbook if the specified name is not an internal
+ * Excel function, and has not been encountered before.
+ *
+ * @param name case preserved function name (as it was entered/appeared in the formula).
+ */
+ private ParseNode function(String name) {
+ Ptg nameToken = null;
+ if(!AbstractFunctionPtg.isBuiltInFunctionName(name)) {
+ // user defined function
+ // in the token tree, the name is more or less the first argument
+
+ if (_book == null) {
+ // Only test cases omit the book (expecting it not to be needed)
+ throw new IllegalStateException("Need book to evaluate name '" + name + "'");
+ }
+ EvaluationName hName = _book.getName(name, _sheetIndex);
+ if (hName == null) {
+
+ nameToken = _book.getNameXPtg(name);
+ if (nameToken == null) {
+ throw new FormulaParseException("Name '" + name
+ + "' is completely unknown in the current workbook");
+ }
+ } else {
+ if (!hName.isFunctionName()) {
+ throw new FormulaParseException("Attempt to use name '" + name
+ + "' as a function, but defined name in workbook does not refer to a function");
+ }
+
+ // calls to user-defined functions within the workbook
+ // get a Name token which points to a defined name record
+ nameToken = hName.createPtg();
+ }
+ }
+
+ Match('(');
+ ParseNode[] args = Arguments();
+ Match(')');
+
+ return getFunction(name, nameToken, args);
+ }
+
+ /**
+ * Generates the variable function ptg for the formula.
+ *
+ * For IF Formulas, additional PTGs are added to the tokens
+ * @param name a {@link NamePtg} or {@link NameXPtg} or null
+ * @param numArgs
+ * @return Ptg a null is returned if we're in an IF formula, it needs extreme manipulation and is handled in this function
+ */
+ private ParseNode getFunction(String name, Ptg namePtg, ParseNode[] args) {
+
+ FunctionMetadata fm = FunctionMetadataRegistry.getFunctionByName(name.toUpperCase());
+ int numArgs = args.length;
+ if(fm == null) {
+ if (namePtg == null) {
+ throw new IllegalStateException("NamePtg must be supplied for external functions");
+ }
+ // must be external function
+ ParseNode[] allArgs = new ParseNode[numArgs+1];
+ allArgs[0] = new ParseNode(namePtg);
+ System.arraycopy(args, 0, allArgs, 1, numArgs);
+ return new ParseNode(new FuncVarPtg(name, (byte)(numArgs+1)), allArgs);
+ }
+
+ if (namePtg != null) {
+ throw new IllegalStateException("NamePtg no applicable to internal functions");
+ }
+ boolean isVarArgs = !fm.hasFixedArgsLength();
+ int funcIx = fm.getIndex();
+ if (funcIx == FunctionMetadataRegistry.FUNCTION_INDEX_SUM && args.length == 1) {
+ // Excel encodes the sum of a single argument as tAttrSum
+ // POI does the same for consistency, but this is not critical
+ return new ParseNode(AttrPtg.getSumSingle(), args);
+ // The code below would encode tFuncVar(SUM) which seems to do no harm
+ }
+ validateNumArgs(args.length, fm);
+
+ AbstractFunctionPtg retval;
+ if(isVarArgs) {
+ retval = new FuncVarPtg(name, (byte)numArgs);
+ } else {
+ retval = new FuncPtg(funcIx);
+ }
+ return new ParseNode(retval, args);
+ }
+
+ private void validateNumArgs(int numArgs, FunctionMetadata fm) {
+ if(numArgs < fm.getMinParams()) {
+ String msg = "Too few arguments to function '" + fm.getName() + "'. ";
+ if(fm.hasFixedArgsLength()) {
+ msg += "Expected " + fm.getMinParams();
+ } else {
+ msg += "At least " + fm.getMinParams() + " were expected";
+ }
+ msg += " but got " + numArgs + ".";
+ throw new FormulaParseException(msg);
+ }
+ if(numArgs > fm.getMaxParams()) {
+ String msg = "Too many arguments to function '" + fm.getName() + "'. ";
+ if(fm.hasFixedArgsLength()) {
+ msg += "Expected " + fm.getMaxParams();
+ } else {
+ msg += "At most " + fm.getMaxParams() + " were expected";
+ }
+ msg += " but got " + numArgs + ".";
+ throw new FormulaParseException(msg);
+ }
+ }
+
+ private static boolean isArgumentDelimiter(char ch) {
+ return ch == ',' || ch == ')';
+ }
+
+ /** get arguments to a function */
+ private ParseNode[] Arguments() {
+ //average 2 args per function
+ List