/*
* 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
*
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;
}
}