|
|
@@ -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(); |
|
|
|
} |
|
|
|
} |