/**
* Copyright 2016 StreamSets Inc.
*
* Licensed under the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.streamsets.pipeline.lib.el;
import com.streamsets.pipeline.api.ElFunction;
import com.streamsets.pipeline.api.ElParam;
import com.streamsets.pipeline.api.impl.Utils;
/**
* Encapsulated methods available in java.lang.Math
*/
public class MathEL {
// Various types that the Math class supports
enum GivenType {
DOUBLE,
FLOAT,
INT,
LONG,
}
/**
* Abstract operation that encapsulates single operation like abs, just executed for all supported data types. We're
* not converting the incoming argument to a single shared type as we want to retain the type information. E.g. if
* user called math:abs(int) result will be int, if it was called as math:abs(double) the result will be double. The
* only exception is a String input that is converted to a number in all cases (either to double if it contains dot
* or to long if not). Any advanced smartness will require user to cast their types properly.
*/
private static abstract class Operation {
String operationName;
public Operation(String name) {
this.operationName = name;
}
public Object process(Object ...params) {
if(params == null || params.length < 1 || params.length > 2) {
throw new IllegalArgumentException(Utils.format("Invalid number of arguments for {}", operationName));
}
// There always have to be at least one argument
Object param = params[0];
if(param == null) {
return null;
}
if(param instanceof String) {
param = convertStringToAppropriateNumber((String) param);
}
// Second argument might be missing
Object secondParam = null;
if(params.length > 1) {
secondParam = params[1];
if(secondParam == null) {
return null;
}
if(secondParam instanceof String) {
secondParam = convertStringToAppropriateNumber((String) secondParam);
}
if(param.getClass() != secondParam.getClass()) {
throw new IllegalArgumentException(Utils.format("Arguments for operation {} doesn't have the same type.", operationName));
}
}
// Detect type, so that parent classes don't have to do that
GivenType type;
if (param.getClass() == Double.class) {
type = GivenType.DOUBLE;
} else if (param.getClass() == Float.class) {
type = GivenType.FLOAT;
} else if (param.getClass() == Integer.class) {
type = GivenType.INT;
} else if (param.getClass() == Long.class) {
type = GivenType.LONG;
} else {
throw new IllegalArgumentException(Utils.format("Unsupported data type {} for operation {} and value '{}'", param.getClass(), operationName, param));
}
return calculate(type, param, secondParam);
}
protected abstract Object calculate(GivenType type, Object param, Object secondParam);
}
/**
* We need to support Strings as some information that user might need to deal with is inherently stored in String
* variables - for example header values or CSV files.
*
* The way we're processing strings is - if it contains dot, then it's assumed to be a double otherwise long. If we for
* whatever reason fail to parse the number and error is thrown.
*/
private static Object convertStringToAppropriateNumber(String value) {
if(value.contains(".")) {
return Double.valueOf(value);
} else {
return Long.valueOf(value);
}
}
/*
* Math:abs
*/
private static Operation ABS = new Operation("abs") {
@Override
protected Object calculate(GivenType type, Object param, Object secondParam) {
switch (type) {
case DOUBLE: return Math.abs((double)param);
case FLOAT: return Math.abs((float)param);
case INT: return Math.abs((int)param);
case LONG: return Math.abs((long)param);
default:
throw new IllegalArgumentException(Utils.format("Unsupported data type {} for operation {} and value '{}'", param.getClass(), operationName, param));
}
}
};
@ElFunction(
prefix = "math",
name = "abs",
description = "Returns absolute value of the argument."
)
public static Object abs(@ElParam("number") Object number) {
return ABS.process(number);
}
/*
* Math:ceil
*/
private static Operation CEIL = new Operation("ceil") {
@Override
protected Object calculate(GivenType type, Object param, Object secondParam) {
switch (type) {
case DOUBLE:
case FLOAT:
return Math.ceil((double)param);
default:
throw new IllegalArgumentException(Utils.format("Unsupported data type {} for operation {} and value '{}'", param.getClass(), operationName, param));
}
}
};
@ElFunction(
prefix = "math",
name = "ceil",
description = "Returns the smallest (closest to negative infinity) double value that is greater than or equal to the argument and is equal to a mathematical integer."
)
public static Object ceil(@ElParam("number") Object number) {
return CEIL.process(number);
}
/*
* Math:floor
*/
private static Operation FLOOR = new Operation("floor") {
@Override
protected Object calculate(GivenType type, Object param, Object secondParam) {
switch (type) {
case DOUBLE:
case FLOAT:
return Math.floor((double)param);
default:
throw new IllegalArgumentException(Utils.format("Unsupported data type {} for operation {} and value '{}'", param.getClass(), operationName, param));
}
}
};
@ElFunction(
prefix = "math",
name = "floor",
description = "Returns the largest (closest to positive infinity) double value that is less than or equal to the argument and is equal to a mathematical integer."
)
public static Object floor(@ElParam("number") Object number) {
return FLOOR.process(number);
}
/*
* Math:max
*/
private static Operation MAX = new Operation("max") {
@Override
protected Object calculate(GivenType type, Object param, Object secondParam) {
switch (type) {
case DOUBLE: return Math.max((double)param, (double)secondParam);
case FLOAT: return Math.max((float)param, (double)secondParam);
case INT: return Math.max((int)param, (int)secondParam);
case LONG: return Math.max((long)param, (long)secondParam);
default:
throw new IllegalArgumentException(Utils.format("Unsupported data type {} for operation {} and value '{}'", param.getClass(), operationName, param));
}
}
};
@ElFunction(
prefix = "math",
name = "max",
description = "Returns the greater of two numbers."
)
public static Object max(@ElParam("Number 1") Object number, @ElParam("Number 2") Object number2) {
return MAX.process(number, number2);
}
/*
* Math:min
*/
private static Operation MIN = new Operation("min") {
@Override
protected Object calculate(GivenType type, Object param, Object secondParam) {
switch (type) {
case DOUBLE: return Math.min((double)param, (double)secondParam);
case FLOAT: return Math.min((float)param, (double)secondParam);
case INT: return Math.min((int)param, (int)secondParam);
case LONG: return Math.min((long)param, (long)secondParam);
default:
throw new IllegalArgumentException(Utils.format("Unsupported data type {} for operation {} and value '{}'", param.getClass(), operationName, param));
}
}
};
@ElFunction(
prefix = "math",
name = "min",
description = "Return minimal value of given two numbers."
)
public static Object min(@ElParam("Number 1") Object number, @ElParam("Number 2") Object number2) {
return MIN.process(number, number2);
}
/*
* Math:round
*/
private static Operation ROUND = new Operation("round") {
@Override
protected Object calculate(GivenType type, Object param, Object secondParam) {
switch (type) {
case DOUBLE: return Math.round((double)param);
case FLOAT: return Math.round((float)param);
default:
throw new IllegalArgumentException(Utils.format("Unsupported data type {} for operation {} and value '{}'", param.getClass(), operationName, param));
}
}
};
@ElFunction(
prefix = "math",
name = "round",
description = "Returns the closest int or long to the argument, with ties rounding up."
)
public static Object round(@ElParam("number") Object number) {
return ROUND.process(number);
}
private MathEL() {
}
}