/**
* 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.operator;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import com.rapidminer.MacroHandler;
import com.rapidminer.generator.GenerationException;
import com.rapidminer.operator.ports.DummyPortPairExtender;
import com.rapidminer.operator.ports.PortPairExtender;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeExpression;
import com.rapidminer.parameter.ParameterTypeList;
import com.rapidminer.parameter.ParameterTypeString;
import com.rapidminer.tools.Tools;
import com.rapidminer.tools.expression.Expression;
import com.rapidminer.tools.expression.ExpressionException;
import com.rapidminer.tools.expression.ExpressionParser;
import com.rapidminer.tools.expression.ExpressionType;
import com.rapidminer.tools.expression.UnknownValue;
import com.rapidminer.tools.expression.internal.ExpressionParserUtils;
/**
* <p>
* This operator constructs new macros from expressions which might also use already existing
* macros. The names of the new macros and their construction description are defined in the
* parameter list "functions".
* </p>
*
* <p>
* The following <em>operators</em> are supported:
* <ul>
* <li>Addition: +</li>
* <li>Subtraction: -</li>
* <li>Multiplication: *</li>
* <li>Division: /</li>
* <li>Power: ^</li>
* <li>Modulus: %</li>
* <li>Less Than: <</li>
* <li>Greater Than: ></li>
* <li>Less or Equal: <=</li>
* <li>More or Equal: >=</li>
* <li>Equal: ==</li>
* <li>Not Equal: !=</li>
* <li>Boolean Not: !</li>
* <li>Boolean And: two ampers and</li>
* <li>Boolean Or: ||</li>
* </ul>
* </p>
*
* <p>
* The following <em>log and exponential functions</em> are supported:
* <ul>
* <li>Natural Logarithm: ln(x)</li>
* <li>Logarithm Base 10: log(x)</li>
* <li>Logarithm Dualis (Base 2): ld(x)</li>
* <li>Exponential (e^x): exp(x)</li>
* <li>Power: pow(x,y)</li>
* </ul>
* </p>
*
* <p>
* The following <em>trigonometric functions</em> are supported:
* <ul>
* <li>Sine: sin(x)</li>
* <li>Cosine: cos(x)</li>
* <li>Tangent: tan(x)</li>
* <li>Arc Sine: asin(x)</li>
* <li>Arc Cosine: acos(x)</li>
* <li>Arc Tangent: atan(x)</li>
* <li>Arc Tangent (with 2 parameters): atan2(x,y)</li>
* <li>Hyperbolic Sine: sinh(x)</li>
* <li>Hyperbolic Cosine: cosh(x)</li>
* <li>Hyperbolic Tangent: tanh(x)</li>
* <li>Inverse Hyperbolic Sine: asinh(x)</li>
* <li>Inverse Hyperbolic Cosine: acosh(x)</li>
* <li>Inverse Hyperbolic Tangent: atanh(x)</li>
* </ul>
* </p>
*
* <p>
* The following <em>statistical functions</em> are supported:
* <ul>
* <li>Round: round(x)</li>
* <li>Round to p decimals: round(x,p)</li>
* <li>Floor: floor(x)</li>
* <li>Ceiling: ceil(x)</li>
* </ul>
* </p>
*
* <p>
* The following <em>miscellaneous functions</em> are supported:
* <ul>
* <li>Average: avg(x,y,z...)</li>
* <li>Minimum: min(x,y,z...)</li>
* <li>Maximum: max(x,y,z...)</li>
* </ul>
* </p>
*
* <p>
* The following <em>miscellaneous functions</em> are supported:
* <ul>
* <li>If-Then-Else: if(cond,true-evaluation, false-evaluation)</li>
* <li>Absolute: abs(x)</li>
* <li>Square Root: sqrt(x)</li>
* <li>Signum (delivers the sign of a number): sgn(x)</li>
* <li>Random Number (between 0 and 1): rand()</li>
* <li>Modulus (x % y): mod(x,y)</li>
* <li>Sum of k Numbers: sum(x,y,z...)</li>
* <li>Binomial Coefficients: binom(n, i)</li>
* <li>Number to String: str(x)</li>
* </ul>
* </p>
*
* <p>
* Beside those operators and functions, this operator also supports the constants pi and e. You can
* also use strings in formulas (for example in a conditioned if-formula) but the string values have
* to be enclosed in double quotes.
* </p>
*
* <p>
* Please note that there are some restrictions for the usage of other macros. The values of used
* macros have to fulfill the following in order to let this operator work properly:
* <ul>
* <li>If the standard constants are usable, macro values with names like "e" or
* "pi" are not allowed.</li>
* <li>Macro values with function or operator names are also not allowed.</li>
* <li>Macro values containing parentheses are not allowed.</li>
* </ul>
* </p>
*
* <p>
* <br/>
* <em>Examples:</em><br/>
* 17+sin(%{macro1}*%{macro2})<br/>
* if (%macro1}>5, %{macro2}*%{macro3}, -abs(%{macro4}))<br/>
* </p>
*
* @author Ingo Mierswa
*/
public class MacroConstructionOperator extends Operator {
/** The parameter name for "List of functions to generate." */
public static final String PARAMETER_FUNCTIONS = "function_descriptions";
private PortPairExtender dummyPorts = new DummyPortPairExtender("through", getInputPorts(), getOutputPorts());
private static final OperatorVersion FAILING_AT_WRONG_EXPRESSIONS = new OperatorVersion(6, 0, 2);
private static final OperatorVersion NUMBERFORMAT_WITHOUT_ZEROS = new OperatorVersion(6, 0, 3);
public MacroConstructionOperator(OperatorDescription description) {
super(description);
dummyPorts.start();
getTransformer().addRule(dummyPorts.makePassThroughRule());
}
@Override
public void doWork() throws OperatorException {
ExpressionParser parser = ExpressionParserUtils.createAllModulesParser(this, null);
Iterator<String[]> j = getParameterList(PARAMETER_FUNCTIONS).iterator();
while (j.hasNext()) {
String[] nameFunctionPair = j.next();
String name = nameFunctionPair[0];
String function = nameFunctionPair[1];
try {
addMacro(getProcess().getMacroHandler(), name, function,
this.getCompatibilityLevel().isAtMost(NUMBERFORMAT_WITHOUT_ZEROS), parser);
} catch (ExpressionException e) {
if (!getCompatibilityLevel().isAtMost(FAILING_AT_WRONG_EXPRESSIONS)) {
throw ExpressionParserUtils.convertToUserError(this, function, e);
} else {
// do nothing
}
}
}
dummyPorts.passDataThrough();
}
/**
* This method allows to derive a value from the given function and store it as a macro in the
* macroHandler under the given name.
*
* @param enforceNumberFormat
* if false the number will be returned the way they were entered. Else the method
* will try to enforce the user specified number of decimal places.
* @throws GenerationException
*
*/
private void addMacro(MacroHandler macroHandler, String name, String expression, boolean enforceNumberFormat,
ExpressionParser parser) throws ExpressionException {
Expression exp = parser.parse(expression);
ExpressionType type = exp.getExpressionType();
// set result as macro
if (type == ExpressionType.DATE) {
Date date = exp.evaluateDate();
if (date != null) {
macroHandler.addMacro(name, Tools.formatDateTime(date));
} else {
macroHandler.addMacro(name, null);
}
} else {
Object result = exp.evaluate();
if (result instanceof UnknownValue) {
macroHandler.addMacro(name, null);
} else if (result instanceof Number && Double.isNaN(((Number) result).doubleValue())) {
macroHandler.addMacro(name, null);
} else {
try {
if (enforceNumberFormat) {
macroHandler.addMacro(name, Tools.formatIntegerIfPossible(Double.parseDouble(result.toString())));
} else if (type == ExpressionType.INTEGER) {
// if the result type is integer and the result is an integer, format
// accordingly
double doubleValue = ((Double) result).doubleValue();
int intValue = ((Double) result).intValue();
macroHandler
.addMacro(name, doubleValue == intValue ? Integer.toString(intValue) : result.toString());
} else {
macroHandler.addMacro(name, result.toString());
}
} catch (NumberFormatException e) {
macroHandler.addMacro(name, result.toString());
}
}
}
}
@Override
public List<ParameterType> getParameterTypes() {
List<ParameterType> types = super.getParameterTypes();
ParameterType type = new ParameterTypeList(PARAMETER_FUNCTIONS,
"The list of macro names together with the expressions which define the new macros",
new ParameterTypeString("macro_name", "The name of the constructed macro."), new ParameterTypeExpression(
"functions_expressions", "The expressions which define the new macros.", getInputPorts()
.getPortByIndex(0), false));
type.setExpert(false);
types.add(type);
return types;
}
@Override
public OperatorVersion[] getIncompatibleVersionChanges() {
OperatorVersion[] incompatibleVersions = super.getIncompatibleVersionChanges();
OperatorVersion[] extendedIncompatibleVersions = Arrays
.copyOf(incompatibleVersions, incompatibleVersions.length + 2);
extendedIncompatibleVersions[incompatibleVersions.length] = FAILING_AT_WRONG_EXPRESSIONS;
extendedIncompatibleVersions[incompatibleVersions.length + 1] = NUMBERFORMAT_WITHOUT_ZEROS;
// add expression parser version change to allow usage of old functions
// and old macro handling
extendedIncompatibleVersions = ExpressionParserUtils
.addIncompatibleExpressionParserChange(extendedIncompatibleVersions);
return extendedIncompatibleVersions;
}
}