package jnpf.elnFormula; import org.junit.Test; import java.math.BigDecimal; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; public class FormulaMathFunctionsTest { private final FormulaParser parser = new FormulaParser(); private final FormulaFunctionRegistry functions = new FormulaFunctionRegistry(); private final FormulaTypeChecker checker = new FormulaTypeChecker(functions); private final FormulaEvaluator evaluator = new FormulaEvaluator(functions); @Test public void recognizesExpandedFunctionSignatures() { assertType("LN(x_a1)", FormulaType.DECIMAL); assertType("LOG10(x_a1)", FormulaType.DECIMAL); assertType("LOG(x_a1, 10)", FormulaType.DECIMAL); assertType("EXP(x_a1)", FormulaType.DECIMAL); assertType("NTH_ROOT(x_a1, 3)", FormulaType.DECIMAL); assertType("COUNT(values_a1)", FormulaType.DECIMAL); assertType("MEDIAN(values_a1)", FormulaType.DECIMAL); assertType("STDDEV_P(values_a1)", FormulaType.DECIMAL); assertType("VAR_S(values_a1)", FormulaType.DECIMAL); assertType("VAR_P(values_a1)", FormulaType.DECIMAL); assertType("GEOMEAN(values_a1)", FormulaType.DECIMAL); assertType("WEIGHTED_MEAN(values_a1, weights_b2)", FormulaType.DECIMAL); assertType("SLOPE(values_a1, weights_b2)", FormulaType.DECIMAL); assertType("INTERCEPT(values_a1, weights_b2)", FormulaType.DECIMAL); assertType("CORREL(values_a1, weights_b2)", FormulaType.DECIMAL); assertType("LINEAR_PREDICT(x_a1, values_a1, weights_b2)", FormulaType.DECIMAL); assertType("ROUND_HALF_EVEN(x_a1, 2)", FormulaType.DECIMAL); assertType("ROUND_SIG(x_a1, 3)", FormulaType.DECIMAL); assertType("ROUND_SIG_HALF_EVEN(x_a1, 3)", FormulaType.DECIMAL); } @Test public void rejectsInvalidExpandedFunctionShapes() { assertError("LN(values_a1)", "TYPE_MISMATCH"); assertError("LOG(x_a1)", "FUNCTION_ARGUMENT_COUNT_MISMATCH"); assertError("COUNT(x_a1)", "TYPE_MISMATCH"); assertError("WEIGHTED_MEAN(values_a1)", "FUNCTION_ARGUMENT_COUNT_MISMATCH"); assertError("SLOPE(values_a1, x_a1)", "TYPE_MISMATCH"); assertError("LINEAR_PREDICT(values_a1, values_a1, weights_b2)", "TYPE_MISMATCH"); assertError("ROUND_SIG(values_a1, 3)", "TYPE_MISMATCH"); } @Test public void evaluatesBigDecimalMathAndIntermediateRounding() { assertDecimal("LN(1)", "0"); assertDecimal("EXP(0)", "1"); assertClose("LOG10(1000)", "3"); assertClose("LOG(8, 2)", "3"); assertDecimal("NTH_ROOT(27, 3)", "3"); assertDecimal("NTH_ROOT(-27, 3)", "-3"); assertDecimal("ROUND_HALF_EVEN(2.345, 2)", "2.34"); assertDecimal("ROUND_SIG(12345, 3)", "12300"); assertDecimal("ROUND_SIG_HALF_EVEN(1.245, 3)", "1.24"); } @Test public void rejectsMathDomainAndRoundingBoundaryViolations() { assertFormulaError("LN(0)", "LOG_DOMAIN_ERROR"); assertFormulaError("LOG10(-1)", "LOG_DOMAIN_ERROR"); assertFormulaError("LOG(10, 1)", "INVALID_LOG_BASE"); assertFormulaError("LOG(10, -2)", "INVALID_LOG_BASE"); assertFormulaError("EXP(1001)", "EXPONENT_OUT_OF_RANGE"); assertFormulaError("EXP(-1001)", "EXPONENT_OUT_OF_RANGE"); assertFormulaError("NTH_ROOT(16, 1)", "INVALID_ROOT_DEGREE"); assertFormulaError("NTH_ROOT(16, 2.5)", "INVALID_ROOT_DEGREE"); assertFormulaError("NTH_ROOT(-16, 2)", "NEGATIVE_EVEN_ROOT"); assertFormulaError("ROUND_SIG(1.23, 0)", "INVALID_SIGNIFICANT_DIGITS"); assertFormulaError("ROUND_SIG(1.23, 35)", "INVALID_SIGNIFICANT_DIGITS"); assertFormulaError("ROUND_SIG(1.23, 1.5)", "INVALID_SIGNIFICANT_DIGITS"); } @Test public void evaluatesStatisticsAndLinearCalibration() { Map values = statisticValues(); assertDecimal("COUNT(empty_d4)", values, "0"); assertDecimal("MEDIAN(values_a1)", values, "2"); assertDecimal("MEDIAN(even_values_f6)", values, "2.5"); assertDecimal("STDDEV_P(two_values_g7)", values, "1"); assertDecimal("VAR_S(values_a1)", values, "1"); assertClose("VAR_P(values_a1)", values, "0.6666666666666666666666666666666667"); assertDecimal("GEOMEAN(geomean_h8)", values, "2"); assertDecimal("WEIGHTED_MEAN(weight_values_i9, weights_c3)", values, "17.5"); assertDecimal("SLOPE(x_values_a1, y_values_b2)", values, "2"); assertDecimal("INTERCEPT(x_values_a1, y_values_b2)", values, "1"); assertDecimal("CORREL(x_values_a1, y_values_b2)", values, "1"); assertDecimal("LINEAR_PREDICT(4, x_values_a1, y_values_b2)", values, "9"); } @Test public void rejectsInvalidStatisticAndRegressionInputs() { Map values = statisticValues(); assertFormulaError("MEDIAN(empty_d4)", values, "INSUFFICIENT_SAMPLE"); assertFormulaError("STDDEV_P(empty_d4)", values, "INSUFFICIENT_SAMPLE"); assertFormulaError("VAR_S(one_value_e5)", values, "INSUFFICIENT_SAMPLE"); assertFormulaError("VAR_P(empty_d4)", values, "INSUFFICIENT_SAMPLE"); assertFormulaError("GEOMEAN(negative_values_j0)", values, "GEOMEAN_DOMAIN_ERROR"); assertFormulaError("WEIGHTED_MEAN(values_a1, one_value_e5)", values, "LIST_LENGTH_MISMATCH"); assertFormulaError("WEIGHTED_MEAN(two_values_g7, negative_weights_k1)", values, "INVALID_WEIGHT"); assertFormulaError("WEIGHTED_MEAN(two_values_g7, zero_weights_l2)", values, "INVALID_WEIGHT"); assertFormulaError("SLOPE(x_values_a1, one_value_e5)", values, "LIST_LENGTH_MISMATCH"); assertFormulaError("SLOPE(constant_values_m3, y_values_b2)", values, "ZERO_VARIANCE"); assertFormulaError("CORREL(x_values_a1, constant_values_m3)", values, "ZERO_VARIANCE"); } @Test public void preservesStatisticsForLargeNearbyDecimals() { Map values = statisticValues(); assertDecimal("VAR_P(large_nearby_n4)", values, "0.25"); assertDecimal("SLOPE(large_nearby_n4, large_y_o5)", values, "2"); assertDecimal("CORREL(large_nearby_n4, large_y_o5)", values, "1"); assertDecimal("LINEAR_PREDICT(10000000000000000000000000000000000, " + "large_nearby_n4, large_y_o5)", values, "5"); assertDecimal("CORREL(large_nearby_n4, large_negative_y_t0)", values, "-1"); assertDecimal("CORREL(values_a1, partial_correlation_y_u1)", values, "0.5"); } @Test public void roundsLargeAveragesOnlyAfterAggregation() { Map values = statisticValues(); String expected = "9999999999999999999999999999999998"; assertDecimal("MEAN(large_nearby_n4)", values, expected); assertDecimal("MEDIAN(large_nearby_n4)", values, expected); assertDecimal("WEIGHTED_MEAN(large_nearby_n4, equal_weights_s9)", values, expected); } @Test public void validatesEveryGeometricMeanItemBeforeReturningZero() { assertFormulaError("GEOMEAN(zero_then_negative_p6)", statisticValues(), "GEOMEAN_DOMAIN_ERROR"); } @Test public void keepsCorrelationInsidePearsonRangeAfterRounding() { assertDecimal("CORREL(correlation_x_q7, correlation_y_r8)", statisticValues(), "1"); } private void assertType(String expression, FormulaType expected) { assertEquals(expected, checker.check(parser.parse(expression), slots())); } private void assertError(String expression, String expectedCode) { try { checker.check(parser.parse(expression), slots()); fail("expected " + expectedCode); } catch (FormulaException ex) { assertEquals(expectedCode, ex.getCode()); } } private void assertDecimal(String expression, String expected) { assertDecimal(expression, Collections.emptyMap(), expected); } private void assertDecimal(String expression, Map values, String expected) { FormulaAst.Node ast = parser.parse(expression); assertEquals(FormulaType.DECIMAL, checker.check(ast, slots())); assertEquals(expected, evaluator.evaluate(ast, values).toPlainString()); } private void assertClose(String expression, String expected) { assertClose(expression, Collections.emptyMap(), expected); } private void assertClose(String expression, Map values, String expected) { FormulaAst.Node ast = parser.parse(expression); checker.check(ast, slots()); BigDecimal actual = evaluator.evaluate(ast, values).getDecimal(); BigDecimal difference = actual.subtract(new BigDecimal(expected)).abs(); if (difference.compareTo(new BigDecimal("1E-30")) > 0) { fail(expression + " expected approximately " + expected + " but was " + actual); } } private void assertFormulaError(String expression, String expectedCode) { assertFormulaError(expression, Collections.emptyMap(), expectedCode); } private void assertFormulaError(String expression, Map values, String expectedCode) { try { FormulaAst.Node ast = parser.parse(expression); checker.check(ast, slots()); evaluator.evaluate(ast, values); fail("expected " + expectedCode); } catch (FormulaException ex) { assertEquals(expectedCode, ex.getCode()); } } private static Map statisticValues() { Map result = new LinkedHashMap<>(); result.put("values_a1", list("1", "2", "3")); result.put("x_values_a1", list("1", "2", "3")); result.put("y_values_b2", list("3", "5", "7")); result.put("weights_c3", list("1", "3")); result.put("empty_d4", list()); result.put("one_value_e5", list("1")); result.put("even_values_f6", list("1", "2", "3", "4")); result.put("two_values_g7", list("1", "3")); result.put("geomean_h8", list("1", "4")); result.put("weight_values_i9", list("10", "20")); result.put("negative_values_j0", list("-1", "4")); result.put("negative_weights_k1", list("1", "-1")); result.put("zero_weights_l2", list("0", "0")); result.put("constant_values_m3", list("2", "2", "2")); result.put("large_nearby_n4", list( "9999999999999999999999999999999998", "9999999999999999999999999999999999")); result.put("large_y_o5", list("1", "3")); result.put("zero_then_negative_p6", list("0", "-1")); String[] correlationX = {"4056.4", "1871.7", "-0.0163297", "-0.0085862", "7.1395"}; result.put("correlation_x_q7", list(correlationX)); result.put("correlation_y_r8", scaledList("1.234567890123456789", correlationX)); result.put("equal_weights_s9", list("1", "1")); result.put("large_negative_y_t0", list("3", "1")); result.put("partial_correlation_y_u1", list("0", "-1", "1")); return result; } private static FormulaValue list(String... values) { FormulaValue[] items = new FormulaValue[values.length]; for (int i = 0; i < values.length; i++) { items[i] = FormulaValue.decimal(values[i]); } return FormulaValue.decimalList(items); } private static FormulaValue scaledList(String factor, String... values) { BigDecimal multiplier = new BigDecimal(factor); FormulaValue[] items = new FormulaValue[values.length]; for (int i = 0; i < values.length; i++) { items[i] = FormulaValue.decimal(new BigDecimal(values[i]).multiply(multiplier)); } return FormulaValue.decimalList(items); } private static Map slots() { Map result = new LinkedHashMap<>(); result.put("x_a1", FormulaType.DECIMAL); result.put("values_a1", FormulaType.DECIMAL_LIST); result.put("weights_b2", FormulaType.DECIMAL_LIST); result.put("x_values_a1", FormulaType.DECIMAL_LIST); result.put("y_values_b2", FormulaType.DECIMAL_LIST); result.put("weights_c3", FormulaType.DECIMAL_LIST); result.put("empty_d4", FormulaType.DECIMAL_LIST); result.put("one_value_e5", FormulaType.DECIMAL_LIST); result.put("even_values_f6", FormulaType.DECIMAL_LIST); result.put("two_values_g7", FormulaType.DECIMAL_LIST); result.put("geomean_h8", FormulaType.DECIMAL_LIST); result.put("weight_values_i9", FormulaType.DECIMAL_LIST); result.put("negative_values_j0", FormulaType.DECIMAL_LIST); result.put("negative_weights_k1", FormulaType.DECIMAL_LIST); result.put("zero_weights_l2", FormulaType.DECIMAL_LIST); result.put("constant_values_m3", FormulaType.DECIMAL_LIST); result.put("large_nearby_n4", FormulaType.DECIMAL_LIST); result.put("large_y_o5", FormulaType.DECIMAL_LIST); result.put("zero_then_negative_p6", FormulaType.DECIMAL_LIST); result.put("correlation_x_q7", FormulaType.DECIMAL_LIST); result.put("correlation_y_r8", FormulaType.DECIMAL_LIST); result.put("equal_weights_s9", FormulaType.DECIMAL_LIST); result.put("large_negative_y_t0", FormulaType.DECIMAL_LIST); result.put("partial_correlation_y_u1", FormulaType.DECIMAL_LIST); return result; } }