From 4d2a757cdc1f0cd6c0c461e804c5642f26c9fa18 Mon Sep 17 00:00:00 2001 From: Danny Mui Date: Sat, 15 Mar 2003 01:57:41 +0000 Subject: [PATCH] FormulaParser changes to support IF function(s) git-svn-id: https://svn.apache.org/repos/asf/jakarta/poi/trunk@353027 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/poi/hssf/model/FormulaParser.java | 174 ++++++++++++++++-- .../record/formula/AbstractFunctionPtg.java | 57 +++++- .../poi/hssf/record/formula/AttrPtg.java | 10 +- .../poi/hssf/model/TestFormulaParser.java | 136 +++++++++++++- .../poi/hssf/usermodel/TestFormulas.java | 55 +++++- 5 files changed, 408 insertions(+), 24 deletions(-) diff --git a/src/java/org/apache/poi/hssf/model/FormulaParser.java b/src/java/org/apache/poi/hssf/model/FormulaParser.java index 06c28c9f5d..99314bb86b 100644 --- a/src/java/org/apache/poi/hssf/model/FormulaParser.java +++ b/src/java/org/apache/poi/hssf/model/FormulaParser.java @@ -2,7 +2,7 @@ /* ==================================================================== * The Apache Software License, Version 1.1 * - * Copyright (c) 2002 The Apache Software Foundation. All rights + * Copyright (c) 2002, 2003 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without @@ -56,12 +56,34 @@ package org.apache.poi.hssf.model; -import org.apache.poi.hssf.record.formula.*; -import org.apache.poi.hssf.util.SheetReferences; - import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedList; import java.util.List; +import org.apache.poi.hssf.record.formula.AbstractFunctionPtg; +import org.apache.poi.hssf.record.formula.AddPtg; +import org.apache.poi.hssf.record.formula.Area3DPtg; +import org.apache.poi.hssf.record.formula.AreaPtg; +import org.apache.poi.hssf.record.formula.AttrPtg; +import org.apache.poi.hssf.record.formula.BoolPtg; +import org.apache.poi.hssf.record.formula.ConcatPtg; +import org.apache.poi.hssf.record.formula.DividePtg; +import org.apache.poi.hssf.record.formula.EqualPtg; +import org.apache.poi.hssf.record.formula.FuncVarPtg; +import org.apache.poi.hssf.record.formula.IntPtg; +import org.apache.poi.hssf.record.formula.MultiplyPtg; +import org.apache.poi.hssf.record.formula.NumberPtg; +import org.apache.poi.hssf.record.formula.OperationPtg; +import org.apache.poi.hssf.record.formula.ParenthesisPtg; +import org.apache.poi.hssf.record.formula.PowerPtg; +import org.apache.poi.hssf.record.formula.Ptg; +import org.apache.poi.hssf.record.formula.Ref3DPtg; +import org.apache.poi.hssf.record.formula.ReferencePtg; +import org.apache.poi.hssf.record.formula.StringPtg; +import org.apache.poi.hssf.record.formula.SubtractPtg; +import org.apache.poi.hssf.util.SheetReferences; + /** * This class parses a formula string into a List of tokens in RPN order. @@ -90,6 +112,12 @@ public class FormulaParser { private int formulaLength; private List tokens = new java.util.Stack(); + + /** + * Using an unsynchronized linkedlist to implement a stack since we're not multi-threaded. + */ + private List functionTokens = new LinkedList(); + //private Stack tokens = new java.util.Stack(); private List result = new ArrayList(); private int numParen; @@ -98,6 +126,7 @@ public class FormulaParser { private static char CR = '\n'; private char look; // Lookahead Character + private boolean inFunction = false; private Workbook book; @@ -297,25 +326,140 @@ public class FormulaParser { } } + /** + * Adds a pointer to the last token to the latest function argument list. + * @param obj + */ + private void addArgumentPointer() { + if (this.functionTokens.size() > 0) { + //no bounds check because this method should not be called unless a token array is setup by function() + List arguments = (List)this.functionTokens.get(0); + arguments.add(tokens.get(tokens.size()-1)); + } + } + private void function(String name) { + //average 2 args per function + this.functionTokens.add(0, new ArrayList(2)); + Match('('); int numArgs = Arguments(); Match(')'); - tokens.add(getFunction(name,(byte)numArgs)); + + Ptg functionPtg = getFunction(name,(byte)numArgs); + + tokens.add(functionPtg); + + //remove what we just put in + this.functionTokens.remove(0); } + /** + * Adds the size of all the ptgs after the provided index (inclusive). + *

+ * 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 Ptg getFunction(String name,byte numArgs) { Ptg retval = null; - //retval = new FuncVarPtg(name,numArgs); - if (name.equals("IF")) { - AttrPtg ptg = new AttrPtg(); - ptg.setData((short)6); //sums don't care but this is what excel does. - ptg.setOptimizedIf(true); - retval = ptg; - } else { - retval = new FuncVarPtg(name,numArgs); - } + if (name.equals("IF")) { + retval = new FuncVarPtg(AbstractFunctionPtg.ATTR_NAME, numArgs); + + //simulated pop, no bounds checking because this list better be populated by function() + List argumentPointers = (List)this.functionTokens.get(0); + + + 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 > (int)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)); + + } else { + + retval = new FuncVarPtg(name,numArgs); + } + return retval; } @@ -451,6 +595,8 @@ public class FormulaParser { if (look == '*') Multiply(); if (look == '/') Divide(); } + addArgumentPointer(); + } diff --git a/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java b/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java index 17004fbe77..6dcb7ef0f1 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/AbstractFunctionPtg.java @@ -1,3 +1,56 @@ +/* ==================================================================== + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2002, 2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Apache" and "Apache Software Foundation" and + * "Apache POI" must not be used to endorse or promote products + * derived from this software without prior written permission. For + * written permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * "Apache POI", nor may "Apache" appear in their name, without + * prior written permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ package org.apache.poi.hssf.record.formula; import org.apache.poi.util.BinaryTree; @@ -13,7 +66,9 @@ import java.util.Stack; * @author Andrew C. Oliver (acoliver at apache dot org) */ public abstract class AbstractFunctionPtg extends OperationPtg { - + //constant used allow a ptgAttr to be mapped properly for its functionPtg + public static final String ATTR_NAME = "specialflag"; + private final static int SIZE = 4; private static BinaryTree map = produceHash(); diff --git a/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java b/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java index b87e250696..bd7ede88f3 100644 --- a/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java +++ b/src/java/org/apache/poi/hssf/record/formula/AttrPtg.java @@ -2,7 +2,7 @@ /* ==================================================================== * The Apache Software License, Version 1.1 * - * Copyright (c) 2002 The Apache Software Foundation. All rights + * Copyright (c) 2002, 2003 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without @@ -144,6 +144,14 @@ public class AttrPtg field_1_options=optiIf.setByteBoolean(field_1_options,bif); } + /** + * Flags this ptg as a goto/jump + * @param isGoto + */ + public void setGoto(boolean isGoto) { + field_1_options=optGoto.setByteBoolean(field_1_options, isGoto); + } + // lets hope no one uses this anymore public boolean isBaxcel() { diff --git a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java index 833eabd8bc..6c82ec76f0 100644 --- a/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java +++ b/src/testcases/org/apache/poi/hssf/model/TestFormulaParser.java @@ -1,3 +1,56 @@ +/* ==================================================================== + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2002, 2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Apache" and "Apache Software Foundation" and + * "Apache POI" must not be used to endorse or promote products + * derived from this software without prior written permission. For + * written permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * "Apache POI", nor may "Apache" appear in their name, without + * prior written permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + */ package org.apache.poi.hssf.model; import junit.framework.TestCase; @@ -80,20 +133,95 @@ public class TestFormulaParser extends TestCase { FormulaParser fp = new FormulaParser(yn, null); fp.parse(); Ptg[] asts = fp.getRPNPtg(); - assertEquals(4, asts.length); + assertEquals(7, asts.length); BoolPtg flag = (BoolPtg) asts[0]; - StringPtg y = (StringPtg) asts[1]; - StringPtg n = (StringPtg) asts[2]; - AttrPtg funif = (AttrPtg) asts[3]; + AttrPtg funif = (AttrPtg) asts[1]; + StringPtg y = (StringPtg) asts[2]; + AttrPtg goto1 = (AttrPtg) asts[3]; + StringPtg n = (StringPtg) asts[4]; + assertEquals(true, flag.getValue()); assertEquals("Y", y.getValue()); assertEquals("N", n.getValue()); assertEquals("IF", funif.toFormulaString(new SheetReferences())); + assertTrue("Goto ptg exists", goto1.isGoto()); } + public void testSimpleIf() throws Exception { + final String simpleif = "IF(1=1,0,1)"; + FormulaParser fp = new FormulaParser(simpleif, null); + fp.parse(); + Ptg[] asts = fp.getRPNPtg(); + assertEquals(9, asts.length); + + IntPtg op1 = (IntPtg) asts[0]; + IntPtg op2 = (IntPtg) asts[1]; + EqualPtg eq = (EqualPtg) asts[2]; + AttrPtg ifPtg = (AttrPtg) asts[3]; + IntPtg res1 = (IntPtg) asts[4]; + + AttrPtg ptgGoto= (AttrPtg) asts[5]; + assertEquals("Goto 1 Length", (short)10, ptgGoto.getData()); + + IntPtg res2 = (IntPtg) asts[6]; + AttrPtg ptgGoto2 = (AttrPtg) asts[7]; + assertEquals("Goto 2 Length", (short)3, ptgGoto2.getData()); + + assertEquals("If FALSE offset", (short)7, ifPtg.getData()); + + FuncVarPtg funcPtg = (FuncVarPtg)asts[8]; + + + } + /** + * Make sure the ptgs are generated properly with two functions embedded + * + */ + public void testNestedFunctionIf() { + String function = "IF(A1=B1,AVERAGE(A1:B1),AVERAGE(A2:B2))"; + + FormulaParser fp = new FormulaParser(function, null); + fp.parse(); + Ptg[] asts = fp.getRPNPtg(); + assertEquals("11 Ptgs expected", 11, asts.length); + + assertTrue("IF Attr set correctly", (asts[3] instanceof AttrPtg)); + AttrPtg ifFunc = (AttrPtg)asts[3]; + assertTrue("It is not an if", ifFunc.isOptimizedIf()); + + assertTrue("Average Function set correctly", (asts[5] instanceof FuncVarPtg)); + + + } + + public void testIfSingleCondition(){ + String function = "IF(1=1,10)"; + + FormulaParser fp = new FormulaParser(function, null); + fp.parse(); + Ptg[] asts = fp.getRPNPtg(); + assertEquals("7 Ptgs expected", 7, asts.length); + + assertTrue("IF Attr set correctly", (asts[3] instanceof AttrPtg)); + AttrPtg ifFunc = (AttrPtg)asts[3]; + assertTrue("It is not an if", ifFunc.isOptimizedIf()); + + assertTrue("Single Value is not an IntPtg", (asts[4] instanceof IntPtg)); + IntPtg intPtg = (IntPtg)asts[4]; + assertEquals("Result", (short)10, intPtg.getValue()); + + assertTrue("Ptg is not a Variable Function", (asts[6] instanceof FuncVarPtg)); + FuncVarPtg funcPtg = (FuncVarPtg)asts[6]; + assertEquals("Arguments", 2, funcPtg.getNumberOfOperands()); + + + + + } + public static void main(String [] args) { System.out.println("Testing org.apache.poi.hssf.record.formula.FormulaParser"); junit.textui.TestRunner.run(TestFormulaParser.class); diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java index 0df72a7349..bab97e1d22 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestFormulas.java @@ -2,7 +2,7 @@ /* ==================================================================== * The Apache Software License, Version 1.1 * - * Copyright (c) 2002 The Apache Software Foundation. All rights + * Copyright (c) 2002, 2003 The Apache Software Foundation. All rights * reserved. * * Redistribution and use in source and binary forms, with or without @@ -894,7 +894,7 @@ extends TestCase { } -/* + public void testIfFormulas() throws java.io.IOException { @@ -935,9 +935,56 @@ extends TestCase { assertTrue("expected: IF(A3=A1,\"A1\",\"A2\") got "+c.getCellFormula(), ("IF(A3=A1,\"A1\",\"A2\")").equals(c.getCellFormula())); //c = r.getCell((short)1); //assertTrue("expected: A!A1+A!B1 got: "+c.getCellFormula(), ("A!A1+A!B1").equals(c.getCellFormula())); - in.close(); + in.close(); + + File simpleIf = File.createTempFile("testSimpleIfFormulaWrite",".xls"); + out = new FileOutputStream(simpleIf); + wb = new HSSFWorkbook(); + s = wb.createSheet("Sheet1"); + r = null; + c = null; + r = s.createRow((short)0); + c=r.createCell((short)0); c.setCellFormula("IF(1=1,0,1)"); + + wb.write(out); + out.close(); + assertTrue("file exists", simpleIf.exists()); + + assertTrue("length of simpleIf file is zero", (simpleIf.length()>0)); + + File nestedIf = File.createTempFile("testNestedIfFormula",".xls"); + out = new FileOutputStream(nestedIf); + wb = new HSSFWorkbook(); + s = wb.createSheet("Sheet1"); + r = null; + c = null; + r = s.createRow((short)0); + c=r.createCell((short)0); + c.setCellValue(1); + + c=r.createCell((short)1); + c.setCellValue(3); + + + HSSFCell formulaCell=r.createCell((short)3); + + r = s.createRow((short)1); + c=r.createCell((short)0); + c.setCellValue(3); + + c=r.createCell((short)1); + c.setCellValue(7); + + formulaCell.setCellFormula("IF(A1=B1,AVERAGE(A1:B1),AVERAGE(A2:B2))"); + + + wb.write(out); + out.close(); + assertTrue("file exists", nestedIf.exists()); + + assertTrue("length of nestedIf file is zero", (nestedIf.length()>0)); } -*/ + public static void main(String [] args) { System.out