--- /dev/null
+/* ====================================================================
+ 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();
+ }
+}