/*
* OpenClinica is distributed under the
* GNU Lesser General Public License (GNU LGPL).
* For details see: http://www.openclinica.org/license
* copyright 2003-2005 Akaza Research
*/
package org.akaza.openclinica.logic.score;
import org.akaza.openclinica.exception.ScoreException;
import org.akaza.openclinica.logic.score.function.Function;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Stack;
/**
* Util class for scoring feature. It contains expression evaluation methods.
* Expression should pass ScoreValidator before evaluation.
*
* @author ywang (Jan. 2008)
*
*/
public class ScoreUtil {
protected static final Logger logger = LoggerFactory.getLogger(ScoreUtil.class.getName());
private static final String FUNCTION_PACKAGE = "org.akaza.openclinica.logic.score.function";
/**
* Evaluation math expression which might contain functions.
* <p>
* Some pre-conditions:
* <ul>
* <li>Supported operators include only '+', '-', '*', '/'
* <li>Math expression should pass ScoreValidator before evaluation.
* </ul>
*
* @param expression
* @return String
*/
public static String eval(ArrayList<ScoreToken> expression) throws ScoreException {
if (expression == null || expression.size() < 1) {
return "";
}
ScoreToken token = new ScoreToken();
ArrayList<ScoreToken> finalexp = new ArrayList<ScoreToken>();
String value = "";
Info info = new Info();
info.pos = 0;
info.level = 0;
boolean couldBeSign = true;
while (info.pos < expression.size()) {
ScoreToken t = new ScoreToken();
t = expression.get(info.pos);
char c = t.getSymbol();
// ignore spaces
if (c == ' ') {
// do nothing
} else if (c == ScoreSymbol.ARITHMETIC_OPERATOR_SYMBOL) {
if (couldBeSign & isSign(t.getName())) {
if (token.getName().length() > 0) {
logger.info("Wrong at operator " + t.getName() + " at position " + info.pos);
throw new ScoreException(t.getName() + " at position " + info.pos + " is invalid.", "1");
} else {
token.setName(t.getName());
token.setSymbol(t.getSymbol());
}
couldBeSign = false;
} else {
if (token.getName().length() > 0) {
finalexp.add(token);
token = new ScoreToken();
}
finalexp.add(t);
couldBeSign = true;
}
} else if (c == '(') {
couldBeSign = true;
String sign = "";
String tokenname = token.getName();
if (tokenname.length() > 0 && isSign(tokenname.charAt(0))) {
sign = tokenname.charAt(0) + "";
tokenname = tokenname.substring(1);
}
String funcname = getFunctionName(tokenname);
if (funcname != null && !funcname.equalsIgnoreCase("getexternalvalue") && !funcname.equalsIgnoreCase(FUNCTION_PACKAGE + "getexternalvalue")) {
try {
token.setName(sign + evalFunc(expression, info, (Function) Class.forName(funcname).newInstance()));
token.setSymbol(ScoreSymbol.TERM_SYMBOL);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
finalexp.add(token);
token = new ScoreToken();
couldBeSign = false;
} else {
info.level++;
if (token.getName().length() > 0) {
finalexp.add(token);
}
token = new ScoreToken();
finalexp.add(t);
}
} else if (c == ')') {
couldBeSign = false;
if (token.getName().length() > 0) {
finalexp.add(token);
}
token = new ScoreToken();
finalexp.add(t);
info.level--;
}
// This should not happen since it is automatically handled by
// getFunctionValue()
// So, if this happens, either the expression or unlikely code has a
// problem.
else if (c == ',') {
token = new ScoreToken();
throw new ScoreException("Found unexpected character , when doing evaluation", "2");
} else {
couldBeSign = false;
if (isSign(token.getName())) {
token.setName(token.getName() + t.getName());
token.setSymbol(ScoreSymbol.TERM_SYMBOL);
} else {
token.setName(t.getName());
token.setSymbol(t.getSymbol());
}
}
info.pos++;
}
if (info.level != 0) {
logger.info("expression invalid, unpaired parentheses."); // !!!!!
throw new ScoreException("Found unpaired parentheses when doing evaluation", "3");
}
// There might be a last token which should be added to the final
// expression.
// For example, to expression 2+4, we must do so.
if (token.getName().length() > 0) {
finalexp.add(token);
}
if (finalexp != null && finalexp.size() > 0) {
if (finalexp.size() == 1) {
value = finalexp.get(0).getName();
} else {
value = evalSimple(createPostfix(finalexp));
}
}
return "" + value;
}
/**
* Evaluate a function which might contain arithmetic expressions, and
* return result as a String.
* <p>
* If an item can not be found in the eventCRF, it will be treated as empty.
* If empty items exist in a function, the result will be empty.
*
* @param contents
* @param info
* @param function
* @return
*/
public static String evalFunc(ArrayList<ScoreToken> contents, Info info, Function function) throws ScoreException {
int originalLevel = info.level;
info.pos++;
info.level++;
ScoreToken token = new ScoreToken();
// currArg is in fact representing the current argument.
ArrayList<ScoreToken> currArg = new ArrayList<ScoreToken>();
boolean couldBeSign = true;
while (info.pos < contents.size()) {
ScoreToken scoretoken = contents.get(info.pos);
char c = scoretoken.getSymbol();
if (c == ')') {
couldBeSign = false;
info.level--;
// end of the function, marked by the equal level
if (info.level == originalLevel) {
if (token.getName().length() > 0) {
currArg.add(token);
}
String t = evalArgument(currArg);
if (t != null && t.length() > 0) {
function.addArgument(t);
} else {
// error message has been handled in evalArgument()
return "";
}
token = new ScoreToken();
break;
} else {
// end of an expression, just store them in the current
// argument
if (token.getName().length() > 0) {
currArg.add(token);
}
currArg.add(scoretoken);
}
token = new ScoreToken();
} else if (c == '(') {
couldBeSign = true;
String sign = "";
String tokenname = token.getName();
if (tokenname.length() > 0 && isSign(tokenname.charAt(0))) {
sign = tokenname.charAt(0) + "";
tokenname = tokenname.substring(1);
}
// it is either the start of a function or an expression
String funcname = getFunctionName(tokenname);
if (funcname != null) {
// store in the current argument
try {
token.setName(sign + evalFunc(contents, info, (Function) Class.forName(funcname).newInstance()));
token.setSymbol(ScoreSymbol.TERM_SYMBOL);
currArg.add(token);
couldBeSign = false;
} catch (InstantiationException e) {
e.printStackTrace();
return "";
} catch (ClassNotFoundException e) {
e.printStackTrace();
return "";
} catch (IllegalAccessException e) {
e.printStackTrace();
return "";
}
}// if it is the start of an expression
else {
info.level++;
if (token.getName().length() > 0) {
currArg.add(token);
}
currArg.add(scoretoken);
}
token = new ScoreToken();
}// end of an argument
else if (c == ',') {
couldBeSign = true;
if (token.getName().length() > 0) {
currArg.add(token);
}
// compute the argument
String t = evalArgument(currArg);
if (t != null && t.length() > 0) {
function.addArgument(t);
} else {
return "";
}
token = new ScoreToken();
// reset the argument for next one
currArg = new ArrayList<ScoreToken>();
}
// else if(isOperator(c)){
else if (c == ScoreSymbol.ARITHMETIC_OPERATOR_SYMBOL) {
if (couldBeSign && isSign(scoretoken.getName())) {
if (token.getName().length() > 0) {
throw new ScoreException(scoretoken.getName() + " at position " + info.pos + " is invalid.", "1");
} else {
// token = scoretoken;
token.setName(scoretoken.getName());
token.setSymbol(scoretoken.getSymbol());
}
couldBeSign = false;
} else {
if (token.getName().length() > 0) {
currArg.add(token);
}
token = new ScoreToken();
currArg.add(scoretoken);
couldBeSign = true;
}
} else {
couldBeSign = false;
if (isSign(token.getName())) {
token.setName(token.getName() + scoretoken.getName());
token.setSymbol(ScoreSymbol.TERM_SYMBOL);
} else {
if (c != ' ') {
// token = scoretoken;
token.setName(scoretoken.getName());
token.setSymbol(scoretoken.getSymbol());
}
}
}
info.pos++;
}
function.execute();
if (function.getErrors().size() > 0) {
String errors = new String();
HashMap<Integer, String> es = function.getErrors();
for (int i = 0; i < es.size(); ++i) {
errors += es.get(Integer.valueOf(i));
}
throw new ScoreException(errors, "4");
}
return function.getValue();
}
/**
* Evaluate argument of a function. Argument could be an expression.
*
* @param arg
* @return
*/
public static String evalArgument(ArrayList<ScoreToken> arg) {
String v = "";
if (arg != null && arg.size() > 0) {
try {
if (arg.size() == 1) {
v = arg.get(0).getName();
} else {
v = evalSimple(createPostfix(arg));
}
} catch (Exception e) {
try {
v = eval(arg);
} catch (ScoreException sc) {
sc.printStackTrace();
}
}
}
return v;
}
/**
* Create postfix(ArrayList<ScoreToken>) for an expression(ArrayList<ScoreToken>).
* This method only handles (, ), sign(ie, + -), arithmethic operators (ie, + * - /)
* and numbers.
*
* @param exp
* @return
*/
public static ArrayList<ScoreToken> createPostfix(ArrayList<ScoreToken> exp) {
if (exp.size() < 3) {
return exp;
}
Stack<ScoreToken> temp = new Stack<ScoreToken>();
ArrayList<ScoreToken> post = new ArrayList<ScoreToken>();
for (int i = 0; i < exp.size(); ++i) {
if (exp.get(i).getName().equals("(")) {
temp.push(exp.get(i));
} else if (exp.get(i).getName().equals(")")) {
while (!temp.isEmpty()) {
ScoreToken s = temp.pop();
if (!s.getName().equals("(")) {
post.add(s);
} else {
break;
}
}
} else if (isOperator(exp.get(i).getName())) {
boolean finished = false;
while (!temp.isEmpty() && !finished) {
ScoreToken s = temp.pop();
if (isOperator(s.getName())) {
if (getPriority(s.getName()) >= getPriority(exp.get(i).getName())) {
post.add(s);
} else {
temp.push(s);
finished = true;
}
} else {
temp.push(s);
finished = true;
}
}
temp.push(exp.get(i));
} else {
post.add(exp.get(i));
}
}
while (!temp.isEmpty()) {
post.add(temp.pop());
}
return post;
}
/**
* Evaluates + * - / using postfix algorithm and return a String. If the
* parameter exp size is 1, the first and the only element of exp will be
* returned. If exp size > 1 and contains non-number elements, empty string
* will be returned.
*
* @param exp
* ArrayList<ScoreToken> should be postfix of an expression.
* @param errors
* @return
*/
// public static String evalSimple(ArrayList<ScoreToken> exp, StringBuffer
// errors) {
public static String evalSimple(ArrayList<ScoreToken> exp) {
String stringValue = "";
double value = Double.NaN;
Stack<Double> st = new Stack<Double>();
int size = exp.size();
if (size == 1) {
try {
value = Double.valueOf(exp.get(0).getName());
} catch (Exception e) {
// for function like decode whose argument might be a String
stringValue = exp.get(0).getName();
}
} else if (size > 2) {
for (int i = 0; i < size; ++i) {
String s = exp.get(i).getName();
if (isOperator(s)) {
double second = st.pop();
double first = st.pop();
try {
if (s.equals("+")) {
value = first + second;
} else if (s.equals("-")) {
value = first - second;
} else if (s.equals("*")) {
value = first * second;
} else if (s.equals("/")) {
value = first / second;
}
} catch (Exception ee) {
ee.printStackTrace();
value = Double.NaN;
}
st.push(value);
} else {
double d = Double.NaN;
try {
d = Double.valueOf(exp.get(i).getName());
} catch (Exception e) {
e.printStackTrace();
return exp.get(i).getName();
}
st.push(d);
}
}
}
stringValue = stringValue.equalsIgnoreCase("") ? ((value + "").equalsIgnoreCase("NaN") ? "" : value + "") : stringValue;
return stringValue;
}
/**
* Return true if one character matches one of those characters '+', '-',
* '*', '/'
*
* @param ch
* @return
*/
public static boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
private static boolean isOperator(String s) {
return s.equals("+") || s.equals("-") || s.equals("*") || s.equals("/");
}
private static boolean isSign(String s) {
return s.equals("+") || s.equals("-");
}
private static boolean isSign(char c) {
return c == '+' || c == '-';
}
public static String getFunctionName(String token) {
return Parser.convertToClassName(FUNCTION_PACKAGE, token);
}
/*
* Only handled + * - /
*/
protected static byte getPriority(String operator) {
byte p = 0;
if (operator.equals("+") || operator.equals("-")) {
p = 0;
} else if (operator.equals("*") || operator.equals("/")) {
p = 1;
} else {
p = -1;
}
return p;
}
static class Info {
int level = 0;
int pos = 0;
}
}