]> source.dussan.org Git - poi.git/commitdiff
Bug 57007: Add initial implementations of DMIN and DGET functions
authorDominik Stadler <centic@apache.org>
Sun, 28 Dec 2014 10:47:41 +0000 (10:47 +0000)
committerDominik Stadler <centic@apache.org>
Sun, 28 Dec 2014 10:47:41 +0000 (10:47 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1648166 13f79535-47bb-0310-9956-ffa450edef68

src/java/org/apache/poi/ss/formula/eval/FunctionEval.java
src/java/org/apache/poi/ss/formula/functions/DGet.java [new file with mode: 0644]
src/java/org/apache/poi/ss/formula/functions/DMin.java [new file with mode: 0644]
src/java/org/apache/poi/ss/formula/functions/DStarRunner.java [new file with mode: 0644]
src/java/org/apache/poi/ss/formula/functions/IDStarAlgorithm.java [new file with mode: 0644]
src/testcases/org/apache/poi/ss/formula/functions/TestDGetFunctionsFromSpreadsheet.java [new file with mode: 0644]
src/testcases/org/apache/poi/ss/formula/functions/TestDStarFunctionsFromSpreadsheet.java [new file with mode: 0644]
test-data/spreadsheet/DGet.xls [new file with mode: 0644]
test-data/spreadsheet/DStar.xls [new file with mode: 0644]

index b9cf3bafdc9ad5891cab79827c393ef699dc37e0..5ba89470675ff9e42c99ba0c22a927b449f5081e 100644 (file)
@@ -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 (file)
index 0000000..273c4ea
--- /dev/null
@@ -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 (file)
index 0000000..6f996c5
--- /dev/null
@@ -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 (file)
index 0000000..fbf074c
--- /dev/null
@@ -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 </<= condition.
+            String number = condition.substring(1);
+            if(number.startsWith("=")) {
+                number = number.substring(1);
+                return testNumericCondition(value, operator.smallerEqualThan, number);
+            } else {
+                return testNumericCondition(value, operator.smallerThan, number);
+            }
+        }
+        else if(condition.startsWith(">")) { // 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 (file)
index 0000000..21e7f94
--- /dev/null
@@ -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 (file)
index 0000000..ebc9f7a
--- /dev/null
@@ -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 (file)
index 0000000..da9375d
--- /dev/null
@@ -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 (file)
index 0000000..e576d73
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 (file)
index 0000000..0c89261
Binary files /dev/null and b/test-data/spreadsheet/DStar.xls differ