MathMethod.java

package fr.inrae.agroclim.indicators.model.function.aggregation;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import fr.inrae.agroclim.indicators.model.JEXLFormula;

/**
 * Aggregation methods for {@link JEXLFormula}.
 *
 * @author Olivier Maury
 */
public class MathMethod {
    /**
     * Returns the average of the values.
     *
     * @param values
     * @return average value
     */
    public static Double avg(final Number... values) {
        int nb = 0;
        Double sum = 0.;
        for (final Number value : values) {
            if (value == null) {
                continue;
            }
            sum += value.doubleValue();
            nb++;
        }
        if (nb == 0) {
            return null;
        }
        return sum / nb;
    }

    /**
     * Returns Euler's number <i>e</i> raised to the power of a
     * {@code double} value.  Special cases:
     * <ul><li>If the argument is NaN, the result is NaN.
     * <li>If the argument is positive infinity, then the result is
     * positive infinity.
     * <li>If the argument is negative infinity, then the result is
     * positive zero.
     * <li>If the argument is zero, then the result is {@code 1.0}.
     * </ul>
     *
     * <p>The computed result must be within 1 ulp of the exact result.
     * Results must be semi-monotonic.
     *
     * @param   a   the exponent to raise <i>e</i> to.
     * @return  the value <i>e</i><sup>{@code a}</sup>,
     *          where <i>e</i> is the base of the natural logarithms.
     */
    public static Double exp(final Number a) {
        if (isNaN(a)) {
            return null;
        }
        return Math.exp(a.doubleValue());
    }

    /**
     * List all available methods.
     *
     * @return method name ⮕ method representation with parameters
     */
    public static Map<String, String> getMethodNamesWithParameters() {
        final Map<String, String> map = new HashMap<>();
        final Method[] methods = MathMethod.class.getDeclaredMethods();

        for (final Method method : methods) {
            final Class<?>[] parameters = method.getParameterTypes();
            if (parameters == null || parameters.length == 0
                    || "isNaN".equals(method.getName()) || method.getName().startsWith("$")) {
                continue;
            }
            final String val;
            if (parameters.length == 1) {
                val = "math:" + method.getName() + "(?)";
            } else {
                val = "math:" + method.getName() + "(?, ...)";
            }
            map.put(method.getName(), val);
        }
        return map;
    }

    private static boolean isNaN(final Number value) {
        if (value instanceof Float floatValue) {
            return Float.isNaN(floatValue);
        }
        return false;
    }

    /**
     * Returns the natural logarithm (base <i>e</i>) of a {@code double}
     * value.  Special cases:
     * <ul><li>If the argument is NaN or less than zero, then the result
     * is NaN.
     * <li>If the argument is positive infinity, then the result is
     * positive infinity.
     * <li>If the argument is positive zero or negative zero, then the
     * result is negative infinity.
     * <li>If the argument is {@code 1.0}, then the result is positive
     * zero.
     * </ul>
     *
     * <p>The computed result must be within 1 ulp of the exact result.
     * Results must be semi-monotonic.
     *
     * @param   a   a value
     * @return  the value ln&nbsp;{@code a}, the natural logarithm of
     *          {@code a}.
     */
    public static Double log(final Number a) {
        if (isNaN(a)) {
            return null;
        }
        return Math.log(a.doubleValue());
    }

    /**
     * Returns the greater of Number values.
     *
     * That is, the result is the argument closer to positive infinity.<br>
     * If the arguments have the same value, the result is that same value.<br>
     * If either value is NaN, then the result is NaN.<br>
     * Unlike the numerical comparison operators, this method considers negative
     * zero to be strictly smaller than positive zero. If one argument is positive
     * zero and the other negative zero, the result is positive zero.
     *
     * @param values arguments
     * @return greatest of values.
     */
    public static Double max(final Number... values) {
        Double max = null;
        for (final Number val : values) {
            if (val == null || isNaN(val)) {
                continue;
            }
            if (max == null || val.doubleValue() > max) {
                max = val.doubleValue();
            }
        }
        return max;
    }

    /**
     * Returns the smaller of Number values.
     *
     * That is, the result is the value closer to negative infinity.<br>
     * If the arguments have the same value, the result is that same value.<br>
     * If either value is NaN, then the result is NaN.<br>
     * Unlike the numerical comparison operators, this method considers negative
     * zero to be strictly smaller than positive zero. If one argument is positive
     * zero and the other is negative zero, the result is negative zero.
     *
     * @param values
     * @return smallest of values.
     */
    public static Double min(final Number... values) {
        Double min = null;
        for (final Number val : values) {
            if (val == null || isNaN(val)) {
                continue;
            }
            if (min == null || val.doubleValue() < min) {
                min = val.doubleValue();
            }
        }
        return min;
    }

    /**
     * Returns the value of the first argument raised to the power of the second argument.
     *
     * Special cases, see {@java.lang.Math#pow(double, double)}.
     *
     * @param a the base
     * @param b the exponent
     * @return the value aᵇ.
     */
    public static Double pow(final Number a, final Number b) {
        if (a == null) {
            return null;
        }
        if (b == null) {
            return null;
        }
        double aValue = a.doubleValue();
        double bValue = b.doubleValue();
        return Math.pow(aValue, bValue);
    }

    /**
     * Empty constructor to inject the class into JEXL.
     */
    public MathMethod() {
        // Do nothing
    }
}