diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 9ee283fcb..a5e467040 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 48539 - Added implementation for MROUND(), VAR() and VARP() 50446 - Code cleanup and optimizations to keep some IDE quiet 50437 - Support passing ranges to NPV() 50409 - Added implementation for IRR() diff --git a/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java b/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java index dca3774f5..53237caa0 100644 --- a/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java +++ b/src/java/org/apache/poi/ss/formula/atp/AnalysisToolPak.java @@ -138,7 +138,7 @@ public final class AnalysisToolPak implements UDFFinder { r(m, "JIS", null); r(m, "LCM", null); r(m, "MDURATION", null); - r(m, "MROUND", null); + r(m, "MROUND", MRound.instance); r(m, "MULTINOMIAL", null); r(m, "NETWORKDAYS", null); r(m, "NOMINAL", null); diff --git a/src/java/org/apache/poi/ss/formula/atp/MRound.java b/src/java/org/apache/poi/ss/formula/atp/MRound.java new file mode 100644 index 000000000..55ed87084 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/atp/MRound.java @@ -0,0 +1,76 @@ +/* ==================================================================== + 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.atp; + +import org.apache.poi.ss.formula.OperationEvaluationContext; +import org.apache.poi.ss.formula.eval.*; +import org.apache.poi.ss.formula.functions.FreeRefFunction; +import org.apache.poi.ss.formula.functions.NumericFunction; +import org.apache.poi.ss.usermodel.DateUtil; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.regex.Pattern; + +/** + * Implementation of Excel 'Analysis ToolPak' function MROUND()
+ * + * Returns a number rounded to the desired multiple.

+ * + * Syntax
+ * MROUND(number, multiple) + * + *

+ * + * @author Yegor Kozlov + */ +final class MRound implements FreeRefFunction { + + public static final FreeRefFunction instance = new MRound(); + + private MRound() { + // enforce singleton + } + + public ValueEval evaluate(ValueEval[] args, OperationEvaluationContext ec) { + double number, multiple, result; + + if (args.length != 2) { + return ErrorEval.VALUE_INVALID; + } + + try { + number = OperandResolver.coerceValueToDouble(OperandResolver.getSingleValue(args[0], ec.getRowIndex(), ec.getColumnIndex())); + multiple = OperandResolver.coerceValueToDouble(OperandResolver.getSingleValue(args[1], ec.getRowIndex(), ec.getColumnIndex())); + + if( multiple == 0.0 ) { + result = 0.0; + } else { + if(number*multiple < 0) { + // Returns #NUM! because the number and the multiple have different signs + throw new EvaluationException(ErrorEval.NUM_ERROR); + } + result = multiple * Math.round( number / multiple ); + } + NumericFunction.checkValue(result); + return new NumberEval(result); + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } +} 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 01fc068fe..a7bc15cee 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -93,6 +93,8 @@ public final class FunctionEval { retval[37] = BooleanFunction.OR; retval[38] = BooleanFunction.NOT; retval[39] = NumericFunction.MOD; + + retval[46] = AggregateFunction.VAR; retval[48] = TextFunction.TEXT; retval[56] = FinanceFunction.PV; @@ -153,6 +155,7 @@ public final class FunctionEval { retval[184] = NumericFunction.FACT; retval[190] = LogicalFunction.ISNONTEXT; + retval[194] = AggregateFunction.VARP; retval[197] = NumericFunction.TRUNC; retval[198] = LogicalFunction.ISLOGICAL; diff --git a/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java b/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java index 7a047dc70..3c465f17b 100644 --- a/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java +++ b/src/java/org/apache/poi/ss/formula/functions/AggregateFunction.java @@ -142,4 +142,20 @@ public abstract class AggregateFunction extends MultiOperandNumericFunction { return MathX.sumsq(values); } }; + public static final Function VAR = new AggregateFunction() { + protected double evaluate(double[] values) throws EvaluationException { + if (values.length < 1) { + throw new EvaluationException(ErrorEval.DIV_ZERO); + } + return StatsLib.var(values); + } + }; + public static final Function VARP = new AggregateFunction() { + protected double evaluate(double[] values) throws EvaluationException { + if (values.length < 1) { + throw new EvaluationException(ErrorEval.DIV_ZERO); + } + return StatsLib.varp(values); + } + }; } diff --git a/src/java/org/apache/poi/ss/formula/functions/NumericFunction.java b/src/java/org/apache/poi/ss/formula/functions/NumericFunction.java index 9235850e0..623cf65db 100644 --- a/src/java/org/apache/poi/ss/formula/functions/NumericFunction.java +++ b/src/java/org/apache/poi/ss/formula/functions/NumericFunction.java @@ -43,7 +43,7 @@ public abstract class NumericFunction implements Function { /** * @throws EvaluationException (#NUM!) if result is NaN or Infinity */ - static final void checkValue(double result) throws EvaluationException { + public static final void checkValue(double result) throws EvaluationException { if (Double.isNaN(result) || Double.isInfinite(result)) { throw new EvaluationException(ErrorEval.NUM_ERROR); } diff --git a/src/java/org/apache/poi/ss/formula/functions/StatsLib.java b/src/java/org/apache/poi/ss/formula/functions/StatsLib.java index 645f7e438..ebef9d4b2 100644 --- a/src/java/org/apache/poi/ss/formula/functions/StatsLib.java +++ b/src/java/org/apache/poi/ss/formula/functions/StatsLib.java @@ -59,6 +59,21 @@ final class StatsLib { return r; } + public static double var(double[] v) { + double r = Double.NaN; + if (v!=null && v.length > 1) { + r = devsq(v) / (v.length - 1); + } + return r; + } + + public static double varp(double[] v) { + double r = Double.NaN; + if (v!=null && v.length > 1) { + r = devsq(v) /v.length; + } + return r; + } public static double median(double[] v) { double r = Double.NaN; diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java index 5a48a228e..40cb33fae 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFBugs.java @@ -26,16 +26,8 @@ import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackagingURIHelper; -import org.apache.poi.ss.usermodel.BaseTestBugzillaIssues; -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.DataFormatter; -import org.apache.poi.ss.usermodel.Font; -import org.apache.poi.ss.usermodel.FormulaError; -import org.apache.poi.ss.usermodel.FormulaEvaluator; -import org.apache.poi.ss.usermodel.Name; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.ss.usermodel.Sheet; -import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.usermodel.*; +import org.apache.poi.ss.formula.eval.NotImplementedException; import org.apache.poi.xssf.XSSFITestDataProvider; import org.apache.poi.xssf.XSSFTestDataSamples; import org.apache.poi.xssf.model.CalculationChain; @@ -212,8 +204,10 @@ public final class TestXSSFBugs extends BaseTestBugzillaIssues { * NameXPtgs. * Blows up on: * IF(B6= (ROUNDUP(B6,0) + ROUNDDOWN(B6,0))/2, MROUND(B6,2),ROUND(B6,0)) + * + * TODO: delete this test case when MROUND and VAR are implemented */ - public void DISABLEDtest48539() throws Exception { + public void test48539() throws Exception { XSSFWorkbook wb = XSSFTestDataSamples.openSampleWorkbook("48539.xlsx"); assertEquals(3, wb.getNumberOfSheets()); @@ -224,14 +218,21 @@ public final class TestXSSFBugs extends BaseTestBugzillaIssues { for(Row r : s) { for(Cell c : r) { if(c.getCellType() == Cell.CELL_TYPE_FORMULA) { - eval.evaluate(c); + CellValue cv = eval.evaluate(c); + if(cv.getCellType() == Cell.CELL_TYPE_NUMERIC) { + // assert that the calculated value agrees with + // the cached formula result calculated by Excel + double cachedFormulaResult = c.getNumericCellValue(); + double evaluatedFormulaResult = cv.getNumberValue(); + assertEquals(c.getCellFormula(), cachedFormulaResult, evaluatedFormulaResult, 1E-7); + } } } } } // Now all of them - XSSFFormulaEvaluator.evaluateAllFormulaCells(wb); + XSSFFormulaEvaluator.evaluateAllFormulaCells(wb); } /** diff --git a/src/testcases/org/apache/poi/ss/formula/atp/TestMRound.java b/src/testcases/org/apache/poi/ss/formula/atp/TestMRound.java new file mode 100644 index 000000000..5f9acff9f --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/atp/TestMRound.java @@ -0,0 +1,71 @@ +/* ==================================================================== + 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.atp; + +import junit.framework.TestCase; +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.usermodel.*; + +/** + * Testcase for 'Analysis Toolpak' function MROUND() + * + * @author Yegor Kozlov + */ +public class TestMRound extends TestCase { + + /** +=MROUND(10, 3) Rounds 10 to a nearest multiple of 3 (9) +=MROUND(-10, -3) Rounds -10 to a nearest multiple of -3 (-9) +=MROUND(1.3, 0.2) Rounds 1.3 to a nearest multiple of 0.2 (1.4) +=MROUND(5, -2) Returns an error, because -2 and 5 have different signs (#NUM!) * + */ + public static void testEvaluate(){ + Workbook wb = new HSSFWorkbook(); + Sheet sh = wb.createSheet(); + Cell cell1 = sh.createRow(0).createCell(0); + cell1.setCellFormula("MROUND(10, 3)"); + Cell cell2 = sh.createRow(0).createCell(0); + cell2.setCellFormula("MROUND(-10, -3)"); + Cell cell3 = sh.createRow(0).createCell(0); + cell3.setCellFormula("MROUND(1.3, 0.2)"); + Cell cell4 = sh.createRow(0).createCell(0); + cell4.setCellFormula("MROUND(5, -2)"); + Cell cell5 = sh.createRow(0).createCell(0); + cell5.setCellFormula("MROUND(5, 0)"); + + double accuracy = 1E-9; + + FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + + assertEquals("Rounds 10 to a nearest multiple of 3 (9)", + 9.0, evaluator.evaluate(cell1).getNumberValue(), accuracy); + + assertEquals("Rounds -10 to a nearest multiple of -3 (-9)", + -9.0, evaluator.evaluate(cell2).getNumberValue(), accuracy); + + assertEquals("Rounds 1.3 to a nearest multiple of 0.2 (1.4)", + 1.4, evaluator.evaluate(cell3).getNumberValue(), accuracy); + + assertEquals("Returns an error, because -2 and 5 have different signs (#NUM!)", + ErrorEval.NUM_ERROR.getErrorCode(), evaluator.evaluate(cell4).getErrorValue()); + + assertEquals("Returns 0 because the multiple is 0", + 0.0, evaluator.evaluate(cell5).getNumberValue()); + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestStatsLib.java b/src/testcases/org/apache/poi/ss/formula/functions/TestStatsLib.java index 47ce2d5a9..cd0176f22 100644 --- a/src/testcases/org/apache/poi/ss/formula/functions/TestStatsLib.java +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestStatsLib.java @@ -268,4 +268,54 @@ public class TestStatsLib extends AbstractNumericTestCase { x = 3.02765035410; assertEquals("stdev ", x, d); } + + public void testVar() { + double[] v = null; + double d, x = 0; + + v = new double[] {3.50, 5.00, 7.23, 2.99}; + d = StatsLib.var(v); + x = 3.6178; + assertEquals("var ", x, d); + + v = new double[] {34.5, 2.0, 8.9, -4.0}; + d = StatsLib.var(v); + x = 286.99; + assertEquals("var ", x, d); + + v = new double[] {7.0, 25.0, 21.69}; + d = StatsLib.var(v); + x = 91.79203333; + assertEquals("var ", x, d); + + v = new double[] {1345,1301,1368,1322,1310,1370,1318,1350,1303,1299}; + d = StatsLib.var(v); + x = 754.2666667; + assertEquals("var ", x, d); + } + + public void testVarp() { + double[] v = null; + double d, x = 0; + + v = new double[] {3.50, 5.00, 7.23, 2.99}; + d = StatsLib.varp(v); + x = 2.71335; + assertEquals("varp ", x, d); + + v = new double[] {34.5, 2.0, 8.9, -4.0}; + d = StatsLib.varp(v); + x = 215.2425; + assertEquals("varp ", x, d); + + v = new double[] {7.0, 25.0, 21.69}; + d = StatsLib.varp(v); + x = 61.19468889; + assertEquals("varp ", x, d); + + v = new double[] {1345,1301,1368,1322,1310,1370,1318,1350,1303,1299}; + d = StatsLib.varp(v); + x = 678.84; + assertEquals("varp ", x, d); + } } diff --git a/test-data/spreadsheet/FormulaEvalTestData.xls b/test-data/spreadsheet/FormulaEvalTestData.xls index 35d18f700..919bf867e 100644 Binary files a/test-data/spreadsheet/FormulaEvalTestData.xls and b/test-data/spreadsheet/FormulaEvalTestData.xls differ