diff --git a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java index 021d6980f0..733b14b26b 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -28,7 +28,7 @@ import java.util.TreeSet; /** * @author Amol S. Deshmukh < amolweb at ya hoo dot com > - * @author Johan Karlsteen - added Intercept + * @author Johan Karlsteen - added Intercept and Slope */ public final class FunctionEval { /** @@ -210,6 +210,7 @@ public final class FunctionEval { retval[305] = new Sumx2py2(); retval[311] = new Intercept(); + retval[315] = new Slope(); retval[318] = AggregateFunction.DEVSQ; diff --git a/src/java/org/apache/poi/ss/formula/functions/Intercept.java b/src/java/org/apache/poi/ss/formula/functions/Intercept.java index 06bb6f97db..cf76cc5880 100644 --- a/src/java/org/apache/poi/ss/formula/functions/Intercept.java +++ b/src/java/org/apache/poi/ss/formula/functions/Intercept.java @@ -19,13 +19,8 @@ package org.apache.poi.ss.formula.functions; -import org.apache.poi.ss.formula.TwoDEval; -import org.apache.poi.ss.formula.eval.ErrorEval; -import org.apache.poi.ss.formula.eval.EvaluationException; -import org.apache.poi.ss.formula.eval.NumberEval; -import org.apache.poi.ss.formula.eval.RefEval; import org.apache.poi.ss.formula.eval.ValueEval; -import org.apache.poi.ss.formula.functions.LookupUtils.ValueVector; +import org.apache.poi.ss.formula.functions.LinearRegressionFunction.FUNCTION; /** * Implementation of Excel function INTERCEPT()

@@ -40,184 +35,15 @@ import org.apache.poi.ss.formula.functions.LookupUtils.ValueVector; */ public final class Intercept extends Fixed2ArgFunction { - private static abstract class ValueArray implements ValueVector { - private final int _size; - protected ValueArray(int size) { - _size = size; - } - - public ValueEval getItem(int index) { - if (index < 0 || index > _size) { - throw new IllegalArgumentException("Specified index " + index - + " is outside range (0.." + (_size - 1) + ")"); - } - return getItemInternal(index); - } - protected abstract ValueEval getItemInternal(int index); - - public final int getSize() { - return _size; - } + private final LinearRegressionFunction func; + public Intercept() { + func = new LinearRegressionFunction(FUNCTION.INTERCEPT); } - - private static final class SingleCellValueArray extends ValueArray { - private final ValueEval _value; - public SingleCellValueArray(ValueEval value) { - super(1); - _value = value; - } - @Override - protected ValueEval getItemInternal(int index) { - return _value; - } - } - - private static final class RefValueArray extends ValueArray { - private final RefEval _ref; - public RefValueArray(RefEval ref) { - super(1); - _ref = ref; - } - @Override - protected ValueEval getItemInternal(int index) { - return _ref.getInnerValueEval(); - } - } - - private static final class AreaValueArray extends ValueArray { - private final TwoDEval _ae; - private final int _width; - - public AreaValueArray(TwoDEval ae) { - super(ae.getWidth() * ae.getHeight()); - _ae = ae; - _width = ae.getWidth(); - } - @Override - protected ValueEval getItemInternal(int index) { - int rowIx = index / _width; - int colIx = index % _width; - return _ae.getValue(rowIx, colIx); - } - } - + @Override public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { - double result; - try { - ValueVector vvX = createValueVector(arg0); - ValueVector vvY = createValueVector(arg1); - int size = vvX.getSize(); - if (size == 0 || vvY.getSize() != size) { - return ErrorEval.NA; - } - result = evaluateInternal(vvX, vvY, size); - } catch (EvaluationException e) { - return e.getErrorEval(); - } - if (Double.isNaN(result) || Double.isInfinite(result)) { - return ErrorEval.NUM_ERROR; - } - return new NumberEval(result); + return func.evaluate(srcRowIndex, srcColumnIndex, arg0, arg1); } - - private double evaluateInternal(ValueVector x, ValueVector y, int size) - throws EvaluationException { +} - // error handling is as if the x is fully evaluated before y - ErrorEval firstXerr = null; - ErrorEval firstYerr = null; - boolean accumlatedSome = false; - double result = 0.0; - // first pass: read in data, compute xbar and ybar - double sumx = 0.0, sumy = 0.0; - - for (int i = 0; i < size; i++) { - ValueEval vx = x.getItem(i); - ValueEval vy = y.getItem(i); - if (vx instanceof ErrorEval) { - if (firstXerr == null) { - firstXerr = (ErrorEval) vx; - continue; - } - } - if (vy instanceof ErrorEval) { - if (firstYerr == null) { - firstYerr = (ErrorEval) vy; - continue; - } - } - // only count pairs if both elements are numbers - if (vx instanceof NumberEval && vy instanceof NumberEval) { - accumlatedSome = true; - NumberEval nx = (NumberEval) vx; - NumberEval ny = (NumberEval) vy; - sumx += nx.getNumberValue(); - sumy += ny.getNumberValue(); - } else { - // all other combinations of value types are silently ignored - } - } - double xbar = sumx / size; - double ybar = sumy / size; - - // second pass: compute summary statistics - double xxbar = 0.0, xybar = 0.0; - for (int i = 0; i < size; i++) { - ValueEval vx = x.getItem(i); - ValueEval vy = y.getItem(i); - - if (vx instanceof ErrorEval) { - if (firstXerr == null) { - firstXerr = (ErrorEval) vx; - continue; - } - } - if (vy instanceof ErrorEval) { - if (firstYerr == null) { - firstYerr = (ErrorEval) vy; - continue; - } - } - - // only count pairs if both elements are numbers - if (vx instanceof NumberEval && vy instanceof NumberEval) { - NumberEval nx = (NumberEval) vx; - NumberEval ny = (NumberEval) vy; - xxbar += (nx.getNumberValue() - xbar) * (nx.getNumberValue() - xbar); - xybar += (nx.getNumberValue() - xbar) * (ny.getNumberValue() - ybar); - } else { - // all other combinations of value types are silently ignored - } - } - double beta1 = xybar / xxbar; - double beta0 = ybar - beta1 * xbar; - - if (firstXerr != null) { - throw new EvaluationException(firstXerr); - } - if (firstYerr != null) { - throw new EvaluationException(firstYerr); - } - if (!accumlatedSome) { - throw new EvaluationException(ErrorEval.DIV_ZERO); - } - - result = beta0; - return result; - } - - private static ValueVector createValueVector(ValueEval arg) throws EvaluationException { - if (arg instanceof ErrorEval) { - throw new EvaluationException((ErrorEval) arg); - } - if (arg instanceof TwoDEval) { - return new AreaValueArray((TwoDEval) arg); - } - if (arg instanceof RefEval) { - return new RefValueArray((RefEval) arg); - } - return new SingleCellValueArray(arg); - } -} \ No newline at end of file diff --git a/src/java/org/apache/poi/ss/formula/functions/LinearRegressionFunction.java b/src/java/org/apache/poi/ss/formula/functions/LinearRegressionFunction.java new file mode 100644 index 0000000000..740fd05071 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/LinearRegressionFunction.java @@ -0,0 +1,236 @@ +/* + * ==================================================================== + * Licensed to 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 org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.formula.TwoDEval; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.RefEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.functions.LookupUtils.ValueVector; + +/** + * Base class for linear regression functions. + * + * Calculates the linear regression line that is used to predict y values from x values
+ * (http://introcs.cs.princeton.edu/java/97data/LinearRegression.java.html) + * Syntax:
+ * INTERCEPT(arrayX, arrayY)

+ * or + * SLOPE(arrayX, arrayY)

+ * + * + * @author Johan Karlsteen + */ +public final class LinearRegressionFunction extends Fixed2ArgFunction { + + private static abstract class ValueArray implements ValueVector { + private final int _size; + protected ValueArray(int size) { + _size = size; + } + @Override + public ValueEval getItem(int index) { + if (index < 0 || index > _size) { + throw new IllegalArgumentException("Specified index " + index + + " is outside range (0.." + (_size - 1) + ")"); + } + return getItemInternal(index); + } + protected abstract ValueEval getItemInternal(int index); + @Override + public final int getSize() { + return _size; + } + } + + private static final class SingleCellValueArray extends ValueArray { + private final ValueEval _value; + public SingleCellValueArray(ValueEval value) { + super(1); + _value = value; + } + @Override + protected ValueEval getItemInternal(int index) { + return _value; + } + } + + private static final class RefValueArray extends ValueArray { + private final RefEval _ref; + public RefValueArray(RefEval ref) { + super(1); + _ref = ref; + } + @Override + protected ValueEval getItemInternal(int index) { + return _ref.getInnerValueEval(); + } + } + + private static final class AreaValueArray extends ValueArray { + private final TwoDEval _ae; + private final int _width; + + public AreaValueArray(TwoDEval ae) { + super(ae.getWidth() * ae.getHeight()); + _ae = ae; + _width = ae.getWidth(); + } + @Override + protected ValueEval getItemInternal(int index) { + int rowIx = index / _width; + int colIx = index % _width; + return _ae.getValue(rowIx, colIx); + } + } + + public enum FUNCTION {INTERCEPT, SLOPE}; + public FUNCTION function; + + public LinearRegressionFunction(FUNCTION function) { + this.function = function; + } + + @Override + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, + ValueEval arg0, ValueEval arg1) { + double result; + try { + ValueVector vvX = createValueVector(arg0); + ValueVector vvY = createValueVector(arg1); + int size = vvX.getSize(); + if (size == 0 || vvY.getSize() != size) { + return ErrorEval.NA; + } + result = evaluateInternal(vvX, vvY, size); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + if (Double.isNaN(result) || Double.isInfinite(result)) { + return ErrorEval.NUM_ERROR; + } + return new NumberEval(result); + } + + private double evaluateInternal(ValueVector x, ValueVector y, int size) + throws EvaluationException { + + // error handling is as if the x is fully evaluated before y + ErrorEval firstXerr = null; + ErrorEval firstYerr = null; + boolean accumlatedSome = false; + double result = 0.0; + // first pass: read in data, compute xbar and ybar + double sumx = 0.0, sumy = 0.0; + + for (int i = 0; i < size; i++) { + ValueEval vx = x.getItem(i); + ValueEval vy = y.getItem(i); + if (vx instanceof ErrorEval) { + if (firstXerr == null) { + firstXerr = (ErrorEval) vx; + continue; + } + } + if (vy instanceof ErrorEval) { + if (firstYerr == null) { + firstYerr = (ErrorEval) vy; + continue; + } + } + // only count pairs if both elements are numbers + if (vx instanceof NumberEval && vy instanceof NumberEval) { + accumlatedSome = true; + NumberEval nx = (NumberEval) vx; + NumberEval ny = (NumberEval) vy; + sumx += nx.getNumberValue(); + sumy += ny.getNumberValue(); + } else { + // all other combinations of value types are silently ignored + } + } + double xbar = sumx / size; + double ybar = sumy / size; + + // second pass: compute summary statistics + double xxbar = 0.0, xybar = 0.0; + for (int i = 0; i < size; i++) { + ValueEval vx = x.getItem(i); + ValueEval vy = y.getItem(i); + + if (vx instanceof ErrorEval) { + if (firstXerr == null) { + firstXerr = (ErrorEval) vx; + continue; + } + } + if (vy instanceof ErrorEval) { + if (firstYerr == null) { + firstYerr = (ErrorEval) vy; + continue; + } + } + + // only count pairs if both elements are numbers + if (vx instanceof NumberEval && vy instanceof NumberEval) { + NumberEval nx = (NumberEval) vx; + NumberEval ny = (NumberEval) vy; + xxbar += (nx.getNumberValue() - xbar) * (nx.getNumberValue() - xbar); + xybar += (nx.getNumberValue() - xbar) * (ny.getNumberValue() - ybar); + } else { + // all other combinations of value types are silently ignored + } + } + double beta1 = xybar / xxbar; + double beta0 = ybar - beta1 * xbar; + + if (firstXerr != null) { + throw new EvaluationException(firstXerr); + } + if (firstYerr != null) { + throw new EvaluationException(firstYerr); + } + if (!accumlatedSome) { + throw new EvaluationException(ErrorEval.DIV_ZERO); + } + + if(function == FUNCTION.INTERCEPT) { + return beta0; + } else { + return beta1; + } + } + + private static ValueVector createValueVector(ValueEval arg) throws EvaluationException { + if (arg instanceof ErrorEval) { + throw new EvaluationException((ErrorEval) arg); + } + if (arg instanceof TwoDEval) { + return new AreaValueArray((TwoDEval) arg); + } + if (arg instanceof RefEval) { + return new RefValueArray((RefEval) arg); + } + return new SingleCellValueArray(arg); + } +} + diff --git a/src/java/org/apache/poi/ss/formula/functions/Slope.java b/src/java/org/apache/poi/ss/formula/functions/Slope.java new file mode 100644 index 0000000000..ec72102145 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/Slope.java @@ -0,0 +1,49 @@ +/* + * ==================================================================== + * Licensed to 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 org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.formula.functions.LinearRegressionFunction.FUNCTION; + +/** + * Implementation of Excel function SLOPE()

+ * + * Calculates the SLOPE of the linear regression line that is used to predict y values from x values
+ * (http://introcs.cs.princeton.edu/java/97data/LinearRegression.java.html) + * Syntax:
+ * SLOPE(arrayX, arrayY)

+ * + * + * @author Johan Karlsteen + */ +public final class Slope extends Fixed2ArgFunction { + + private final LinearRegressionFunction func; + public Slope() { + func = new LinearRegressionFunction(FUNCTION.SLOPE); + } + + @Override + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, + ValueEval arg0, ValueEval arg1) { + return func.evaluate(srcRowIndex, srcColumnIndex, arg0, arg1); + } +} + diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestSlope.java b/src/testcases/org/apache/poi/ss/formula/functions/TestSlope.java new file mode 100644 index 0000000000..2ea0332eda --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestSlope.java @@ -0,0 +1,137 @@ +/* + * ==================================================================== + * Licensed to 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 org.apache.poi.ss.formula.functions; + +import junit.framework.TestCase; + +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.ValueEval; +/** + * Test for Excel function SLOPE() + * + * @author Johan Karlsteen + */ +public final class TestSlope extends TestCase { + private static final Function SLOPE = new Slope(); + + private static ValueEval invoke(Function function, ValueEval xArray, ValueEval yArray) { + ValueEval[] args = new ValueEval[] { xArray, yArray, }; + return function.evaluate(args, -1, (short)-1); + } + + private void confirm(Function function, ValueEval xArray, ValueEval yArray, double expected) { + ValueEval result = invoke(function, xArray, yArray); + assertEquals(NumberEval.class, result.getClass()); + assertEquals(expected, ((NumberEval)result).getNumberValue(), 0); + } + private void confirmError(Function function, ValueEval xArray, ValueEval yArray, ErrorEval expectedError) { + ValueEval result = invoke(function, xArray, yArray); + assertEquals(ErrorEval.class, result.getClass()); + assertEquals(expectedError.getErrorCode(), ((ErrorEval)result).getErrorCode()); + } + + private void confirmError(ValueEval xArray, ValueEval yArray, ErrorEval expectedError) { + confirmError(SLOPE, xArray, yArray, expectedError); + } + + public void testBasic() { + Double exp = Math.pow(10, 7.5); + ValueEval[] xValues = { + new NumberEval(3+exp), + new NumberEval(4+exp), + new NumberEval(2+exp), + new NumberEval(5+exp), + new NumberEval(4+exp), + new NumberEval(7+exp), + }; + ValueEval areaEvalX = createAreaEval(xValues); + + ValueEval[] yValues = { + new NumberEval(1), + new NumberEval(2), + new NumberEval(3), + new NumberEval(4), + new NumberEval(5), + new NumberEval(6), + }; + ValueEval areaEvalY = createAreaEval(yValues); + confirm(SLOPE, areaEvalX, areaEvalY, 0.7752808988764045); + // Excel 2010 gives 0.775280898876405 + } + + /** + * number of items in array is not limited to 30 + */ + public void testLargeArrays() { + ValueEval[] xValues = createMockNumberArray(100, 3); // [1,2,0,1,2,0,...,0,1] + xValues[0] = new NumberEval(2.0); // Changes first element to 2 + ValueEval[] yValues = createMockNumberArray(100, 101); // [1,2,3,4,...,99,100] + + confirm(SLOPE, createAreaEval(xValues), createAreaEval(yValues), -1.231527093596059); + // Excel 2010 gives -1.23152709359606 + } + + private ValueEval[] createMockNumberArray(int size, double value) { + ValueEval[] result = new ValueEval[size]; + for (int i = 0; i < result.length; i++) { + result[i] = new NumberEval((i+1)%value); + } + return result; + } + + private static ValueEval createAreaEval(ValueEval[] values) { + String refStr = "A1:A" + values.length; + return EvalFactory.createAreaEval(refStr, values); + } + + public void testErrors() { + ValueEval[] xValues = { + ErrorEval.REF_INVALID, + new NumberEval(2), + }; + ValueEval areaEvalX = createAreaEval(xValues); + ValueEval[] yValues = { + new NumberEval(2), + ErrorEval.NULL_INTERSECTION, + }; + ValueEval areaEvalY = createAreaEval(yValues); + ValueEval[] zValues = { // wrong size + new NumberEval(2), + }; + ValueEval areaEvalZ = createAreaEval(zValues); + + // if either arg is an error, that error propagates + confirmError(ErrorEval.REF_INVALID, ErrorEval.NAME_INVALID, ErrorEval.REF_INVALID); + confirmError(areaEvalX, ErrorEval.NAME_INVALID, ErrorEval.NAME_INVALID); + confirmError(ErrorEval.NAME_INVALID, areaEvalX, ErrorEval.NAME_INVALID); + + // array sizes must match + confirmError(areaEvalX, areaEvalZ, ErrorEval.NA); + confirmError(areaEvalZ, areaEvalY, ErrorEval.NA); + + // any error in an array item propagates up + confirmError(areaEvalX, areaEvalX, ErrorEval.REF_INVALID); + + // search for errors array by array, not pair by pair + confirmError(areaEvalX, areaEvalY, ErrorEval.REF_INVALID); + confirmError(areaEvalY, areaEvalX, ErrorEval.NULL_INTERSECTION); + } +}