[bug-67475] better support for edge cases in TEXT function. Thanks to Jakub Vojtisek

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1912973 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
PJ Fanning 2023-10-14 22:34:58 +00:00
parent b48de0fe42
commit dbddc32c1b
2 changed files with 184 additions and 15 deletions

View File

@ -20,12 +20,15 @@ package org.apache.poi.ss.formula.functions;
import java.util.Locale;
import org.apache.poi.ss.formula.eval.AreaEval;
import org.apache.poi.ss.formula.eval.BlankEval;
import org.apache.poi.ss.formula.eval.BoolEval;
import org.apache.poi.ss.formula.eval.ErrorEval;
import org.apache.poi.ss.formula.eval.EvaluationException;
import org.apache.poi.ss.formula.eval.NumberEval;
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.usermodel.DataFormatter;
@ -342,22 +345,57 @@ public abstract class TextFunction implements Function {
@Override
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) {
double s0;
String s1;
try {
s0 = evaluateDoubleArg(arg0, srcRowIndex, srcColumnIndex);
s1 = evaluateStringArg(arg1, srcRowIndex, srcColumnIndex);
} catch (EvaluationException e) {
return e.getErrorEval();
}
ValueEval valueEval;
try {
// Ask DataFormatter to handle the String for us
String formattedStr = formatter.formatRawCellContents(s0, -1, s1);
return new StringEval(formattedStr);
} catch (Exception e) {
return ErrorEval.VALUE_INVALID;
ValueEval valueVe = OperandResolver.getSingleValue(arg0, srcRowIndex, srcColumnIndex);
ValueEval formatVe = OperandResolver.getSingleValue(arg1, srcRowIndex, srcColumnIndex);
try {
Double valueDouble = null;
String evaluated = null;
if (valueVe == BlankEval.instance) {
valueDouble = 0.0;
} else if (valueVe instanceof BoolEval) {
evaluated = ((BoolEval) valueVe).getStringValue();
} else if (valueVe instanceof NumericValueEval) {
valueDouble = ((NumericValueEval) valueVe).getNumberValue();
} else if (valueVe instanceof StringEval) {
evaluated = ((StringEval) valueVe).getStringValue();
valueDouble = OperandResolver.parseDouble(evaluated);
}
if (valueDouble != null) {
String format = formatPatternValueEval2String(formatVe);
evaluated = formatter.formatRawCellContents(valueDouble, -1, format);
}
valueEval = new StringEval(evaluated);
} catch (Exception e) {
valueEval = ErrorEval.VALUE_INVALID;
}
} catch (EvaluationException e) {
valueEval = e.getErrorEval();
}
return valueEval;
}
/**
* Using it instead of {@link OperandResolver#coerceValueToString(ValueEval)} in order to handle booleans differently.
*/
private String formatPatternValueEval2String(ValueEval ve) {
String format = null;
if (!(ve instanceof BoolEval) && (ve instanceof StringValueEval)) {
StringValueEval sve = (StringValueEval) ve;
format = sve.getStringValue();
} else if (ve == BlankEval.instance) {
format = "";
} else {
throw new IllegalArgumentException("Unexpected eval class (" + ve.getClass().getName() + ")");
}
return format;
}
};

View File

@ -18,12 +18,17 @@
package org.apache.poi.ss.formula.functions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.text.DateFormatSymbols;
import java.text.DecimalFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.List;
import java.util.TimeZone;
import org.apache.poi.ss.formula.eval.BlankEval;
import org.apache.poi.ss.formula.eval.BoolEval;
import org.apache.poi.ss.formula.eval.ErrorEval;
import org.apache.poi.ss.formula.eval.NumberEval;
import org.apache.poi.ss.formula.eval.StringEval;
@ -35,13 +40,16 @@ import org.junit.jupiter.api.Test;
* Test case for TEXT()
*/
final class TestText {
private static final List<ErrorEval> EXCEL_ERRORS = Arrays.asList(ErrorEval.NULL_INTERSECTION, ErrorEval.DIV_ZERO,
ErrorEval.VALUE_INVALID, ErrorEval.REF_INVALID, ErrorEval.NAME_INVALID, ErrorEval.NUM_ERROR, ErrorEval.NA);
@Test
void testTextWithStringFirstArg() {
ValueEval strArg = new StringEval("abc");
ValueEval formatArg = new StringEval("abc");
ValueEval[] args = { strArg, formatArg };
ValueEval result = TextFunction.TEXT.evaluate(args, -1, (short)-1);
assertEquals(ErrorEval.VALUE_INVALID, result);
assertEquals(strArg.toString(), result.toString());
}
@Test
@ -159,7 +167,130 @@ final class TestText {
result = TextFunction.TEXT.evaluate(args, -1, -1);
testResult = new StringEval("1900-11-16T07:42:14.400");
assertEquals(testResult.toString(), result.toString());
}
// Test cases from the workbook attached to the bug 67475 which were OK
@Test
void testTextVariousValidNumberFormats() {
// negative values: 3 decimals
testText(new NumberEval(-123456.789012345), new StringEval("#0.000"), "-123456.789");
// no decimals
testText(new NumberEval(-123456.789012345), new StringEval("000000"), "-123457");
// common format - more digits
testText(new NumberEval(-123456.789012345), new StringEval("00.0000"), "-123456.7890");
// common format - less digits
testText(new NumberEval(-12.78), new StringEval("00000.000000"), "-00012.780000");
// half up
testText(new NumberEval(-0.56789012375), new StringEval("#0.0000000000"), "-0.5678901238");
// half up
testText(new NumberEval(-0.56789012385), new StringEval("#0.0000000000"), "-0.5678901239");
// positive values: 3 decimals
testText(new NumberEval(123456.789012345), new StringEval("#0.000"), "123456.789");
// no decimals
testText(new NumberEval(123456.789012345), new StringEval("000000"), "123457");
// common format - more digits
testText(new NumberEval(123456.789012345), new StringEval("00.0000"), "123456.7890");
// common format - less digits
testText(new NumberEval(12.78), new StringEval("00000.000000"), "00012.780000");
// half up
testText(new NumberEval(0.56789012375), new StringEval("#0.0000000000"), "0.5678901238");
// half up
testText(new NumberEval(0.56789012385), new StringEval("#0.0000000000"), "0.5678901239");
}
@Test
void testTextBlankTreatedAsZero() {
testText(BlankEval.instance, new StringEval("#0.000"), "0.000");
}
@Test
void testTextStrangeFormat() {
// number 0
testText(new NumberEval(-123456.789012345), new NumberEval(0), "-123457");
// negative number with few zeros
testText(new NumberEval(-123456.789012345), new NumberEval(-0.0001), "--123456.7891");
// format starts with "."
testText(new NumberEval(0.0123), new StringEval(".000"), ".012");
// one zero negative
testText(new NumberEval(1001.202), new NumberEval(-8808), "-8810018");
// format contains 0
testText(new NumberEval(43368.0), new NumberEval(909), "9433689");
}
@Test
void testTextErrorAsFormat() {
for (ErrorEval errorEval : EXCEL_ERRORS) {
testText(new NumberEval(3.14), errorEval, errorEval);
testText(BoolEval.TRUE, errorEval, errorEval);
testText(BoolEval.FALSE, errorEval, errorEval);
}
}
@Test
void testTextErrorAsValue() {
for (ErrorEval errorEval : EXCEL_ERRORS) {
testText(errorEval, new StringEval("#0.000"), errorEval);
testText(errorEval, new StringEval("yyyymmmdd"), errorEval);
}
}
// Test cases from the workbook attached to the bug 67475 which were failing and are fixed by the patch
@Test
void testTextEmptyStringWithDateFormat() {
testText(new StringEval(""), new StringEval("yyyymmmdd"), "");
}
@Test
void testTextAnyTextWithDateFormat() {
testText(new StringEval("anyText"), new StringEval("yyyymmmdd"), "anyText");
}
@Test
void testTextBooleanWithDateFormat() {
testText(BoolEval.TRUE, new StringEval("yyyymmmdd"), BoolEval.TRUE.getStringValue());
testText(BoolEval.FALSE, new StringEval("yyyymmmdd"), BoolEval.FALSE.getStringValue());
}
@Test
void testTextNumberWithBooleanFormat() {
testText(new NumberEval(43368), BoolEval.TRUE, ErrorEval.VALUE_INVALID);
testText(new NumberEval(43368), BoolEval.FALSE, ErrorEval.VALUE_INVALID);
testText(new NumberEval(3.14), BoolEval.TRUE, ErrorEval.VALUE_INVALID);
testText(new NumberEval(3.14), BoolEval.FALSE, ErrorEval.VALUE_INVALID);
}
@Test
void testTextEmptyStringWithNumberFormat() {
testText(new StringEval(""), new StringEval("#0.000"), "");
}
@Test
void testTextAnyTextWithNumberFormat() {
testText(new StringEval("anyText"), new StringEval("#0.000"), "anyText");
}
@Test
void testTextBooleanWithNumberFormat() {
testText(BoolEval.TRUE, new StringEval("#0.000"), BoolEval.TRUE.getStringValue());
testText(BoolEval.FALSE, new StringEval("#0.000"), BoolEval.FALSE.getStringValue());
}
private void testText(ValueEval valueArg, ValueEval formatArg, String expectedResult) {
ValueEval[] args = { valueArg, formatArg };
ValueEval result = TextFunction.TEXT.evaluate(args, -1, -1);
assertTrue(result instanceof StringEval, "Expected StringEval got " + result.getClass().getSimpleName());
assertEquals(expectedResult, ((StringEval) result).getStringValue());
}
private void testText(ValueEval valueArg, ValueEval formatArg, ErrorEval expectedResult) {
ValueEval[] args = { valueArg, formatArg };
ValueEval result = TextFunction.TEXT.evaluate(args, -1, -1);
assertTrue(result instanceof ErrorEval, "Expected ErrorEval got " + result.getClass().getSimpleName());
assertEquals(expectedResult, result);
}
}