/** * Copyright (C) 2001-2017 by RapidMiner and the contributors * * Complete list of developers available at our web site: * * http://rapidminer.com * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see http://www.gnu.org/licenses/. */ package com.rapidminer.tools.expression.internal.function.eval; import java.util.Date; import java.util.concurrent.Callable; import com.rapidminer.tools.Ontology; import com.rapidminer.tools.expression.DoubleCallable; import com.rapidminer.tools.expression.ExpressionEvaluator; import com.rapidminer.tools.expression.ExpressionException; import com.rapidminer.tools.expression.ExpressionParsingException; import com.rapidminer.tools.expression.ExpressionType; import com.rapidminer.tools.expression.FunctionDescription; import com.rapidminer.tools.expression.FunctionInputException; import com.rapidminer.tools.expression.internal.SimpleExpressionEvaluator; import com.rapidminer.tools.expression.internal.antlr.AntlrParser; import com.rapidminer.tools.expression.internal.function.AbstractFunction; /** * A {@link Function} that evaluates subexpressions using an {@link AntlrParser}. * <p> * If the first input of the eval function is constant, the parser is only called once during the * callable-creation step and not again when the resulting {@link Expression} is evaluated. This * means that the evaluation of the {@link Expressions} generated from {@code eval("4+3")*[att1]}, * {@code eval("(4+3)*[att1]")} and {@code 7*[att1]} have the same complexity at evaluation time. * <p> * If the first argument is not constant, then a second argument is needed to determine the result * type and the parser is called every time the resulting {@link Expression} is evaluated. In * particular, evaluating {@link Expression}s such as {@code eval("(4+3)*"+[att1],REAL)} is way * slower than the examples above. * * @author Gisa Schaefer * */ public class Evaluation extends AbstractFunction { private AntlrParser parser; /** * Creates a evaluation {@link Function}. Before this functions * {@link #compute(ExpressionEvaluator...)} method can be called, a parser needs to be set via * {@link #setParser(AntlrParser)}. */ public Evaluation() { super("process.eval", FunctionDescription.UNFIXED_NUMBER_OF_ARGUMENTS, Ontology.ATTRIBUTE_VALUE); } /** * Sets the parser that this evaluation function should use. This must always be done before * using {@link #compute(ExpressionEvaluator...)}. * * @param parser * the parser to use */ public void setParser(AntlrParser parser) { this.parser = parser; } @Override public ExpressionEvaluator compute(final ExpressionEvaluator... inputEvaluators) { if (parser == null) { throw new IllegalStateException("parser must be set in order to evaluate"); } // check for string inputs getResultType(inputEvaluators); // Note that if inputEvaluators[0] is constant then this does not mean the result of eval is // constant, it only means that the return type is constant. Example: 'eval("attribute_1")' // has the constant string input "attribute_1", but will result in a non-constant evaluator // that returns the different values of the attribute. if (inputEvaluators.length == 1) { if (!inputEvaluators[0].isConstant()) { throw new FunctionInputException("expression_parser.eval.non_constant_single_argument", getFunctionName()); } // if eval has one constant argument compute it once using the parser return compute(inputEvaluators[0]); } else if (inputEvaluators.length == 2) { if (inputEvaluators[0].isConstant()) { // if eval has one constant argument and one type argument compute result of first // argument once using the parser and check the type return computeAndCheckType(inputEvaluators[0], getExpectedReturnType(inputEvaluators[1])); } else { // if eval has one non-constant argument and one type argument create callables // depending on the type that do the same in the case above final ExpressionType expectedType = getExpectedReturnType(inputEvaluators[1]); switch (expectedType) { case DATE: return makeDateEvaluator(expectedType, inputEvaluators[0]); case DOUBLE: case INTEGER: return makeDoubleEvaluator(expectedType, inputEvaluators[0]); case BOOLEAN: return makeBooleanEvaluator(expectedType, inputEvaluators[0]); case STRING: default: return makeStringEvaluator(expectedType, inputEvaluators[0]); } } } else { throw new FunctionInputException("expression_parser.function_wrong_input_two", getFunctionName(), 1, 2, inputEvaluators.length); } } @Override protected ExpressionType computeType(ExpressionType... inputTypes) { // check if inputs are strings, otherwise throw exception for (ExpressionType type : inputTypes) { if (!(type == ExpressionType.STRING)) { throw new FunctionInputException("expression_parser.function_wrong_type", getFunctionName(), "nominal"); } } // is not used to compute the resultType, only used to check the input types return null; } /** * Evaluates the expression into a String and feeds it to the parser. * * @param subexpressionEvaluator * the evaluator whose string function call yields the expression string * @return the result of parsing the expression string */ private ExpressionEvaluator compute(ExpressionEvaluator subexpressionEvaluator) { String expressionString = null; try { expressionString = subexpressionEvaluator.getStringFunction().call(); } catch (ExpressionParsingException e) { throw e; } catch (Exception e) { throw new ExpressionParsingException(e); } // if subexpression is a missing nominal don't feed it into the parser if (expressionString == null) { return new SimpleExpressionEvaluator((String) null, ExpressionType.STRING); } try { return parser.parseToEvaluator(expressionString); } catch (ExpressionParsingException | ExpressionException e) { throw new SubexpressionEvaluationException(getFunctionName(), expressionString, e); } } /** * Evaluates the expression into a String, feeds it to the parser and checks if the resulting * type is the expected type. If the resulting type is not as expected and the expected type is * String then converts the result to a string evaluator. If the expected type is double and the * result type is integer the result type is changed to double. * * @param subexpressionEvaluator * the evaluator whose string function call yields the expression string * @param expectedType * the expected type of the result * @return the result of parsing the expression string */ private ExpressionEvaluator computeAndCheckType(ExpressionEvaluator subexpressionEvaluator, ExpressionType expectedType) { ExpressionEvaluator outEvaluator = compute(subexpressionEvaluator); if (outEvaluator.getType() == expectedType) { return outEvaluator; } else if (expectedType == ExpressionType.DOUBLE && outEvaluator.getType() == ExpressionType.INTEGER) { // use same resulting evaluator but with different type return new SimpleExpressionEvaluator(outEvaluator.getDoubleFunction(), expectedType, outEvaluator.isConstant()); } else if (expectedType == ExpressionType.STRING) { return convertToStringEvaluator(outEvaluator); } else { throw new FunctionInputException("expression_parser.eval.type_not_matching", getFunctionName(), getConstantName(expectedType), getConstantName(outEvaluator.getType())); } } /** * Converts the outEvaluator into an {@link ExpressionEvaluator} of type string. If outEvaluator * is constant the result is also constant. * * @param outEvaluator * a evaluator which is not of type String * @return an {@link ExpressionEvaluator} of type String */ private ExpressionEvaluator convertToStringEvaluator(final ExpressionEvaluator outEvaluator) { if (outEvaluator.isConstant()) { try { String stringValue = getStringValue(outEvaluator); return new SimpleExpressionEvaluator(stringValue, ExpressionType.STRING); } catch (ExpressionParsingException e) { throw e; } catch (Exception e) { throw new ExpressionParsingException(e); } } else { Callable<String> stringCallable = new Callable<String>() { @Override public String call() throws Exception { return getStringValue(outEvaluator); } }; return new SimpleExpressionEvaluator(stringCallable, ExpressionType.STRING, false); } } /** * Calculates the String value that the outEvaluator should return. */ private String getStringValue(ExpressionEvaluator outEvaluator) throws Exception { switch (outEvaluator.getType()) { case DOUBLE: case INTEGER: return doubleToString(outEvaluator.getDoubleFunction().call(), outEvaluator.getType() == ExpressionType.INTEGER); case BOOLEAN: return booleanToString(outEvaluator.getBooleanFunction().call()); case DATE: return dateToString(outEvaluator.getDateFunction().call()); default: // cannot happen return null; } } /** * Converts the input to a string with special missing value handling */ private String dateToString(Date input) { if (input == null) { return null; } else { return input.toString(); } } /** * Converts the input to a string with special missing value handling. */ private String booleanToString(Boolean input) { if (input == null) { return null; } else { return input.toString(); } } /** * Converts the input to a string with special missing value handling and integers represented * as integers if possible. */ private String doubleToString(double input, boolean isInteger) { if (Double.isNaN(input)) { return null; } if (isInteger && input == (int) input) { return "" + (int) input; } else { return "" + input; } } /** * Converts the ExpressionType to the name of the constant that the user should use to mark this * type. * * @param type * an {@link ExpressionType} * @return the string name of the constant associated to this type */ private String getConstantName(ExpressionType type) { return TypeConstants.INSTANCE.getNameForType(type); } /** * Converts the type constant passed by the user to an {@link ExpressionType}. * * @param expressionEvaluator * the evaluator holding the type constant. * @return */ private ExpressionType getExpectedReturnType(ExpressionEvaluator expressionEvaluator) { if (!expressionEvaluator.isConstant()) { String validTypeArguments = TypeConstants.INSTANCE.getValidConstantsString(); throw new FunctionInputException("expression_parser.eval.type_not_constant", getFunctionName(), validTypeArguments); } String typeString = null; try { typeString = expressionEvaluator.getStringFunction().call(); } catch (ExpressionParsingException e) { throw e; } catch (Exception e) { throw new ExpressionParsingException(e); } ExpressionType expectedType = TypeConstants.INSTANCE.getTypeForName(typeString); if (expectedType == null) { String validTypeArguments = TypeConstants.INSTANCE.getValidConstantsString(); throw new FunctionInputException("expression_parser.eval.invalid_type", typeString, getFunctionName(), validTypeArguments); } return expectedType; } /** * Creates an {@link ExpressionEvaluator} with a date callable that calls * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. */ private ExpressionEvaluator makeDateEvaluator(final ExpressionType expectedType, final ExpressionEvaluator inputEvaluator) { Callable<Date> dateCallable = new Callable<Date>() { @Override public Date call() throws Exception { ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); return subExpressionEvaluator.getDateFunction().call(); } }; return new SimpleExpressionEvaluator(expectedType, dateCallable, false); } /** * Creates an {@link ExpressionEvaluator} with a boolean callable that calls * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. */ private ExpressionEvaluator makeBooleanEvaluator(final ExpressionType expectedType, final ExpressionEvaluator inputEvaluator) { Callable<Boolean> booleanCallable = new Callable<Boolean>() { @Override public Boolean call() throws Exception { ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); return subExpressionEvaluator.getBooleanFunction().call(); } }; return new SimpleExpressionEvaluator(booleanCallable, false, expectedType); } /** * Creates an {@link ExpressionEvaluator} with a double callable that calls * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. */ private ExpressionEvaluator makeDoubleEvaluator(final ExpressionType expectedType, final ExpressionEvaluator inputEvaluator) { DoubleCallable doubleCallable = new DoubleCallable() { @Override public double call() throws Exception { ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); return subExpressionEvaluator.getDoubleFunction().call(); } }; return new SimpleExpressionEvaluator(doubleCallable, expectedType, false); } /** * Creates an {@link ExpressionEvaluator} with a string callable that calls * {@link #computeAndCheckType(ExpressionEvaluator, ExpressionType)}. */ private ExpressionEvaluator makeStringEvaluator(final ExpressionType expectedType, final ExpressionEvaluator inputEvaluator) { Callable<String> stringCallable = new Callable<String>() { @Override public String call() throws Exception { ExpressionEvaluator subExpressionEvaluator = computeAndCheckType(inputEvaluator, expectedType); return subExpressionEvaluator.getStringFunction().call(); } }; return new SimpleExpressionEvaluator(stringCallable, expectedType, false); } }