From a0248ff4f0c400ff479b9cd81277b8a728f11ea4 Mon Sep 17 00:00:00 2001 From: Dominik Stadler Date: Sun, 28 Dec 2014 10:47:41 +0000 Subject: [PATCH] Bug 57007: Add initial implementations of DMIN and DGET functions git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1648166 13f79535-47bb-0310-9956-ffa450edef68 --- .../poi/ss/formula/eval/FunctionEval.java | 4 + .../apache/poi/ss/formula/functions/DGet.java | 59 +++ .../apache/poi/ss/formula/functions/DMin.java | 62 +++ .../poi/ss/formula/functions/DStarRunner.java | 369 ++++++++++++++++++ .../ss/formula/functions/IDStarAlgorithm.java | 27 ++ .../TestDGetFunctionsFromSpreadsheet.java | 27 ++ .../TestDStarFunctionsFromSpreadsheet.java | 27 ++ test-data/spreadsheet/DGet.xls | Bin 0 -> 39936 bytes test-data/spreadsheet/DStar.xls | Bin 0 -> 31232 bytes 9 files changed, 575 insertions(+) create mode 100644 src/java/org/apache/poi/ss/formula/functions/DGet.java create mode 100644 src/java/org/apache/poi/ss/formula/functions/DMin.java create mode 100644 src/java/org/apache/poi/ss/formula/functions/DStarRunner.java create mode 100644 src/java/org/apache/poi/ss/formula/functions/IDStarAlgorithm.java create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestDGetFunctionsFromSpreadsheet.java create mode 100644 src/testcases/org/apache/poi/ss/formula/functions/TestDStarFunctionsFromSpreadsheet.java create mode 100644 test-data/spreadsheet/DGet.xls create mode 100644 test-data/spreadsheet/DStar.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 b9cf3bafd..5ba894706 100644 --- a/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java +++ b/src/java/org/apache/poi/ss/formula/eval/FunctionEval.java @@ -98,6 +98,8 @@ public final class FunctionEval { retval[38] = BooleanFunction.NOT; retval[39] = NumericFunction.MOD; + retval[43] = new DStarRunner(new DMin()); + retval[46] = AggregateFunction.VAR; retval[48] = TextFunction.TEXT; @@ -188,6 +190,8 @@ public final class FunctionEval { retval[233] = NumericFunction.ACOSH; retval[234] = NumericFunction.ATANH; + retval[235] = new DStarRunner(new DGet()); + retval[FunctionID.EXTERNAL_FUNC] = null; // ExternalFunction is a FreeREfFunction retval[261] = new Errortype(); diff --git a/src/java/org/apache/poi/ss/formula/functions/DGet.java b/src/java/org/apache/poi/ss/formula/functions/DGet.java new file mode 100644 index 000000000..273c4eaac --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/DGet.java @@ -0,0 +1,59 @@ +/* ==================================================================== + 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.ErrorEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * Implementation of the DGet function: + * Finds the value of a column in an area with given conditions. + * + * TODO: + * - wildcards ? and * in string conditions + * - functions as conditions + */ +public final class DGet implements IDStarAlgorithm { + private ValueEval result; + + public void reset() { + result = null; + } + + public boolean processMatch(ValueEval eval) { + if(result == null) // First match, just set the value. + { + result = eval; + } + else // There was a previous match, since there is only exactly one allowed, bail out. + { + result = ErrorEval.NUM_ERROR; + return false; + } + + return true; + } + + public ValueEval getResult() { + if(result == null) { + return ErrorEval.VALUE_INVALID; + } else { + return result; + } + } +} diff --git a/src/java/org/apache/poi/ss/formula/functions/DMin.java b/src/java/org/apache/poi/ss/formula/functions/DMin.java new file mode 100644 index 000000000..6f996c55f --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/DMin.java @@ -0,0 +1,62 @@ +/* ==================================================================== + 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.NumberEval; +import org.apache.poi.ss.formula.eval.NumericValueEval; +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * Implementation of the DMin function: + * Finds the minimum value of a column in an area with given conditions. + * + * TODO: + * - wildcards ? and * in string conditions + * - functions as conditions + */ +public final class DMin implements IDStarAlgorithm { + private ValueEval minimumValue; + + public void reset() { + minimumValue = null; + } + + public boolean processMatch(ValueEval eval) { + if(eval instanceof NumericValueEval) { + if(minimumValue == null) { // First match, just set the value. + minimumValue = eval; + } else { // There was a previous match, find the new minimum. + double currentValue = ((NumericValueEval)eval).getNumberValue(); + double oldValue = ((NumericValueEval)minimumValue).getNumberValue(); + if(currentValue < oldValue) { + minimumValue = eval; + } + } + } + + return true; + } + + public ValueEval getResult() { + if(minimumValue == null) { + return NumberEval.ZERO; + } else { + return minimumValue; + } + } +} diff --git a/src/java/org/apache/poi/ss/formula/functions/DStarRunner.java b/src/java/org/apache/poi/ss/formula/functions/DStarRunner.java new file mode 100644 index 000000000..fbf074cdb --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/DStarRunner.java @@ -0,0 +1,369 @@ +/* ==================================================================== + 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.BlankEval; +import org.apache.poi.ss.formula.eval.ErrorEval; +import org.apache.poi.ss.formula.eval.EvaluationException; +import org.apache.poi.ss.formula.eval.NotImplementedException; +import org.apache.poi.ss.formula.eval.NumericValueEval; +import org.apache.poi.ss.formula.eval.RefEval; +import org.apache.poi.ss.formula.eval.StringValueEval; +import org.apache.poi.ss.formula.eval.ValueEval; +import org.apache.poi.ss.util.NumberComparer; + +/** + * This class performs a D* calculation. It takes an {@link IDStarAlgorithm} object and + * uses it for calculating the result value. Iterating a database and checking the + * entries against the set of conditions is done here. + */ +public final class DStarRunner implements Function3Arg { + private IDStarAlgorithm algorithm; + + public DStarRunner(IDStarAlgorithm algorithm) { + this.algorithm = algorithm; + } + + public final ValueEval evaluate(ValueEval[] args, int srcRowIndex, int srcColumnIndex) { + if(args.length == 3) { + return evaluate(srcRowIndex, srcColumnIndex, args[0], args[1], args[2]); + } + else { + return ErrorEval.VALUE_INVALID; + } + } + + public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, + ValueEval database, ValueEval filterColumn, ValueEval conditionDatabase) { + // Input processing and error checks. + if(!(database instanceof TwoDEval) || !(conditionDatabase instanceof TwoDEval)) { + return ErrorEval.VALUE_INVALID; + } + TwoDEval db = (TwoDEval)database; + TwoDEval cdb = (TwoDEval)conditionDatabase; + + int fc; + try { + fc = getColumnForName(filterColumn, db); + } + catch (EvaluationException e) { + return ErrorEval.VALUE_INVALID; + } + if(fc == -1) { // column not found + return ErrorEval.VALUE_INVALID; + } + + // Reset algorithm. + algorithm.reset(); + + // Iterate over all db entries. + for(int row = 1; row < db.getHeight(); ++row) { + boolean matches = true; + try { + matches = fullfillsConditions(db, row, cdb); + } + catch (EvaluationException e) { + return ErrorEval.VALUE_INVALID; + } + // Filter each entry. + if(matches) { + try { + ValueEval currentValueEval = solveReference(db.getValue(row, fc)); + // Pass the match to the algorithm and conditionally abort the search. + boolean shouldContinue = algorithm.processMatch(currentValueEval); + if(! shouldContinue) { + break; + } + } catch (EvaluationException e) { + return e.getErrorEval(); + } + } + } + + // Return the result of the algorithm. + return algorithm.getResult(); + } + + private enum operator { + largerThan, + largerEqualThan, + smallerThan, + smallerEqualThan, + equal + } + + /** + * Resolve reference(-chains) until we have a normal value. + * + * @param field a ValueEval which can be a RefEval. + * @return a ValueEval which is guaranteed not to be a RefEval + * @throws EvaluationException If a multi-sheet reference was found along the way. + */ + private static ValueEval solveReference(ValueEval field) throws EvaluationException { + if (field instanceof RefEval) { + RefEval refEval = (RefEval)field; + if (refEval.getNumberOfSheets() > 1) { + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + return solveReference(refEval.getInnerValueEval(refEval.getFirstSheetIndex())); + } + else { + return field; + } + } + + /** + * Returns the first column index that matches the given name. The name can either be + * a string or an integer, when it's an integer, then the respective column + * (1 based index) is returned. + * @param nameValueEval + * @param db + * @return the first column index that matches the given name (or int) + * @throws EvaluationException + */ + @SuppressWarnings("unused") + private static int getColumnForTag(ValueEval nameValueEval, TwoDEval db) + throws EvaluationException { + int resultColumn = -1; + + // Numbers as column indicator are allowed, check that. + if(nameValueEval instanceof NumericValueEval) { + double doubleResultColumn = ((NumericValueEval)nameValueEval).getNumberValue(); + resultColumn = (int)doubleResultColumn; + // Floating comparisions are usually not possible, but should work for 0.0. + if(doubleResultColumn - resultColumn != 0.0) + throw new EvaluationException(ErrorEval.VALUE_INVALID); + resultColumn -= 1; // Numbers are 1-based not 0-based. + } else { + resultColumn = getColumnForName(nameValueEval, db); + } + return resultColumn; + } + + private static int getColumnForName(ValueEval nameValueEval, TwoDEval db) + throws EvaluationException { + String name = getStringFromValueEval(nameValueEval); + return getColumnForString(db, name); + } + + /** + * For a given database returns the column number for a column heading. + * + * @param db Database. + * @param name Column heading. + * @return Corresponding column number. + * @throws EvaluationException If it's not possible to turn all headings into strings. + */ + private static int getColumnForString(TwoDEval db,String name) + throws EvaluationException { + int resultColumn = -1; + for(int column = 0; column < db.getWidth(); ++column) { + ValueEval columnNameValueEval = db.getValue(0, column); + String columnName = getStringFromValueEval(columnNameValueEval); + if(name.equals(columnName)) { + resultColumn = column; + break; + } + } + return resultColumn; + } + + /** + * Checks a row in a database against a condition database. + * + * @param db Database. + * @param row The row in the database to check. + * @param cdb The condition database to use for checking. + * @return Whether the row matches the conditions. + * @throws EvaluationException If references could not be resolved or comparison + * operators and operands didn't match. + */ + private static boolean fullfillsConditions(TwoDEval db, int row, TwoDEval cdb) + throws EvaluationException { + // Only one row must match to accept the input, so rows are ORed. + // Each row is made up of cells where each cell is a condition, + // all have to match, so they are ANDed. + for(int conditionRow = 1; conditionRow < cdb.getHeight(); ++conditionRow) { + boolean matches = true; + for(int column = 0; column < cdb.getWidth(); ++column) { // columns are ANDed + // Whether the condition column matches a database column, if not it's a + // special column that accepts formulas. + boolean columnCondition = true; + ValueEval condition = null; + try { + // The condition to apply. + condition = solveReference(cdb.getValue(conditionRow, column)); + } catch (java.lang.RuntimeException e) { + // It might be a special formula, then it is ok if it fails. + columnCondition = false; + } + // If the condition is empty it matches. + if(condition instanceof BlankEval) + continue; + // The column in the DB to apply the condition to. + ValueEval targetHeader = solveReference(cdb.getValue(0, column)); + targetHeader = solveReference(targetHeader); + + + if(!(targetHeader instanceof StringValueEval)) + columnCondition = false; + else if (getColumnForName(targetHeader, db) == -1) + // No column found, it's again a special column that accepts formulas. + columnCondition = false; + + if(columnCondition == true) { // normal column condition + // Should not throw, checked above. + ValueEval target = db.getValue( + row, getColumnForName(targetHeader, db)); + // Must be a string. + String conditionString = getStringFromValueEval(condition); + if(!testNormalCondition(target, conditionString)) { + matches = false; + break; + } + } else { // It's a special formula condition. + throw new NotImplementedException( + "D* function with formula conditions"); + } + } + if (matches == true) { + return true; + } + } + return false; + } + + /** + * Test a value against a simple (< > <= >= = starts-with) condition string. + * + * @param value The value to check. + * @param condition The condition to check for. + * @return Whether the condition holds. + * @throws EvaluationException If comparison operator and operands don't match. + */ + private static boolean testNormalCondition(ValueEval value, String condition) + throws EvaluationException { + if(condition.startsWith("<")) { // It's a ")) { // It's a >/>= condition. + String number = condition.substring(1); + if(number.startsWith("=")) { + number = number.substring(1); + return testNumericCondition(value, operator.largerEqualThan, number); + } else { + return testNumericCondition(value, operator.largerThan, number); + } + } + else if(condition.startsWith("=")) { // It's a = condition. + String stringOrNumber = condition.substring(1); + // Distinguish between string and number. + boolean itsANumber = false; + try { + Integer.parseInt(stringOrNumber); + itsANumber = true; + } catch (NumberFormatException e) { // It's not an int. + try { + Double.parseDouble(stringOrNumber); + itsANumber = true; + } catch (NumberFormatException e2) { // It's a string. + itsANumber = false; + } + } + if(itsANumber) { + return testNumericCondition(value, operator.equal, stringOrNumber); + } else { // It's a string. + String valueString = getStringFromValueEval(value); + return stringOrNumber.equals(valueString); + } + } else { // It's a text starts-with condition. + String valueString = getStringFromValueEval(value); + return valueString.startsWith(condition); + } + } + + /** + * Test whether a value matches a numeric condition. + * @param valueEval Value to check. + * @param op Comparator to use. + * @param condition Value to check against. + * @return whether the condition holds. + * @throws EvaluationException If it's impossible to turn the condition into a number. + */ + private static boolean testNumericCondition( + ValueEval valueEval, operator op, String condition) + throws EvaluationException { + // Construct double from ValueEval. + if(!(valueEval instanceof NumericValueEval)) + return false; + double value = ((NumericValueEval)valueEval).getNumberValue(); + + // Construct double from condition. + double conditionValue = 0.0; + try { + int intValue = Integer.parseInt(condition); + conditionValue = intValue; + } catch (NumberFormatException e) { // It's not an int. + try { + conditionValue = Double.parseDouble(condition); + } catch (NumberFormatException e2) { // It's not a double. + throw new EvaluationException(ErrorEval.VALUE_INVALID); + } + } + + int result = NumberComparer.compare(value, conditionValue); + switch(op) { + case largerThan: + return result > 0; + case largerEqualThan: + return result >= 0; + case smallerThan: + return result < 0; + case smallerEqualThan: + return result <= 0; + case equal: + return result == 0; + } + return false; // Can not be reached. + } + + /** + * Takes a ValueEval and tries to retrieve a String value from it. + * It tries to resolve references if there are any. + * + * @param value ValueEval to retrieve the string from. + * @return String corresponding to the given ValueEval. + * @throws EvaluationException If it's not possible to retrieve a String value. + */ + private static String getStringFromValueEval(ValueEval value) + throws EvaluationException { + value = solveReference(value); + if(value instanceof BlankEval) + return ""; + if(!(value instanceof StringValueEval)) + throw new EvaluationException(ErrorEval.VALUE_INVALID); + return ((StringValueEval)value).getStringValue(); + } +} diff --git a/src/java/org/apache/poi/ss/formula/functions/IDStarAlgorithm.java b/src/java/org/apache/poi/ss/formula/functions/IDStarAlgorithm.java new file mode 100644 index 000000000..21e7f949b --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/functions/IDStarAlgorithm.java @@ -0,0 +1,27 @@ +package org.apache.poi.ss.formula.functions; + +import org.apache.poi.ss.formula.eval.ValueEval; + +/** + * Interface specifying how an algorithm to be used by {@link DStarRunner} should look like. + * Each implementing class should correspond to one of the D* functions. + */ +public interface IDStarAlgorithm { + /** + * Reset the state of this algorithm. + * This is called before each run through a database. + */ + void reset(); + /** + * Process a match that is found during a run through a database. + * @param eval ValueEval of the cell in the matching row. References will already be resolved. + * @return Whether we should continue iterating through the database. + */ + boolean processMatch(ValueEval eval); + /** + * Return a result ValueEval that will be the result of the calculation. + * This is always called at the end of a run through the database. + * @return a ValueEval + */ + ValueEval getResult(); +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestDGetFunctionsFromSpreadsheet.java b/src/testcases/org/apache/poi/ss/formula/functions/TestDGetFunctionsFromSpreadsheet.java new file mode 100644 index 000000000..ebc9f7aaf --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestDGetFunctionsFromSpreadsheet.java @@ -0,0 +1,27 @@ +/* ==================================================================== + 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; + +/** +* Tests DGET() as loaded from a test data spreadsheet. +*/ +public class TestDGetFunctionsFromSpreadsheet extends BaseTestFunctionsFromSpreadsheet { + + protected String getFilename() { + return "DGet.xls"; + } +} diff --git a/src/testcases/org/apache/poi/ss/formula/functions/TestDStarFunctionsFromSpreadsheet.java b/src/testcases/org/apache/poi/ss/formula/functions/TestDStarFunctionsFromSpreadsheet.java new file mode 100644 index 000000000..da9375d8c --- /dev/null +++ b/src/testcases/org/apache/poi/ss/formula/functions/TestDStarFunctionsFromSpreadsheet.java @@ -0,0 +1,27 @@ +/* ==================================================================== + 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; + +/** +* Tests D*() functions as loaded from a test data spreadsheet. +*/ +public class TestDStarFunctionsFromSpreadsheet extends BaseTestFunctionsFromSpreadsheet { + + protected String getFilename() { + return "DStar.xls"; + } +} diff --git a/test-data/spreadsheet/DGet.xls b/test-data/spreadsheet/DGet.xls new file mode 100644 index 0000000000000000000000000000000000000000..e576d73a0327a5e5574f07e272ac0480f14d5586 GIT binary patch literal 39936 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 literal 0 HcmV?d00001 diff --git a/test-data/spreadsheet/DStar.xls b/test-data/spreadsheet/DStar.xls new file mode 100644 index 0000000000000000000000000000000000000000..0c89261426a89341c1521de4db5a393b5fa3ee9a GIT binary patch literal 31232 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 s^(zqQV;jQuQ&;+(5FIuixZpcDKp~Uzz