package org.apache.poi.ss.formula.functions;
+import org.apache.commons.math3.stat.descriptive.moment.GeometricMean;
import org.apache.poi.ss.formula.eval.ErrorEval;
import org.apache.poi.ss.formula.eval.EvaluationException;
import org.apache.poi.ss.formula.eval.NumberEval;
*/
public abstract class AggregateFunction extends MultiOperandNumericFunction {
- private static final class LargeSmall extends Fixed2ArgFunction {
- private final boolean _isLarge;
- protected LargeSmall(boolean isLarge) {
- _isLarge = isLarge;
- }
+ private static final class LargeSmall extends Fixed2ArgFunction {
+ private final boolean _isLarge;
+ protected LargeSmall(boolean isLarge) {
+ _isLarge = isLarge;
+ }
- public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0,
- ValueEval arg1) {
- double dn;
- try {
- ValueEval ve1 = OperandResolver.getSingleValue(arg1, srcRowIndex, srcColumnIndex);
- dn = OperandResolver.coerceValueToDouble(ve1);
- } catch (EvaluationException e1) {
- // all errors in the second arg translate to #VALUE!
- return ErrorEval.VALUE_INVALID;
- }
- // weird Excel behaviour on second arg
- if (dn < 1.0) {
- // values between 0.0 and 1.0 result in #NUM!
- return ErrorEval.NUM_ERROR;
- }
- // all other values are rounded up to the next integer
- int k = (int) Math.ceil(dn);
+ public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0,
+ ValueEval arg1) {
+ double dn;
+ try {
+ ValueEval ve1 = OperandResolver.getSingleValue(arg1, srcRowIndex, srcColumnIndex);
+ dn = OperandResolver.coerceValueToDouble(ve1);
+ } catch (EvaluationException e1) {
+ // all errors in the second arg translate to #VALUE!
+ return ErrorEval.VALUE_INVALID;
+ }
+ // weird Excel behaviour on second arg
+ if (dn < 1.0) {
+ // values between 0.0 and 1.0 result in #NUM!
+ return ErrorEval.NUM_ERROR;
+ }
+ // all other values are rounded up to the next integer
+ int k = (int) Math.ceil(dn);
- double result;
- try {
- double[] ds = ValueCollector.collectValues(arg0);
- if (k > ds.length) {
- return ErrorEval.NUM_ERROR;
- }
- result = _isLarge ? StatsLib.kthLargest(ds, k) : StatsLib.kthSmallest(ds, k);
- NumericFunction.checkValue(result);
- } catch (EvaluationException e) {
- return e.getErrorEval();
- }
+ double result;
+ try {
+ double[] ds = ValueCollector.collectValues(arg0);
+ if (k > ds.length) {
+ return ErrorEval.NUM_ERROR;
+ }
+ result = _isLarge ? StatsLib.kthLargest(ds, k) : StatsLib.kthSmallest(ds, k);
+ NumericFunction.checkValue(result);
+ } catch (EvaluationException e) {
+ return e.getErrorEval();
+ }
- return new NumberEval(result);
- }
- }
+ return new NumberEval(result);
+ }
+ }
/**
* Returns the k-th percentile of values in a range. You can use this function to establish a threshold of
* <li>If k is not a multiple of 1/(n - 1), PERCENTILE interpolates to determine the value at the k-th percentile.</li>
* </ul>
*/
- private static final class Percentile extends Fixed2ArgFunction {
-
- protected Percentile() {
- }
+ private static final class Percentile extends Fixed2ArgFunction {
- public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0,
- ValueEval arg1) {
- double dn;
- try {
- ValueEval ve1 = OperandResolver.getSingleValue(arg1, srcRowIndex, srcColumnIndex);
- dn = OperandResolver.coerceValueToDouble(ve1);
- } catch (EvaluationException e1) {
- // all errors in the second arg translate to #VALUE!
- return ErrorEval.VALUE_INVALID;
- }
- if (dn < 0 || dn > 1) { // has to be percentage
- return ErrorEval.NUM_ERROR;
- }
+ protected Percentile() {
+ }
- double result;
- try {
- double[] ds = ValueCollector.collectValues(arg0);
- int N = ds.length;
+ public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0,
+ ValueEval arg1) {
+ double dn;
+ try {
+ ValueEval ve1 = OperandResolver.getSingleValue(arg1, srcRowIndex, srcColumnIndex);
+ dn = OperandResolver.coerceValueToDouble(ve1);
+ } catch (EvaluationException e1) {
+ // all errors in the second arg translate to #VALUE!
+ return ErrorEval.VALUE_INVALID;
+ }
+ if (dn < 0 || dn > 1) { // has to be percentage
+ return ErrorEval.NUM_ERROR;
+ }
- if (N == 0 || N > 8191) {
+ double result;
+ try {
+ double[] ds = ValueCollector.collectValues(arg0);
+ int N = ds.length;
+
+ if (N == 0 || N > 8191) {
return ErrorEval.NUM_ERROR;
}
- double n = (N - 1) * dn + 1;
- if (n == 1d) {
- result = StatsLib.kthSmallest(ds, 1);
- } else if (Double.compare(n, N) == 0) {
- result = StatsLib.kthLargest(ds, 1);
- } else {
- int k = (int) n;
- double d = n - k;
- result = StatsLib.kthSmallest(ds, k) + d
- * (StatsLib.kthSmallest(ds, k + 1) - StatsLib.kthSmallest(ds, k));
- }
+ double n = (N - 1) * dn + 1;
+ if (n == 1d) {
+ result = StatsLib.kthSmallest(ds, 1);
+ } else if (Double.compare(n, N) == 0) {
+ result = StatsLib.kthLargest(ds, 1);
+ } else {
+ int k = (int) n;
+ double d = n - k;
+ result = StatsLib.kthSmallest(ds, k) + d
+ * (StatsLib.kthSmallest(ds, k + 1) - StatsLib.kthSmallest(ds, k));
+ }
+
+ NumericFunction.checkValue(result);
+ } catch (EvaluationException e) {
+ return e.getErrorEval();
+ }
- NumericFunction.checkValue(result);
- } catch (EvaluationException e) {
- return e.getErrorEval();
- }
+ return new NumberEval(result);
+ }
+ }
- return new NumberEval(result);
- }
- }
-
- static final class ValueCollector extends MultiOperandNumericFunction {
- private static final ValueCollector instance = new ValueCollector();
- public ValueCollector() {
- super(false, false);
- }
- public static double[] collectValues(ValueEval...operands) throws EvaluationException {
- return instance.getNumberArray(operands);
- }
- protected double evaluate(double[] values) {
- throw new IllegalStateException("should not be called");
- }
- }
+ static final class ValueCollector extends MultiOperandNumericFunction {
+ private static final ValueCollector instance = new ValueCollector();
+ public ValueCollector() {
+ super(false, false);
+ }
+ public static double[] collectValues(ValueEval...operands) throws EvaluationException {
+ return instance.getNumberArray(operands);
+ }
+ protected double evaluate(double[] values) {
+ throw new IllegalStateException("should not be called");
+ }
+ }
protected AggregateFunction() {
super(false, false);
}
public static final Function AVEDEV = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return StatsLib.avedev(values);
- }
- };
- public static final Function AVERAGE = new AggregateFunction() {
- protected double evaluate(double[] values) throws EvaluationException {
- if (values.length < 1) {
- throw new EvaluationException(ErrorEval.DIV_ZERO);
- }
- return MathX.average(values);
- }
- };
- public static final Function DEVSQ = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return StatsLib.devsq(values);
- }
- };
- public static final Function LARGE = new LargeSmall(true);
- public static final Function MAX = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return values.length > 0 ? MathX.max(values) : 0;
- }
- };
- public static final Function MEDIAN = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return StatsLib.median(values);
- }
- };
- public static final Function MIN = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return values.length > 0 ? MathX.min(values) : 0;
- }
- };
-
- public static final Function PERCENTILE = new Percentile();
-
- public static final Function PRODUCT = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return MathX.product(values);
- }
- };
- public static final Function SMALL = new LargeSmall(false);
- public static final Function STDEV = new AggregateFunction() {
- protected double evaluate(double[] values) throws EvaluationException {
- if (values.length < 1) {
- throw new EvaluationException(ErrorEval.DIV_ZERO);
- }
- return StatsLib.stdev(values);
- }
- };
- public static final Function SUM = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return MathX.sum(values);
- }
- };
- public static final Function SUMSQ = new AggregateFunction() {
- protected double evaluate(double[] values) {
- return MathX.sumsq(values);
- }
- };
+ protected double evaluate(double[] values) {
+ return StatsLib.avedev(values);
+ }
+ };
+ public static final Function AVERAGE = new AggregateFunction() {
+ protected double evaluate(double[] values) throws EvaluationException {
+ if (values.length < 1) {
+ throw new EvaluationException(ErrorEval.DIV_ZERO);
+ }
+ return MathX.average(values);
+ }
+ };
+ public static final Function DEVSQ = new AggregateFunction() {
+ protected double evaluate(double[] values) {
+ return StatsLib.devsq(values);
+ }
+ };
+ public static final Function LARGE = new LargeSmall(true);
+ public static final Function MAX = new AggregateFunction() {
+ protected double evaluate(double[] values) {
+ return values.length > 0 ? MathX.max(values) : 0;
+ }
+ };
+ public static final Function MEDIAN = new AggregateFunction() {
+ protected double evaluate(double[] values) {
+ return StatsLib.median(values);
+ }
+ };
+ public static final Function MIN = new AggregateFunction() {
+ protected double evaluate(double[] values) {
+ return values.length > 0 ? MathX.min(values) : 0;
+ }
+ };
+
+ public static final Function PERCENTILE = new Percentile();
+
+ public static final Function PRODUCT = new AggregateFunction() {
+ protected double evaluate(double[] values) {
+ return MathX.product(values);
+ }
+ };
+ public static final Function SMALL = new LargeSmall(false);
+ public static final Function STDEV = new AggregateFunction() {
+ protected double evaluate(double[] values) throws EvaluationException {
+ if (values.length < 1) {
+ throw new EvaluationException(ErrorEval.DIV_ZERO);
+ }
+ return StatsLib.stdev(values);
+ }
+ };
+ public static final Function SUM = new AggregateFunction() {
+ protected double evaluate(double[] values) {
+ return MathX.sum(values);
+ }
+ };
+ public static final Function SUMSQ = new AggregateFunction() {
+ protected double evaluate(double[] values) {
+ return MathX.sumsq(values);
+ }
+ };
public static final Function VAR = new AggregateFunction() {
protected double evaluate(double[] values) throws EvaluationException {
if (values.length < 1) {
return StatsLib.varp(values);
}
};
+ public static final Function GEOMEAN = new AggregateFunction() {
+ @Override
+ protected double evaluate(double[] values) throws EvaluationException {
+ // The library implementation returns 0 for an input sequence like [1, 0]. So this check is necessary.
+ for (double value: values) {
+ if (value <= 0) {
+ throw new EvaluationException(ErrorEval.NUM_ERROR);
+ }
+ }
+ return new GeometricMean().evaluate(values, 0, values.length);
+ }
+ };
}
--- /dev/null
+package org.apache.poi.ss.formula.functions;
+
+import org.apache.poi.ss.formula.eval.*;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * From Excel documentation at https://support.office.com/en-us/article/geomean-function-db1ac48d-25a5-40a0-ab83-0b38980e40d5:
+ * 1. Arguments can either be numbers or names, arrays, or references that contain numbers.
+ * 2. Logical values and text representations of numbers that you type directly into the list of arguments are counted.
+ * 3. If an array or reference argument contains text, logical values, or empty cells, those values are ignored; however, cells with the value zero are included.
+ * 4. Arguments that are error values or text that cannot be translated into numbers cause errors.
+ * 5. If any data point ≤ 0, GEOMEAN returns the #NUM! error value.
+ *
+ * Remarks:
+ * Actually, 5. is not true. If an error is encountered before a 0 value, the error is returned.
+ */
+public class TestGeomean {
+ @Test
+ public void acceptanceTest() {
+ Function geomean = getInstance();
+
+ final ValueEval result = geomean.evaluate(new ValueEval[]{new NumberEval(2), new NumberEval(3)}, 0, 0);
+ verifyNumericResult(2.449489742783178, result);
+ }
+
+ @Test
+ public void booleansByValueAreCoerced() {
+ final ValueEval[] args = {BoolEval.TRUE};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ verifyNumericResult(1.0, result);
+ }
+
+ @Test
+ public void stringsByValueAreCoerced() {
+ final ValueEval[] args = {new StringEval("2")};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ verifyNumericResult(2.0, result);
+ }
+
+ @Test
+ public void nonCoerceableStringsByValueCauseValueInvalid() {
+ final ValueEval[] args = {new StringEval("foo")};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ assertEquals(ErrorEval.VALUE_INVALID, result);
+ }
+
+ @Test
+ public void booleansByReferenceAreSkipped() {
+ final ValueEval[] args = new ValueEval[]{new NumberEval(2.0), EvalFactory.createRefEval("A1", BoolEval.TRUE)};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ verifyNumericResult(2.0, result);
+ }
+
+ @Test
+ public void booleansStringsAndBlanksByReferenceAreSkipped() {
+ ValueEval ref = EvalFactory.createAreaEval("A1:A3", new ValueEval[]{new StringEval("foo"), BoolEval.FALSE, BlankEval.instance});
+ final ValueEval[] args = {ref, new NumberEval(2.0)};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ verifyNumericResult(2.0, result);
+ }
+
+ @Test
+ public void stringsByValueAreCounted() {
+ final ValueEval[] args = {new StringEval("2.0")};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ verifyNumericResult(2.0, result);
+ }
+
+ @Test
+ public void missingArgCountAsZero() {
+ // and, naturally, produces a NUM_ERROR
+ final ValueEval[] args = {new NumberEval(1.0), MissingArgEval.instance};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ assertEquals(ErrorEval.NUM_ERROR, result);
+ }
+
+ /**
+ * Implementation-specific: the math lib returns 0 for the input [1.0, 0.0], but a NUM_ERROR should be returned.
+ */
+ @Test
+ public void sequence_1_0_shouldReturnError() {
+ final ValueEval[] args = {new NumberEval(1.0), new NumberEval(0)};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ assertEquals(ErrorEval.NUM_ERROR, result);
+ }
+
+ @Test
+ public void minusOneShouldReturnError() {
+ final ValueEval[] args = {new NumberEval(1.0), new NumberEval(-1.0)};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ assertEquals(ErrorEval.NUM_ERROR, result);
+ }
+
+ @Test
+ public void firstErrorPropagates() {
+ final ValueEval[] args = {ErrorEval.DIV_ZERO, ErrorEval.NUM_ERROR};
+ final ValueEval result = getInstance().evaluate(args, 0, 0);
+ assertEquals(ErrorEval.DIV_ZERO, result);
+ }
+
+ private void verifyNumericResult(double expected, ValueEval result) {
+ assertTrue(result instanceof NumberEval);
+ assertEquals(expected, ((NumberEval) result).getNumberValue(), 1e-15);
+ }
+
+ private Function getInstance() {
+ return AggregateFunction.GEOMEAN;
+ }
+}