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 000000000..e576d73a0 Binary files /dev/null and b/test-data/spreadsheet/DGet.xls differ diff --git a/test-data/spreadsheet/DStar.xls b/test-data/spreadsheet/DStar.xls new file mode 100644 index 000000000..0c8926142 Binary files /dev/null and b/test-data/spreadsheet/DStar.xls differ