From 0023138765e8326206bb374d9c55fd7163b9a285 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Fri, 28 Dec 2012 12:50:15 +0000 Subject: [PATCH] Bugzilla 54356: Support of statistical function INTERCEPT git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1426485 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/status.xml | 1 + .../poi/ss/formula/eval/FunctionEval.java | 3 + .../poi/ss/formula/functions/Intercept.java | 223 ++++++++++++++++++ .../ss/formula/functions/TestIntercept.java | 159 +++++++++++++ test-data/spreadsheet/intercept.xls | Bin 0 -> 23552 bytes 5 files changed, 386 insertions(+) create mode 100644 src/java/org/apache/poi/ss/formula/functions/Intercept.java create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestIntercept.java create mode 100644 test-data/spreadsheet/intercept.xls diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 8fef9acb2..2016c13d2 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 54356 - Support for INTERCEPT function 54282 - Improve the performance of ColumnHelper addCleanColIntoCols, speeds up some .xlsx file loading 53650 - Prevent unreadable content and disalow to overwrite rows from input template in SXSSF 54228,53672 - Fixed XSSF to read cells with missing R attribute 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 c76da52c1..021d6980f 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -28,6 +28,7 @@ import java.util.TreeSet; /** * @author Amol S. Deshmukh < amolweb at ya hoo dot com > + * @author Johan Karlsteen - added Intercept */ public final class FunctionEval { /** @@ -208,6 +209,8 @@ public final class FunctionEval { retval[304] = new Sumx2my2(); retval[305] = new Sumx2py2(); + retval[311] = new Intercept(); + retval[318] = AggregateFunction.DEVSQ; retval[321] = AggregateFunction.SUMSQ; diff --git a/src/java/org/apache/poi/ss/formula/functions/Intercept.java b/src/java/org/apache/poi/ss/formula/functions/Intercept.java new file mode 100644 index 000000000..06bb6f97d --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/Intercept.java @@ -0,0 +1,223 @@ +/* + * ==================================================================== + * 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; + +/** + * Implementation of Excel function INTERCEPT()

+ * + * Calculates the INTERCEPT 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:
+ * INTERCEPT(arrayX, arrayY)

+ * + * + * @author Johan Karlsteen + */ +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 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 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); + } + + 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/testcases/org/apache/poi/ss/formula/functions/TestIntercept.java b/src/testcases/org/apache/poi/ss/formula/functions/TestIntercept.java new file mode 100644 index 000000000..942ab6b01 --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestIntercept.java @@ -0,0 +1,159 @@ +/* + * ==================================================================== + * 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.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.usermodel.HSSFCell; +import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator; +import org.apache.poi.hssf.usermodel.HSSFSheet; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +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 INTERCEPT() + * + * @author Johan Karlsteen + */ +public final class TestIntercept extends TestCase { + private static final Function INTERCEPT = new Intercept(); + + 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(INTERCEPT, 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(INTERCEPT, areaEvalX, areaEvalY, -24516534.39905822); + // Excel 2010 gives -24516534.3990583 + } + + /** + * 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(INTERCEPT, createAreaEval(xValues), createAreaEval(yValues), 51.74384236453202); + // Excel 2010 gives 51.74384236453200 + } + + 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); + } + + /** + * Example from + * http://office.microsoft.com/en-us/excel-help/intercept-function-HP010062512.aspx?CTT=5&origin=HA010277524 + */ + public void testFromFile() { + + HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook("intercept.xls"); + HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(wb); + + HSSFSheet example1 = wb.getSheet("Example 1"); + HSSFCell a8 = example1.getRow(7).getCell(0); + assertEquals("INTERCEPT(A2:A6,B2:B6)", a8.getCellFormula()); + fe.evaluate(a8); + assertEquals(0.048387097, a8.getNumericCellValue(), 0.000000001); + + } +} \ No newline at end of file diff --git a/test-data/spreadsheet/intercept.xls b/test-data/spreadsheet/intercept.xls new file mode 100644 index 0000000000000000000000000000000000000000..b734887c39a891e6e2448686066471f97a7c7ac5 GIT binary patch literal 23552 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 literal 0 HcmV?d00001