From 4d3719144e22b53e703936f0a17f51ce022a9e92 Mon Sep 17 00:00:00 2001 From: Yegor Kozlov Date: Fri, 2 Nov 2018 13:34:28 +0000 Subject: [PATCH] Bug 62836: Implementation of Excel TREND function git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1845586 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/ss/formula/eval/FunctionEval.java | 2 +- .../poi/ss/formula/functions/Trend.java | 377 ++++++++++++++++++ .../functions/AllSpreadsheetBasedTests.java | 1 + .../TestTrendFunctionsFromSpreadsheet.java | 31 ++ test-data/spreadsheet/Trend.xls | Bin 0 -> 48128 bytes 5 files changed, 410 insertions(+), 1 deletion(-) create mode 100644 src/java/org/apache/poi/ss/formula/functions/Trend.java create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestTrendFunctionsFromSpreadsheet.java create mode 100644 test-data/spreadsheet/Trend.xls 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 8442f5832..961a9cd81 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -115,7 +115,7 @@ public final class FunctionEval { // 47: DVAR retval[48] = TextFunction.TEXT; // 49: LINEST - // 50: TREND + retval[50] = new Trend(); // 51: LOGEST // 52: GROWTH diff --git a/src/java/org/apache/poi/ss/formula/functions/Trend.java b/src/java/org/apache/poi/ss/formula/functions/Trend.java new file mode 100644 index 000000000..155c1a57a --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/Trend.java @@ -0,0 +1,377 @@ +/* ==================================================================== + 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. +==================================================================== */ + +/* + * Notes: + * Duplicate x values don't work most of the time because of the way the + * math library handles multiple regression. + * The math library currently fails when the number of x variables is >= + * the sample size (see https://github.com/Hipparchus-Math/hipparchus/issues/13). + */ + +package org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.formula.CacheAreaEval; +import org.apache.poi.ss.formula.eval.AreaEval; +import org.apache.poi.ss.formula.eval.BoolEval; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.formula.eval.MissingArgEval; +import org.apache.poi.ss.formula.eval.NotImplementedException; +import org.apache.poi.ss.formula.eval.NumberEval; +import org.apache.poi.ss.formula.eval.NumericValueEval; +import org.apache.poi.ss.formula.eval.RefEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.commons.math3.linear.SingularMatrixException; +import org.apache.commons.math3.stat.regression.OLSMultipleLinearRegression; + +import java.util.Arrays; + + +/** + * Implementation for the Excel function TREND

+ * + * Syntax:
+ * TREND(known_y's, known_x's, new_x's, constant) + * + * + * + *
known_y's, known_x's, new_x'stypically area references, possibly cell references or scalar values
constantTRUE or FALSE: + * determines whether the regression line should include an intercept term

+ * If known_x's is not given, it is assumed to be the default array {1, 2, 3, ...} + * of the same size as known_y's.
+ * If new_x's is not given, it is assumed to be the same as known_x's
+ * If constant is omitted, it is assumed to be TRUE + *

+ */ + +public final class Trend implements Function { + MatrixFunction.MutableValueCollector collector = new MatrixFunction.MutableValueCollector(false, false); + private static final class TrendResults { + public double[] vals; + public int resultWidth; + public int resultHeight; + + public TrendResults(double[] vals, int resultWidth, int resultHeight) { + this.vals = vals; + this.resultWidth = resultWidth; + this.resultHeight = resultHeight; + } + } + + public ValueEval evaluate(ValueEval[] args, int srcRowIndex, int srcColumnIndex) { + if (args.length < 1 || args.length > 4) { + return ErrorEval.VALUE_INVALID; + } + try { + TrendResults tr = getNewY(args); + ValueEval[] vals = new ValueEval[tr.vals.length]; + for (int i = 0; i < tr.vals.length; i++) { + vals[i] = new NumberEval(tr.vals[i]); + } + if (tr.vals.length == 1) { + return vals[0]; + } + return new CacheAreaEval(srcRowIndex, srcColumnIndex, srcRowIndex + tr.resultHeight - 1, srcColumnIndex + tr.resultWidth - 1, vals); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + + private static double[][] evalToArray(ValueEval arg) throws EvaluationException { + double[][] ar; + ValueEval eval; + if (arg instanceof MissingArgEval) { + return new double[0][0]; + } + if (arg instanceof RefEval) { + RefEval re = (RefEval) arg; + if (re.getNumberOfSheets() > 1) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + eval = re.getInnerValueEval(re.getFirstSheetIndex()); + } else { + eval = arg; + } + if (eval == null) { + throw new RuntimeException("Parameter may not be null."); + } + + if (eval instanceof AreaEval) { + AreaEval ae = (AreaEval) eval; + int w = ae.getWidth(); + int h = ae.getHeight(); + ar = new double[h][w]; + for (int i = 0; i < h; i++) { + for (int j = 0; j < w; j++) { + ValueEval ve = ae.getRelativeValue(i, j); + if (!(ve instanceof NumericValueEval)) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + ar[i][j] = ((NumericValueEval)ve).getNumberValue(); + } + } + } else if (eval instanceof NumericValueEval) { + ar = new double[1][1]; + ar[0][0] = ((NumericValueEval)eval).getNumberValue(); + } else { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + return ar; + } + + private static double[][] getDefaultArrayOneD(int w) { + double[][] array = new double[w][1]; + for (int i = 0; i < w; i++) { + array[i][0] = i + 1; + } + return array; + } + + private static double[] flattenArray(double[][] twoD) { + if (twoD.length < 1) { + return new double[0]; + } + double[] oneD = new double[twoD.length * twoD[0].length]; + for (int i = 0; i < twoD.length; i++) { + for (int j = 0; j < twoD[0].length; j++) { + oneD[i * twoD[0].length + j] = twoD[i][j]; + } + } + return oneD; + } + + private static double[][] flattenArrayToRow(double[][] twoD) { + if (twoD.length < 1) { + return new double[0][0]; + } + double[][] oneD = new double[twoD.length * twoD[0].length][1]; + for (int i = 0; i < twoD.length; i++) { + for (int j = 0; j < twoD[0].length; j++) { + oneD[i * twoD[0].length + j][0] = twoD[i][j]; + } + } + return oneD; + } + + private static double[][] switchRowsColumns(double[][] array) { + double[][] newArray = new double[array[0].length][array.length]; + for (int i = 0; i < array.length; i++) { + for (int j = 0; j < array[0].length; j++) { + newArray[j][i] = array[i][j]; + } + } + return newArray; + } + + /** + * Check if all columns in a matrix contain the same values. + * Return true if the number of distinct values in each column is 1. + * + * @param matrix column-oriented matrix. A Row matrix should be transposed to column . + * @return true if all columns contain the same value + */ + private static boolean isAllColumnsSame(double[][] matrix){ + if(matrix.length == 0) return false; + + boolean[] cols = new boolean[matrix[0].length]; + for (int j = 0; j < matrix[0].length; j++) { + double prev = Double.NaN; + for (int i = 0; i < matrix.length; i++) { + double v = matrix[i][j]; + if(i > 0 && v != prev) { + cols[j] = true; + break; + } + prev = v; + } + } + boolean allEquals = true; + for (boolean x : cols) { + if(x) { + allEquals = false; + break; + } + }; + return allEquals; + + } + + private static TrendResults getNewY(ValueEval[] args) throws EvaluationException { + double[][] xOrig; + double[][] x; + double[][] yOrig; + double[] y; + double[][] newXOrig; + double[][] newX; + double[][] resultSize; + boolean passThroughOrigin = false; + switch (args.length) { + case 1: + yOrig = evalToArray(args[0]); + xOrig = new double[0][0]; + newXOrig = new double[0][0]; + break; + case 2: + yOrig = evalToArray(args[0]); + xOrig = evalToArray(args[1]); + newXOrig = new double[0][0]; + break; + case 3: + yOrig = evalToArray(args[0]); + xOrig = evalToArray(args[1]); + newXOrig = evalToArray(args[2]); + break; + case 4: + yOrig = evalToArray(args[0]); + xOrig = evalToArray(args[1]); + newXOrig = evalToArray(args[2]); + if (!(args[3] instanceof BoolEval)) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + // The argument in Excel is false when it *should* pass through the origin. + passThroughOrigin = !((BoolEval)args[3]).getBooleanValue(); + break; + default: + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + + if (yOrig.length < 1) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + y = flattenArray(yOrig); + newX = newXOrig; + + if (newXOrig.length > 0) { + resultSize = newXOrig; + } else { + resultSize = new double[1][1]; + } + + if (y.length == 1) { + /* See comment at top of file + if (xOrig.length > 0 && !(xOrig.length == 1 || xOrig[0].length == 1)) { + throw new EvaluationException(ErrorEval.REF_INVALID); + } else if (xOrig.length < 1) { + x = new double[1][1]; + x[0][0] = 1; + } else { + x = new double[1][]; + x[0] = flattenArray(xOrig); + if (newXOrig.length < 1) { + resultSize = xOrig; + } + }*/ + throw new NotImplementedException("Sample size too small"); + } else if (yOrig.length == 1 || yOrig[0].length == 1) { + if (xOrig.length < 1) { + x = getDefaultArrayOneD(y.length); + if (newXOrig.length < 1) { + resultSize = yOrig; + } + } else { + x = xOrig; + if (xOrig[0].length > 1 && yOrig.length == 1) { + x = switchRowsColumns(x); + } + if (newXOrig.length < 1) { + resultSize = xOrig; + } + } + if (newXOrig.length > 0 && (x.length == 1 || x[0].length == 1)) { + newX = flattenArrayToRow(newXOrig); + } + } else { + if (xOrig.length < 1) { + x = getDefaultArrayOneD(y.length); + if (newXOrig.length < 1) { + resultSize = yOrig; + } + } else { + x = flattenArrayToRow(xOrig); + if (newXOrig.length < 1) { + resultSize = xOrig; + } + } + if (newXOrig.length > 0) { + newX = flattenArrayToRow(newXOrig); + } + if (y.length != x.length || yOrig.length != xOrig.length) { + throw new EvaluationException(ErrorEval.REF_INVALID); + } + } + + if (newXOrig.length < 1) { + newX = x; + } else if (newXOrig.length == 1 && newXOrig[0].length > 1 && xOrig.length > 1 && xOrig[0].length == 1) { + newX = switchRowsColumns(newXOrig); + } + + if (newX[0].length != x[0].length) { + throw new EvaluationException(ErrorEval.REF_INVALID); + } + + if (x[0].length >= x.length) { + /* See comment at top of file */ + throw new NotImplementedException("Sample size too small"); + } + + int resultHeight = resultSize.length; + int resultWidth = resultSize[0].length; + + if(isAllColumnsSame(x)){ + double[] result = new double[newX.length]; + double avg = Arrays.stream(y).average().orElse(0); + for(int i = 0; i < result.length; i++) result[i] = avg; + return new TrendResults(result, resultWidth, resultHeight); + } + + OLSMultipleLinearRegression reg = new OLSMultipleLinearRegression(); + if (passThroughOrigin) { + reg.setNoIntercept(true); + } + + try { + reg.newSampleData(y, x); + } catch (IllegalArgumentException e) { + throw new EvaluationException(ErrorEval.REF_INVALID); + } + double[] par; + try { + par = reg.estimateRegressionParameters(); + } catch (SingularMatrixException e) { + throw new NotImplementedException("Singular matrix in input"); + } + + double[] result = new double[newX.length]; + for (int i = 0; i < newX.length; i++) { + result[i] = 0; + if (passThroughOrigin) { + for (int j = 0; j < par.length; j++) { + result[i] += par[j] * newX[i][j]; + } + } else { + result[i] = par[0]; + for (int j = 1; j < par.length; j++) { + result[i] += par[j] * newX[i][j - 1]; + } + } + } + return new TrendResults(result, resultWidth, resultHeight); + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/AllSpreadsheetBasedTests.java b/src/testcases/org/apache/poi/ss/formula/functions/AllSpreadsheetBasedTests.java index 2b34dcf8a..0dc5ed923 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/AllSpreadsheetBasedTests.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/AllSpreadsheetBasedTests.java @@ -41,6 +41,7 @@ import org.junit.runners.Suite; TestQuotientFunctionsFromSpreadsheet.class, TestReptFunctionsFromSpreadsheet.class, TestRomanFunctionsFromSpreadsheet.class, + TestTrendFunctionsFromSpreadsheet.class, TestWeekNumFunctionsFromSpreadsheet.class, TestWeekNumFunctionsFromSpreadsheet2013.class }) diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestTrendFunctionsFromSpreadsheet.java b/src/testcases/org/apache/poi/ss/formula/functions/TestTrendFunctionsFromSpreadsheet.java new file mode 100644 index 000000000..51871d16e --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestTrendFunctionsFromSpreadsheet.java @@ -0,0 +1,31 @@ +/* ==================================================================== + 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 java.util.Collection; + +import org.junit.runners.Parameterized.Parameters; + +/** +* Tests TREND() as loaded from a test data spreadsheet. +*/ +public class TestTrendFunctionsFromSpreadsheet extends BaseTestFunctionsFromSpreadsheet { + @Parameters(name="{0}") + public static Collection data() throws Exception { + return data(TestTrendFunctionsFromSpreadsheet.class, "Trend.xls"); + } +} diff --git a/test-data/spreadsheet/Trend.xls b/test-data/spreadsheet/Trend.xls new file mode 100644 index 0000000000000000000000000000000000000000..8a88709363434ed227c4a64e5d4907c046c7ee92 GIT binary patch literal 48128 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 literal 0 HcmV?d00001