/* ====================================================================
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
==================================================================== */
package org.apache.poi.hssf.model;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.regex.Pattern;
//import PTG's .. since we need everything, import *
import org.apache.poi.hssf.record.formula.*;
import org.apache.poi.hssf.record.formula.function.FunctionMetadata;
import org.apache.poi.hssf.record.formula.function.FunctionMetadataRegistry;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
/**
* This class parses a formula string into a List of tokens in RPN order.
* Inspired by
* Lets Build a Compiler, by Jack Crenshaw
* BNF for the formula expression is :
*
* Primarily used by test cases when testing for specific parsing exceptions.
* Initially used to count a goto * @param index * @return int */ private int getPtgSize(int index) { int count = 0; Iterator ptgIterator = tokens.listIterator(index); while (ptgIterator.hasNext()) { Ptg ptg = (Ptg)ptgIterator.next(); count+=ptg.getSize(); } return count; } private int getPtgSize(int start, int end) { int count = 0; int index = start; Iterator ptgIterator = tokens.listIterator(index); while (ptgIterator.hasNext() && index <= end) { Ptg ptg = (Ptg)ptgIterator.next(); count+=ptg.getSize(); index++; } return count; } /** * Generates the variable function ptg for the formula. *
* For IF Formulas, additional PTGs are added to the tokens
* @param name
* @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 AbstractFunctionPtg getFunction(String name, int numArgs, List argumentPointers) {
boolean isVarArgs;
int funcIx;
FunctionMetadata fm = FunctionMetadataRegistry.getFunctionByName(name.toUpperCase());
if(fm == null) {
// must be external function
isVarArgs = true;
funcIx = FunctionMetadataRegistry.FUNCTION_INDEX_EXTERNAL;
} else {
isVarArgs = !fm.hasFixedArgsLength();
funcIx = fm.getIndex();
validateNumArgs(numArgs, fm);
}
AbstractFunctionPtg retval;
if(isVarArgs) {
retval = new FuncVarPtg(name, (byte)numArgs);
} else {
retval = new FuncPtg(funcIx);
}
if (!name.equals(AbstractFunctionPtg.FUNCTION_NAME_IF)) {
// early return for everything else besides IF()
return retval;
}
AttrPtg ifPtg = new AttrPtg();
ifPtg.setData((short)7); //mirroring excel output
ifPtg.setOptimizedIf(true);
if (argumentPointers.size() != 2 && argumentPointers.size() != 3) {
throw new IllegalArgumentException("["+argumentPointers.size()+"] Arguments Found - An IF formula requires 2 or 3 arguments. IF(CONDITION, TRUE_VALUE, FALSE_VALUE [OPTIONAL]");
}
//Biffview of an IF formula record indicates the attr ptg goes after the condition ptgs and are
//tracked in the argument pointers
//The beginning first argument pointer is the last ptg of the condition
int ifIndex = tokens.indexOf(argumentPointers.get(0))+1;
tokens.add(ifIndex, ifPtg);
//we now need a goto ptgAttr to skip to the end of the formula after a true condition
//the true condition is should be inserted after the last ptg in the first argument
int gotoIndex = tokens.indexOf(argumentPointers.get(1))+1;
AttrPtg goto1Ptg = new AttrPtg();
goto1Ptg.setGoto(true);
tokens.add(gotoIndex, goto1Ptg);
if (numArgs > 2) { //only add false jump if there is a false condition
//second goto to skip past the function ptg
AttrPtg goto2Ptg = new AttrPtg();
goto2Ptg.setGoto(true);
goto2Ptg.setData((short)(retval.getSize()-1));
//Page 472 of the Microsoft Excel Developer's kit states that:
//The b(or w) field specifies the number byes (or words to skip, minus 1
tokens.add(goto2Ptg); //this goes after all the arguments are defined
}
//data portion of the if ptg points to the false subexpression (Page 472 of MS Excel Developer's kit)
//count the number of bytes after the ifPtg to the False Subexpression
//doesn't specify -1 in the documentation
ifPtg.setData((short)(getPtgSize(ifIndex+1, gotoIndex)));
//count all the additional (goto) ptgs but dont count itself
int ptgCount = this.getPtgSize(gotoIndex)-goto1Ptg.getSize()+retval.getSize();
if (ptgCount > Short.MAX_VALUE) {
throw new RuntimeException("Ptg Size exceeds short when being specified for a goto ptg in an if");
}
goto1Ptg.setData((short)(ptgCount-1));
return retval;
}
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 int Arguments(List argumentPointers) {
SkipWhite();
if(look == ')') {
return 0;
}
boolean missedPrevArg = true;
int numArgs = 0;
while(true) {
SkipWhite();
if(isArgumentDelimiter(look)) {
if(missedPrevArg) {
tokens.add(new MissingArgPtg());
addArgumentPointer(argumentPointers);
numArgs++;
}
if(look == ')') {
break;
}
Match(',');
missedPrevArg = true;
continue;
}
comparisonExpression();
addArgumentPointer(argumentPointers);
numArgs++;
missedPrevArg = false;
}
return numArgs;
}
/** Parse and Translate a Math Factor */
private void powerFactor() {
percentFactor();
while(true) {
SkipWhite();
if(look != '^') {
return;
}
Match('^');
percentFactor();
tokens.add(new PowerPtg());
}
}
private void percentFactor() {
tokens.add(parseSimpleFactor());
while(true) {
SkipWhite();
if(look != '%') {
return;
}
Match('%');
tokens.add(new PercentPtg());
}
}
/**
* factors (without ^ or % )
*/
private Ptg parseSimpleFactor() {
SkipWhite();
switch(look) {
case '#':
return parseErrorLiteral();
case '-':
Match('-');
powerFactor();
return new UnaryMinusPtg();
case '+':
Match('+');
powerFactor();
return new UnaryPlusPtg();
case '(':
Match('(');
comparisonExpression();
Match(')');
return new ParenthesisPtg();
case '"':
return parseStringLiteral();
case ',':
case ')':
return new MissingArgPtg(); // TODO - not quite the right place to recognise a missing arg
}
if (IsAlpha(look) || look == '\''){
return parseIdent();
}
// else - assume number
return parseNumber();
}
private Ptg parseNumber() {
String number2 = null;
String exponent = null;
String number1 = GetNum();
if (look == '.') {
GetChar();
number2 = GetNum();
}
if (look == 'E') {
GetChar();
String sign = "";
if (look == '+') {
GetChar();
} else if (look == '-') {
GetChar();
sign = "-";
}
String number = GetNum();
if (number == null) {
throw expected("Integer");
}
exponent = sign + number;
}
if (number1 == null && number2 == null) {
throw expected("Integer");
}
return getNumberPtgFromString(number1, number2, exponent);
}
private ErrPtg parseErrorLiteral() {
Match('#');
String part1 = GetName().toUpperCase();
switch(part1.charAt(0)) {
case 'V':
if(part1.equals("VALUE")) {
Match('!');
return ErrPtg.VALUE_INVALID;
}
throw expected("#VALUE!");
case 'R':
if(part1.equals("REF")) {
Match('!');
return ErrPtg.REF_INVALID;
}
throw expected("#REF!");
case 'D':
if(part1.equals("DIV")) {
Match('/');
Match('0');
Match('!');
return ErrPtg.DIV_ZERO;
}
throw expected("#DIV/0!");
case 'N':
if(part1.equals("NAME")) {
Match('?'); // only one that ends in '?'
return ErrPtg.NAME_INVALID;
}
if(part1.equals("NUM")) {
Match('!');
return ErrPtg.NUM_ERROR;
}
if(part1.equals("NULL")) {
Match('!');
return ErrPtg.NULL_INTERSECTION;
}
if(part1.equals("N")) {
Match('/');
if(look != 'A' && look != 'a') {
throw expected("#N/A");
}
Match(look);
// Note - no '!' or '?' suffix
return ErrPtg.N_A;
}
throw expected("#NAME?, #NUM!, #NULL! or #N/A");
}
throw expected("#VALUE!, #REF!, #DIV/0!, #NAME?, #NUM!, #NULL! or #N/A");
}
/**
* Get a PTG for an integer from its string representation.
* return Int or Number Ptg based on size of input
*/
private static Ptg getNumberPtgFromString(String number1, String number2, String exponent) {
StringBuffer number = new StringBuffer();
if (number2 == null) {
number.append(number1);
if (exponent != null) {
number.append('E');
number.append(exponent);
}
String numberStr = number.toString();
int intVal;
try {
intVal = Integer.parseInt(numberStr);
} catch (NumberFormatException e) {
return new NumberPtg(numberStr);
}
if (IntPtg.isInRange(intVal)) {
return new IntPtg(intVal);
}
return new NumberPtg(numberStr);
}
if (number1 != null) {
number.append(number1);
}
number.append('.');
number.append(number2);
if (exponent != null) {
number.append('E');
number.append(exponent);
}
return new NumberPtg(number.toString());
}
private StringPtg parseStringLiteral()
{
Match('"');
StringBuffer token = new StringBuffer();
while (true) {
if (look == '"') {
GetChar();
if (look != '"') {
break;
}
}
token.append(look);
GetChar();
}
return new StringPtg(token.toString());
}
/** Parse and Translate a Math Term */
private void Term() {
powerFactor();
while(true) {
SkipWhite();
switch(look) {
case '*':
Match('*');
powerFactor();
tokens.add(new MultiplyPtg());
continue;
case '/':
Match('/');
powerFactor();
tokens.add(new DividePtg());
continue;
}
return; // finished with Term
}
}
private void comparisonExpression() {
concatExpression();
while (true) {
SkipWhite();
switch(look) {
case '=':
case '>':
case '<':
Ptg comparisonToken = getComparisonToken();
concatExpression();
tokens.add(comparisonToken);
continue;
}
return; // finished with predicate expression
}
}
private Ptg getComparisonToken() {
if(look == '=') {
Match(look);
return new EqualPtg();
}
boolean isGreater = look == '>';
Match(look);
if(isGreater) {
if(look == '=') {
Match('=');
return new GreaterEqualPtg();
}
return new GreaterThanPtg();
}
switch(look) {
case '=':
Match('=');
return new LessEqualPtg();
case '>':
Match('>');
return new NotEqualPtg();
}
return new LessThanPtg();
}
private void concatExpression() {
additiveExpression();
while (true) {
SkipWhite();
if(look != '&') {
break; // finished with concat expression
}
Match('&');
additiveExpression();
tokens.add(new ConcatPtg());
}
}
/** Parse and Translate an Expression */
private void additiveExpression() {
Term();
while (true) {
SkipWhite();
switch(look) {
case '+':
Match('+');
Term();
tokens.add(new AddPtg());
continue;
case '-':
Match('-');
Term();
tokens.add(new SubtractPtg());
continue;
}
return; // finished with additive expression
}
}
//{--------------------------------------------------------------}
//{ Parse and Translate an Assignment Statement }
/**
procedure Assignment;
var Name: string[8];
begin
Name := GetName;
Match('=');
Expression;
end;
**/
/** API call to execute the parsing of the formula
*
*/
public void parse() {
pointer=0;
GetChar();
comparisonExpression();
if(pointer <= formulaLength) {
String msg = "Unused input [" + formulaString.substring(pointer-1)
+ "] after attempting to parse the formula [" + formulaString + "]";
throw new FormulaParseException(msg);
}
}
/*********************************
* PARSER IMPLEMENTATION ENDS HERE
* EXCEL SPECIFIC METHODS BELOW
*******************************/
/** API call to retrive the array of Ptgs created as
* a result of the parsing
*/
public Ptg[] getRPNPtg() {
return getRPNPtg(FORMULA_TYPE_CELL);
}
public Ptg[] getRPNPtg(int formulaType) {
Node node = createTree();
setRootLevelRVA(node, formulaType);
setParameterRVA(node,formulaType);
return (Ptg[]) tokens.toArray(new Ptg[0]);
}
private void setRootLevelRVA(Node n, int formulaType) {
//Pg 16, excelfileformat.pdf @ openoffice.org
Ptg p = n.getValue();
if (formulaType == FormulaParser.FORMULA_TYPE_NAMEDRANGE) {
if (p.getDefaultOperandClass() == Ptg.CLASS_REF) {
setClass(n,Ptg.CLASS_REF);
} else {
setClass(n,Ptg.CLASS_ARRAY);
}
} else {
setClass(n,Ptg.CLASS_VALUE);
}
}
private void setParameterRVA(Node n, int formulaType) {
Ptg p = n.getValue();
int numOperands = n.getNumChildren();
if (p instanceof AbstractFunctionPtg) {
for (int i =0;i