Bug 57007: Add initial implementations of DMIN and DGET functions

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1648166 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Dominik Stadler 2014-12-28 10:47:41 +00:00
parent 5c76ccba5b
commit a0248ff4f0
9 changed files with 575 additions and 0 deletions

View 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();

View File

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

View File

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

View File

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

View File

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

View File

@ -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";
}
}

View File

@ -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";
}
}

Binary file not shown.

Binary file not shown.