package com.llamacorp.equate;
import com.llamacorp.equate.unit.Unit;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Solver {
//error messages
public static final String strSyntaxError = "Syntax Error";
public static final String strDivideZeroError = "Divide By Zero Error";
public static final String strInfinityError = "Number Too Large";
//we want the display precision to be a bit less than calculated
private MathContext mMcOperate;
Solver(int solvePrecision) {
if (solvePrecision > 0)
mMcOperate = new MathContext(solvePrecision);
}
Result tryToggleSciNote(Expression exp, boolean forceEngineering) {
//only proceed if only a number is in the expression
if (exp.isOnlyValidNumber())
return null;
//get and save query before operating on it
String query = exp.toString();
//if we want engineering, just convert regardless if we have E already
if (forceEngineering){
exp.roundAndCleanExpression(Expression.NumFormat.ENGINEERING);
}
//determine if we are are in sci notation already
else if (exp.isSciNotation())
exp.roundAndCleanExpression(Expression.NumFormat.PLAIN);
else
exp.roundAndCleanExpression(Expression.NumFormat.SCI_NOTE);
return new Result(query, exp.toString());
}
/**
* Solves a given Expression
* Cleans off the expression, adds missing parentheses, then loads in more
* accurate result values if possible into expression.
* Iterates over expression using PEMAS order of operations
*
* @param exp is the Expression to solve
* @return the expression before conversion (potentially used for result list)
*/
Result solve(Expression exp, Expression.NumFormat numFormat) {
//clean off any dangling operators and E's (not parentheses!!)
exp.cleanDanglingOps();
//if more open parentheses then close, add corresponding close para's
exp.closeOpenPar();
//save away query before we start manipulating it
String cleanedQuery = exp.toString();
//add implied multiplies for display purposes
cleanedQuery = Expression.addImpliedParMult(cleanedQuery);
//if expression empty|invalid, don't need to solve anything
if (exp.isEmpty())
return null;
//load in the precise result if possible
exp.loadPreciseResult();
//deal with percent operators
String strExp = Expression.replacePercentOps(exp.toString());
//put parenthesis around ^ expressions; -(1)^2 to -((1)^2)
strExp = Expression.groupPowerOperands(strExp);
//add implied multiples for parenthesis
strExp = Expression.addImpliedParMult(strExp);
//main calculation: first the P of PEMAS, this function then calls remaining EMAS
strExp = collapsePara(strExp);
//save solved expression away
exp.replaceExpression(strExp);
roundAndClean(exp, numFormat);
//flag used to tell backspace and numbers to clear the expression when pressed
exp.setSolved(true);
return new Result(cleanedQuery, exp.toString());
}
/**
* Function used to convert from one unit to another
*
* @param fromUnit is unit being converted from
* @param toUnit is unit being converted to
*/
void convertFromTo(Unit fromUnit, Unit toUnit, Expression exp) {
String toSolve = fromUnit.convertTo(toUnit, exp.getPreciseResult());
exp.replaceExpression(toSolve);
solve(exp, Expression.NumFormat.NORMAL);
}
/**
* Recursively loop over all parentheses, invoke other operators in results found within
*
* @param str is the String to loop the parentheses solving over
*/
private String collapsePara(String str) {
//find the first open parentheses
int firstPara = str.indexOf("(");
//if no open parentheses exists, move on
if (firstPara != -1){
//loop over all parentheses
int matchingPara = Expression.findMatchingClosePara(str, firstPara);
//we didn't find the matching parentheses put up syntax error and quit
if (matchingPara == -1){
str = strSyntaxError;
return str;
} else {
//this is the section before any parentheses, aka "25+" in "25+(9)", or just "" if "(9)"
String firstSection = str.substring(0, firstPara);
//this is the inside of the outermost parentheses set, recurse over inside to find more parentheses
String middleSection = collapsePara(str.substring(firstPara + 1, matchingPara));
//this is after the close of the outermost found set, might be lots of operators/numbers or ""
String endSection = str.substring(matchingPara + 1, str.length());
//all parentheses found, splice string back together
str = collapsePara(firstSection + middleSection + endSection);
}
}
//perform other operations in proper order of operations
str = collapseOps(Expression.regexGroupedExponent, str);
str = collapseOps(Expression.regexGroupedMultiDiv, str);
str = collapseOps(Expression.regexGroupedAddSub, str);
return str;
}
/**
* Loop over/collapse down input str, solves for either +- or /*. places result in expression
*
* @param regexOperatorType is the type of operators to look for in regex form
* @param str is the string to operate upon
*/
private String collapseOps(String regexOperatorType, String str) {
//find the first instance of operator in the str (we want left to right per order of operations)
Pattern ptn = Pattern.compile(Expression.regexGroupedNonNegNumber + regexOperatorType + Expression.regexGroupedNumber);
Matcher mat = ptn.matcher(str);
BigDecimal result;
//this loop will loop through each occurrence of the "# op #" sequence
while (mat.find()) {
BigDecimal operand1;
BigDecimal operand2;
String operator;
//be sure string is formatted properly
try {
operand1 = new BigDecimal(mat.group(1));
operand2 = new BigDecimal(mat.group(Expression.numGroupsInRegexGroupedNumber + 2));
operator = mat.group(Expression.numGroupsInRegexGroupedNumber + 1);
} catch (NumberFormatException e) {
//throw syntax error if we have a weirdly formatted string
str = strSyntaxError;
return str;
}
//perform actual operation on found operator and operands
if (operator.equals("+")){
//crude fix for 1E999999+1, which hangs the app. Could be handled better with real infinity...
if (operand1.scale() < -9000 || operand2.scale() < -9000)
return strInfinityError;
result = operand1.add(operand2, mMcOperate);
} else if (operator.equals("-")){
if (operand1.scale() < -9000 || operand2.scale() < -9000)
return strInfinityError;
result = operand1.subtract(operand2, mMcOperate);
} else if (operator.equals("*"))
result = operand1.multiply(operand2, mMcOperate);
else if (operator.equals("^")){
//this is a temp hack, will most likely want to use a custom bigdecimal function to perform more accurate/bigger conversions
double dResult = Math.pow(operand1.doubleValue(), operand2.doubleValue());
//catch infinity errors could be neg or pos
try {
result = BigDecimal.valueOf(dResult);
} catch (NumberFormatException ex) {
if (dResult == Double.POSITIVE_INFINITY || dResult == Double.NEGATIVE_INFINITY)
str = strInfinityError;
//else case most likely shouldn't occur
else
str = strSyntaxError;
return str;
}
} else if (operator.equals("/")){
//catch divide by zero errors
try {
result = operand1.divide(operand2, mMcOperate);
} catch (ArithmeticException ex) {
str = strDivideZeroError;
return str;
}
} else
throw new IllegalArgumentException("In collapseOps, invalid operator...");
//save cut out the old str and save in the result
str = str.substring(0, mat.start()) + result + str.substring(mat.end());
//reset the matcher with a our new str
mat = ptn.matcher(str);
}
return str;
}
private void roundAndClean(Expression exp, Expression.NumFormat numFormat) {
//rounding operation may throw NumberFormatException
try {
exp.roundAndCleanExpression(numFormat);
} catch (NumberFormatException e) {
exp.replaceExpression(strSyntaxError);
}
}
}