/* * Formula.java * * Created on August 11, 2006, 12:02 PM * * Copyright 2006 Lee Lofgern and Accounting Enhancements Inc Licensed 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 com.accountingenhancements.formula; import com.accountingenhancements.common.SupportParameters; import java.text.ParseException; /** *The Formula class is the easiest way to interact with this package.
*The formula package is a macro language written in Java. * It can be used as a math engine and to concatinate strings. * The language supports long, double, String, and Date type variables as well as code fragments and variable references. * All of the variables and code segments are in string form. * The language has some built in functions and you can add your own functions (in java) to extend the language.
*

*The language goals are as follows.
*Be able to solve various math problems dynamically based on string based code.
*Support variables that are handed to the various routines as strings.
*Automatically determine data types in a string based on the data's characteristics * (4/5/06 is a date, 543.2 is a decimal, 4 is a long, etc).
*Keep re-calculations down by having a method to determine when a result is still valid. * Be able to assign levels of volatility to variables to help determine when a * result should be recalculated and when the last calculated value is still pertinent.
*Keep the language very simple.
*Allow easy creation of new functions.
*Have a way to easily hand objects to functions that can't come from strings, such as * database connections.
*

*The core of this package is variables. A variable can be a long integer, a double decimal, * a date, a reference to another variable, a reference to a function, a code fragment, * or a text string.
*The FormulaVariable can detect the type of data it is holding * (You can also declare the data type). Numbers with no decimal point are long, * numbers with a decimal point is a double, numbers with slashes such as 4/5/06 are dates, * the words yes, no, true, and false are boolean, text strings starting with a letter * and with no special characters or spaces (except underscore) is a reference to * another variable, a text string that with no special characters or spaces * (except underscore) followed by something enclosed in parentheses is a reference * to a function. Text surrounded by double-quotes is a string. Everything else is a formula.
*The mathematical operators supported are as follows:
*Unary Bit Not (~), Unary Not (!), Equals(=), Not Equal (!=, <>), Less Than or Equal (<=, =<), * Less Than (<), Greater Than Or Equal (>=, =>), Greater Than (>), Or (||, Or), And (&&, And), * Multiply (*), Divide (/), Modulus (%), Bit And (&), Bit Or (|), * Plus (+), Minus (-), and Parentheses. And and Or are not case sensitive.
*

*Data types with special treatment:
*Dates:
*Dates are can be subtracted to determine the number of days between them. * "1/2/06 - 12/12/05" will return 21. Dates can't be added. "1/2/06 + 12/12/05" will * cause an ArithmeticException. Days can be added and subtracted from dates. * "12/12/05 + 21" will return "01/02/2006". "1/2/06 - 21" will return "12/12/2005". * Dates can be compared. "12/2/2006<1/2/07" returns true. *Strings:
*Text strings can be added (concatinated) and compared and nothing more. * "Hi" + " There" returns "Hi There", "Hi" - " There" throws an ArithmeticException. * Comparitors are case sensitive. "\"AA\"<\"aa\"" returns true.
*

*Formulas:
*Formulas are parsed down into there constituent parts, then solved, * from left to right, except according to the precedence of the various * operators. Operator precedence follows standard math principles and, * therefore, is in the same order as they are in java. * 12+5*6 will solve 5*6 before adding the result to 12.
* The Or operator will stop resolving any other OR separated math operations * following a true statement. The And operator will stop resolving any other And * separated math operations following a false statement. * "5>4 Or (8+2*3<0)" will not solve (8+2*3<0) since 5>4 is already true. * "4>5 And (8+2*3>10)" will not solve (8+2*3>10) since 4>5 is already false. *

*Variables:
*Everything is stored in variables except operational objects such as database connections. * This includes Formulas, references to other variables, references to functions, * and data, and the operators.
* Variables are usually kept in a FormulaVariableList. This can retrieve the variable * values by variable name. Varible names are not case-sensitive.
* The FormulaVariable has a method (The solve() method) that * resolves the value based on the content of the variable.
* While creating variables, you can assign a level of volatility, * the higher the number, the less stable the value. This number is used when * resolving a formula or function to determine whether the previous result * is still accurate or whether the calculation has to be re-performed.
*Variables are substituted into the equations only while solving a formula. * This means that a formula can be executed multiple times with nothing changed * except the contents of one or more variables and it will return a new solution.
* When solving a formula, the formula tracks the highest level variable used in the solution. * When re-solving the formula you specify the highest level variable that is still * considered accurate and the solve() method will only re-solve the * equation if the highest level used is higher than the level specified, otherwise * it will return the previous solution.
*Example:
*FormulaVariableList variableList = new FormulaVariableList();
*variableList.addValue("CurrentDate","8/14/06",0); //Level 0
*variableList.addValue("BirthDate","1/3/2001",2); //Level 2
*FormulaVariable formula = new FormulaVariable("","/"You are /"+(currentDate-birthdate)+/" days old!/"",true); //true means treat unquoted text as formulas
*System.out.println(formula.solve(variableList,0,null,null,10) //Resolve everything that has used a level higher than level 10.
*Since this hasn't been solved before, it will solve the problem anyway and print
*"You Are 2048 days old.".
*variableList.getVariable("birthdate").setValue("1/4/2001"); //Update the birthdate
*System.out.println(formula.solve(variableList,0,null,null,10) //Resolve everything that has used a level higher than level 10.
*"You Are 2048 days old." will still be returned since even though we changed the birthdate, * we told solve to only re-calculate if a variable with a level higher than 10 was used * in the previous solution.
*System.out.println(formula.solve(variableList,0,null,null,1) //Resolve everything that has used a level higher than level 1.
*"You Are 2047 days old." is now returned since the solution required at least 1 * variable higher than level 1. Note: If we had changed current date, we still would have * received the correct answer since the method only required that a variable with a level * higher than our specified level was used in the calculation, it does not try to figure out * whether it was the one to change.
* Note: In re-solving the above example when currentDate changes, the following is a logic example where the old value would still have been returned. * If currentDate referenced another variable (varibleList.addValue("CurrentDate","TodaysDate",0))
* and that variable was also less than or equal to 1 (variableLsit.addValue("TodaysDate","1/1/2006",0)),
* then the currentDate will still report the old date since level 1 or lower variables are not supposed to be re-calculated. *
* The intent of levels is to speed up re-calculations in your loops. * Example: You are processing Earning Codes for employees, You could assign todays * date as level 0, employees as level 1, and earning codes as level 2. * Then when you know only earning codes have changed, you could solve as level 1 and * no employee specific data, such as hourly wage, would be solved more than once. However, if the * employee number changed, then you could perform a level 0 solve and the formula's relating * to the employee would be recalculated as well.
* However, if an employee number did change and you stuck to level 1 when using earning formulas * in your earning code loop, they would return the wrong answer.
* To prevent this problem suggest you use a FormulaVariableStack to keep * track of all of your important formulas and when a lower level event occurs, such as an employee change, you use * the purgeResultsInStackGreaterThanSpecifiedLevel(int) method to purge all of the * pertinent results from all of your formulas. Otherwise you risk accidently using old data. * The mistake could occur as follows. You are adding wages by taking an employee's earning * hours times their pay rate, this is a level 1 event because the employee isn't changing, * the employee now changes so you process some formulas at level 0 but you aren't using the * routine that calculates wages, once you are again at wage calculations, you are again using * level 1, however these formulas never knew about the level 0 stuff so would still be using * info from the previous employee. If you keep your formula variables in a FormulaVariableStack * you can purge the results of all formulas, functions and references to variables, myStack.purgeResultsInStackGreaterThanSpecifiedLevel(0), * above a specified level, which will force them to do the calculation the first time through no matter the level.
*

*Functions:
*This package has some built in functions such as IIF(testData,trueResult,falseResult), UCase(aString), Len(aString), and Mid(aString,Start,length).
You can also create your own functions by extending the FormulaFunction class.
*Functions are referenced from a FormulaFunctionList and are access through their names. These names are not case-sensitive. * You can add your own functions to this list and this list is handed to the solve(...) * methods. This is the only way the solve(...) routines have to solve functions.
*You can use these functions in your formulas.
* Example:
*"\"You Are \"+IIF(marStat=\"M\",\"married\",\"single\")+\".\""
*will return "You are married." if the variable MarStat=="M" otherwise it will return "You are single.".
*A function gets handed the contents between the parentheses as arguments. They also get * an array of Objects called SupportParameters. These SupportParameters are used to hand custom * functions Objects such as Database Connections or any other Object that you may need to accomplish * the function's goals. None of the built-in functions should ever need anything from the SupportPrameters class.
*

*

*The following example uses most of the principles described above:
*Create an SQL Function:

*public class Sql_Function extends FormulaFunction{
*    protected static String[][] requiredArguments = {{"ARG1: An SQL query","TYPE_STRING"}}; *    protected static String[][] requiredSupportParameters = {{"SQL_Connection: The sql connection","java.sql.Connection"}}; *    public Sql_Function(FormulaVariableStack functionArgumentStack){super(functionArgumentStack);}
*    public Sql_Function(String functionArgumentString) throws ParseException{super(functionArgumentString);}
*    public Sql_Function(String functionArgumentString, int level) throws ParseException{super(functionArgumentString,level);}
*    public Sql_Function(FormulaVariable functionVariable) throws ParseException{super(functionVariable);}
*    protected FormulaVariable solve(FormulaVariableList variableList, int iteration, SupportParameters supportParameters, FormulaFunctionList functionList, int resolveEverythingAboveLevel) throws java.text.ParseException, java.lang.ArithmeticException, ClassNotFoundException{
*        FormulaVariable result=null;String resultString;int length=0;int highestLevel=0;String field="";
*        if(functionArgumentStack==null)throw new java.lang.ArithmeticException("functionArgumentStack is null");
*        result=functionArgumentStack.get("ARG1"); //Get the only argument this function takes. Any other aruments are ignored.
*        if(result==null)throw new java.lang.ArithmeticException("ARG1 is missing from functionArgumentStack");
*        highestLevel=result.getHighestLevel(); //Keep track of the highest level used so our result reflects this level.
*        result=result.solve(variableList,iteration,supportParameters,functionList,resolveEverythingAboveLevel);
*        if(result!=null){
*        highestLevel=result.getHighestLevel(); //Get the new highest level *            try{
*                java.sql.Connection conn = (java.sql.Connection)supportParameters.get("SQL_Connection"); //The programmer using this function must supply SQL_Connection
*                if(conn!=null){
*                    java.sql.ResultSet rs=conn.createStatement().executeQuery(result.toString());
*                    if(rs.first())field=rs.getString(1);
*                }
*            } catch (Exception ex){} //Ignore all errors.
*        }
*        result=new FormulaVariable("",field,highestLevel);
*        return result;
*    }
*    public static String getName(){return "Sql";}
*    public static String[][] getRequiredArguments(){return requiredArguments;} *    public static String[][] getRequiredSupportParameters(){return requiredSupportParameters;} *}
*
*Use the SQL Function:
*
*Formula formulaEngine=new Formula();
*formulaEngine.add(Sql_Function.class); //Added to functionList
*formulaEngine.add("SQL_Connection",(Object)myPreviouslyConstructedSqlConnection); //Added to supportParametersList
*formulaEngine.add("CustNum=\"000000001\"",1); \\Force to be string by putting quotes around value.
*formulaEngine.add("GetName","\"Your name is \"+SQL(\"select CustName from tblCustomer where Customer='\"+CustNum+\"'\")+\".\"",0); //Automatically added to formulaStack.
*String name = formulaEngine.solve("GetName",1).toString();
*formulaEngine.get("custnum").setValue("000000002"); //Don't need to reuse slashed quotes since this variable is already defined as a TYPE_STRING.
*name = formulaEngine.solve("GetName",1).toString(); //Returns same name as before since we specified not to resolve any calculations using level 1 variables or lower.
*name = formulaEngine.solve("GetName",0).toString(); //Now gets new name since it is told to recalculate any value that was originally calculated involving a level higher that level 0.
*formulaEngine.purgeDownToSpecifiedLevel(0);
*try{
*    name = formulaEngine.solve("GetName",1).toString(); //Now we get an error because the purge deleted the CustNum variable from the variableList and when the engine tries to solve, it can't find custNum.
*}catch(Exception ex){}
*formulaEngine.add("CustNum=CustomerNumber",0);
*formulaEngine.add("CustomerNumber=\"000000001\"",2);
*name = formulaEngine.solve("GetName",2).toString();
*formulaEngine.get("CUSTOMERNUMBER").setValue("000000002");
*name = formulaEngine.solve("GetName",2).toString(); //Wrong name since it is again told not to recalcuate previous results using level 2 variables or below.
*name = formulaEngine.solve("getname",1).toString(); //Right answer now.
*formulaEngine.purgeDownToSpecifiedLevel(1); //Clear out previous calculations involving variables of levels above level 1 and purge all variables with a level above level 1.
*formulaEngine.add("CUSTOMERnumber=\"000000003\"",2); //Need to re-add variable since we dumped it in the last line.
*name = formulaEngine.solve("GETNAME",2).toString(); //Still right answer because, even though we specified not to resolve any calculations using level 2 variables or below, we cleared out all solutions involving levels above level 1 two lines earlier.
*

*

*More Examples:
*System.out.println(Formula.getQuickValue("A+2","A=10")); //Prints 12. The returned class is String.
*System.out.println(Formula.getQuickValue("A+2","A=10.50")); //Prints 12.50. The returned class is Stirng.
*System.out.println(Formula.getQuickValue("iif(Birthdate>\"8/22/2006\",\"You were born after 8/22/2006\",\"You were born on or before 8/22/2006\")","BirthDate=9/28/2000")); //Prints "You were born on or before 8/22/2006"
*System.out.println(Formula.getQuickValue("iif(Birthdate>\"8/22/2006\",\"You were born after 8/22/2006\",\"You were born on or before 8/22/2006\")","BirthDate=9/28/2006")); //Prints "You were born after 8/22/2006"
*System.out.println(Formula.getQuickValue("8/2/3",null); //Prints "08/02/2003" because it interpreted this as a date *System.out.println(Formula.getQuickValue("8/2/ 3",null); //Prints "1" because it now recognizes this is not a date *

* @author Lee lofgren lofgren_opensource@accountingenhancements.com * @version 0.1009102006 */ public class Formula { protected FormulaFunctionList functionList=new FormulaFunctionList(); protected FormulaVariableList variableList=new FormulaVariableList(); protected FormulaVariableStack formulaStack=new FormulaVariableStack(); protected SupportParameters supportParameters=new SupportParameters(); /** Creates a new instance of Formula */ public Formula() { } /** *Get internal the function list. *@return the internal function list used by this object. */ public FormulaFunctionList getFunctionList(){return functionList;} /** *Get the internal variable list *@return the internal variable list used by this object. */ public FormulaVariableList getVariableList(){return variableList;} /** *Get the internal formula stack *@return the internal formula stack used by this object. This stack is used by the purging routine. */ public FormulaVariableStack getFormulaStack(){return formulaStack;} /** *Get the internal support parameter list *@return the internal support parameter list used by this object. */ public SupportParameters getSupportParameters(){return supportParameters;} /** *Purge all variables above the specified level from the variable list * and clear out previously solved results from formulas where a level * higher than this one was used in the solution. *@param specifiedLevel the level, above which, all information * should be discarded to prevent new calculations from being corrupted by old data. */ public void purgeDownToSpecifiedLevel(int specifiedLevel){ formulaStack.purgeResultsInStackGreaterThanSpecifiedLevel(specifiedLevel); variableList.removeLevel(specifiedLevel+1); } /** *Add a variable
*Unquoted text is treated as a formula. *@param variableName the name of the new variable *@param value the value of this variable *@param scaleIfDouble the scale of this value if it is a double, this the rounding of the result. 4.345 scale 2 becomes 4.35. *@param level the level of volatility. * */ public void add(String variableName, String value, int scaleIfDouble, int level){ FormulaVariable variable = variableList.addValue(variableName,value,scaleIfDouble,level,true); if(variable!=null&&(variable.getVariableType()==FormulaVariable.TYPE_FORMULA||variable.getVariableType()==FormulaVariable.TYPE_FUNCTION||variable.getVariableType()==FormulaVariable.TYPE_VARIABLE))formulaStack.add(variable); } /** *Add a class to the FormulaFunction list. *@param functionClass a class of a user defined function that should be added to the function list */ public void add(Class functionClass){ functionList.addFunction(functionClass); } /** *Add a support parameter. *@param supportParameterName the name of a parameter object that is needed by one of the user defined functions *@param supportParameterObject the object that is returned from the supportParameterList when requested with the supportParameterName. */ public void add(String supportParameterName, Object supportParameterObject){supportParameters.put(supportParameterName, supportParameterObject);} /** *Add one or more variables, separated by semi-colons. *@param variables a semi-colon separated list of variable names and their values. Example: add("Var1=2;Var2=A+B; VAR_3=\"Hi there\"") *@param level the level of volatility *@throws ParseException is equal sign is missing */ public void add(String variables, int level) throws ParseException{ FormulaVariableStack variableStack=variableList.addVariables(variables,level); FormulaVariable variable; for(int i=0;i0)variableList.addVariable(variables); variable=variable.solve(variableList,0,null,functionList,0); } return variable; } /** *Get a variable from the variable list. *@param variableName the name of the variable to be retrieved. *@return the formula variable. */ public FormulaVariable get(String variableName){ return variableList.getVariable(variableName); } /** *Get the value of a variable of formula. *@param variableName the name of the variable or formula *@return the result. solve(String variableName) is used to get the result so the return string will be the answer, if this is a formula or function. *@throws ParseException, ClassNotFoundException */ public String getValue(String variableName) throws ParseException, ClassNotFoundException{ FormulaVariable variable=solve(variableName); String value=""; if(variable!=null&&variable.isNull()==false)value=variable.getString(); return value; } /** *Get the value of a variable of formula. *@param variableName the name of the variable or formula *@param resolveEverythingAboveLevel the level, above which, values should be re-solved. *@return the result. solve(String variableName) is used to get the result so the return string will be the answer, if this is a formula or function. *@throws ParseException, ClassNotFoundException */ public String getValue(String variableName, int resolveEverythingAboveLevel) throws ParseException, ClassNotFoundException{ FormulaVariable variable=solve(variableName,resolveEverythingAboveLevel); String value=""; if(variable!=null&&variable.isNull()==false)value=variable.getString(); return value; } /** *Get the value of a variable of formula. Don't throw an exception. Return an empty string if there is any kind of error. *@param variableName the name of the variable or formula *@return the result. solve(String variableName) is used to get the result so the return string will be the answer, if this is a formula or function. */ public String getValueNoErrors(String variableName){ String value=""; try{ FormulaVariable variable=solve(variableName); if(variable!=null&&variable.isNull()==false)value=variable.getString(); }catch (Exception e){} return value; } /** *Get the value of a variable of formula. Don't throw an exception. Return an empty string if there is any kind of error. *@param variableName the name of the variable or formula *@param resolveEverythingAboveLevel the level, above which, values should be re-solved. *@return the result. solve(String variableName) is used to get the result so the return string will be the answer, if this is a formula or function. */ public String getValueNoErrors(String variableName, int resolveEverythingAboveLevel){ String value=""; try{ FormulaVariable variable=solve(variableName,resolveEverythingAboveLevel); if(variable!=null&&variable.isNull()==false)value=variable.getString(); } catch (Exception e){} return value; } /** *A static method to Quickly get the value from a formula. You don't have to instantiate this class to get the result. * This method should not be used when the same formulas or variables are used multiple times within a loop since this has to parse everything every time. *@param formula the formula that requires solving. *@param variables an optional semi-colon separated list of variables, where the variables structured as name, equal symbol, value. Example: getQuickValue("A+B","A=1;B=2")=="3"; *@return the result. solve(String variableName) is used to get the result so the return string will be the answer, if this is a formula or function. *@throws ParseException, ClassNotFoundException */ public static String getQuickValue(String formula, String variables) throws ParseException, ClassNotFoundException{ FormulaVariable variable=solveQuickFormula(formula, variables); String value=""; if(variable!=null&&variable.isNull()==false)value=variable.getString(); return value; } /** *A static method to Quickly get the value from a formula. You don't have to instantiate this class to get the result. * This method should not be used when the same formulas or variables are used multiple times within a loop since this has to parse everything every time. *@param formula the formula that requires solving. *@param variables an optional semi-colon separated list of variables, where the variables structured as name, equal symbol, value. Example: getQuickValueNoErrors("A+B","A=1;B=2")=="3"; *@return the result. solve(String variableName) is used to get the result so the return string will be the answer, if this is a formula or function. */ public static String getQuickValueNoErrors(String formula, String variables){ String value=""; try{ FormulaVariable variable=solveQuickFormula(formula, variables); if(variable!=null&&variable.isNull()==false)value=variable.getString(); }catch(Exception e){} return value; } }