404 lines
16 KiB
Java
404 lines
16 KiB
Java
/* ====================================================================
|
|
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.AreaEval;
|
|
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.OperandResolver;
|
|
import org.apache.poi.ss.formula.eval.StringEval;
|
|
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.
|
|
*
|
|
* TODO:
|
|
* - wildcards ? and * in string conditions
|
|
* - functions as conditions
|
|
*/
|
|
public final class DStarRunner implements Function3Arg {
|
|
public enum DStarAlgorithmEnum {
|
|
DGET,
|
|
DMIN,
|
|
// DMAX, // DMAX is not yet implemented
|
|
}
|
|
private final DStarAlgorithmEnum algoType;
|
|
|
|
public DStarRunner(DStarAlgorithmEnum algorithm) {
|
|
this.algoType = 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 AreaEval) || !(conditionDatabase instanceof AreaEval)) {
|
|
return ErrorEval.VALUE_INVALID;
|
|
}
|
|
AreaEval db = (AreaEval)database;
|
|
AreaEval cdb = (AreaEval)conditionDatabase;
|
|
|
|
try {
|
|
filterColumn = OperandResolver.getSingleValue(filterColumn, srcRowIndex, srcColumnIndex);
|
|
} catch (EvaluationException e) {
|
|
return e.getErrorEval();
|
|
}
|
|
|
|
int fc;
|
|
try {
|
|
fc = getColumnForName(filterColumn, db);
|
|
}
|
|
catch (EvaluationException e) {
|
|
return ErrorEval.VALUE_INVALID;
|
|
}
|
|
if(fc == -1) { // column not found
|
|
return ErrorEval.VALUE_INVALID;
|
|
}
|
|
|
|
// Create an algorithm runner.
|
|
IDStarAlgorithm algorithm = null;
|
|
switch(algoType) {
|
|
case DGET: algorithm = new DGet(); break;
|
|
case DMIN: algorithm = new DMin(); break;
|
|
default:
|
|
throw new IllegalStateException("Unexpected algorithm type " + algoType + " encountered.");
|
|
}
|
|
|
|
// Iterate over all DB entries.
|
|
final int height = db.getHeight();
|
|
for(int row = 1; row < height; ++row) {
|
|
boolean matches = true;
|
|
try {
|
|
matches = fullfillsConditions(db, row, cdb);
|
|
}
|
|
catch (EvaluationException e) {
|
|
return ErrorEval.VALUE_INVALID;
|
|
}
|
|
// Filter each entry.
|
|
if(matches) {
|
|
ValueEval currentValueEval = resolveReference(db, row, fc);
|
|
// Pass the match to the algorithm and conditionally abort the search.
|
|
boolean shouldContinue = algorithm.processMatch(currentValueEval);
|
|
if(! shouldContinue) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the result of the algorithm.
|
|
return algorithm.getResult();
|
|
}
|
|
|
|
private enum operator {
|
|
largerThan,
|
|
largerEqualThan,
|
|
smallerThan,
|
|
smallerEqualThan,
|
|
equal
|
|
}
|
|
|
|
/**
|
|
*
|
|
*
|
|
* @param nameValueEval Must not be a RefEval or AreaEval. Thus make sure resolveReference() is called on the value first!
|
|
* @param db Database
|
|
* @return Corresponding column number.
|
|
* @throws EvaluationException
|
|
*/
|
|
private static int getColumnForName(ValueEval nameValueEval, AreaEval db)
|
|
throws EvaluationException {
|
|
String name = OperandResolver.coerceValueToString(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(AreaEval db,String name)
|
|
throws EvaluationException {
|
|
int resultColumn = -1;
|
|
final int width = db.getWidth();
|
|
for(int column = 0; column < width; ++column) {
|
|
ValueEval columnNameValueEval = resolveReference(db, 0, column);
|
|
if(columnNameValueEval instanceof BlankEval) {
|
|
continue;
|
|
}
|
|
if(columnNameValueEval instanceof ErrorEval) {
|
|
continue;
|
|
}
|
|
String columnName = OperandResolver.coerceValueToString(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(AreaEval db, int row, AreaEval 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.
|
|
final int height = cdb.getHeight();
|
|
for(int conditionRow = 1; conditionRow < height; ++conditionRow) {
|
|
boolean matches = true;
|
|
final int width = cdb.getWidth();
|
|
for(int column = 0; column < width; ++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;
|
|
|
|
// The condition to apply.
|
|
condition = resolveReference(cdb, conditionRow, column);
|
|
|
|
// If the condition is empty it matches.
|
|
if(condition instanceof BlankEval)
|
|
continue;
|
|
// The column in the DB to apply the condition to.
|
|
ValueEval targetHeader = resolveReference(cdb, 0, column);
|
|
|
|
if(!(targetHeader instanceof StringValueEval)) {
|
|
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
|
}
|
|
|
|
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 value = resolveReference(db, row, getColumnForName(targetHeader, db));
|
|
if(!testNormalCondition(value, condition)) {
|
|
matches = false;
|
|
break;
|
|
}
|
|
} else { // It's a special formula condition.
|
|
// TODO: Check whether the condition cell contains a formula and return #VALUE! if it doesn't.
|
|
if(OperandResolver.coerceValueToString(condition).isEmpty()) {
|
|
throw new EvaluationException(ErrorEval.VALUE_INVALID);
|
|
}
|
|
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, ValueEval condition)
|
|
throws EvaluationException {
|
|
if(condition instanceof StringEval) {
|
|
String conditionString = ((StringEval)condition).getStringValue();
|
|
|
|
if(conditionString.startsWith("<")) { // It's a </<= condition.
|
|
String number = conditionString.substring(1);
|
|
if(number.startsWith("=")) {
|
|
number = number.substring(1);
|
|
return testNumericCondition(value, operator.smallerEqualThan, number);
|
|
} else {
|
|
return testNumericCondition(value, operator.smallerThan, number);
|
|
}
|
|
}
|
|
else if(conditionString.startsWith(">")) { // It's a >/>= condition.
|
|
String number = conditionString.substring(1);
|
|
if(number.startsWith("=")) {
|
|
number = number.substring(1);
|
|
return testNumericCondition(value, operator.largerEqualThan, number);
|
|
} else {
|
|
return testNumericCondition(value, operator.largerThan, number);
|
|
}
|
|
}
|
|
else if(conditionString.startsWith("=")) { // It's a = condition.
|
|
String stringOrNumber = conditionString.substring(1);
|
|
|
|
if(stringOrNumber.isEmpty()) {
|
|
return value instanceof BlankEval;
|
|
}
|
|
// 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 = value instanceof BlankEval ? "" : OperandResolver.coerceValueToString(value);
|
|
return stringOrNumber.equals(valueString);
|
|
}
|
|
} else { // It's a text starts-with condition.
|
|
if(conditionString.isEmpty()) {
|
|
return value instanceof StringEval;
|
|
}
|
|
else {
|
|
String valueString = value instanceof BlankEval ? "" : OperandResolver.coerceValueToString(value);
|
|
return valueString.startsWith(conditionString);
|
|
}
|
|
}
|
|
}
|
|
else if(condition instanceof NumericValueEval) {
|
|
double conditionNumber = ((NumericValueEval)condition).getNumberValue();
|
|
Double valueNumber = getNumberFromValueEval(value);
|
|
if(valueNumber == null) {
|
|
return false;
|
|
}
|
|
|
|
return conditionNumber == valueNumber;
|
|
}
|
|
else if(condition instanceof ErrorEval) {
|
|
if(value instanceof ErrorEval) {
|
|
return ((ErrorEval)condition).getErrorCode() == ((ErrorEval)value).getErrorCode();
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
}
|
|
|
|
private static Double getNumberFromValueEval(ValueEval value) {
|
|
if(value instanceof NumericValueEval) {
|
|
return ((NumericValueEval)value).getNumberValue();
|
|
}
|
|
else if(value instanceof StringValueEval) {
|
|
String stringValue = ((StringValueEval)value).getStringValue();
|
|
try {
|
|
return Double.parseDouble(stringValue);
|
|
} catch (NumberFormatException e2) {
|
|
return null;
|
|
}
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a ValueEval that's in an AreaEval.
|
|
*
|
|
* @param db AreaEval from which the cell to resolve is retrieved.
|
|
* @param dbRow Relative row in the AreaEval.
|
|
* @param dbCol Relative column in the AreaEval.
|
|
* @return A ValueEval that is a NumberEval, StringEval, BoolEval, BlankEval or ErrorEval.
|
|
*/
|
|
private static ValueEval resolveReference(AreaEval db, int dbRow, int dbCol) {
|
|
try {
|
|
return OperandResolver.getSingleValue(db.getValue(dbRow, dbCol), db.getFirstRow()+dbRow, db.getFirstColumn()+dbCol);
|
|
} catch (EvaluationException e) {
|
|
return e.getErrorEval();
|
|
}
|
|
}
|
|
}
|