|
|
@@ -44,177 +44,177 @@ import org.apache.poi.ss.usermodel.FormulaError; |
|
|
|
*/ |
|
|
|
public final class Countif extends Fixed2ArgFunction { |
|
|
|
|
|
|
|
private static final class CmpOp { |
|
|
|
public static final int NONE = 0; |
|
|
|
public static final int EQ = 1; |
|
|
|
public static final int NE = 2; |
|
|
|
public static final int LE = 3; |
|
|
|
public static final int LT = 4; |
|
|
|
public static final int GT = 5; |
|
|
|
public static final int GE = 6; |
|
|
|
|
|
|
|
public static final CmpOp OP_NONE = op("", NONE); |
|
|
|
public static final CmpOp OP_EQ = op("=", EQ); |
|
|
|
public static final CmpOp OP_NE = op("<>", NE); |
|
|
|
public static final CmpOp OP_LE = op("<=", LE); |
|
|
|
public static final CmpOp OP_LT = op("<", LT); |
|
|
|
public static final CmpOp OP_GT = op(">", GT); |
|
|
|
public static final CmpOp OP_GE = op(">=", GE); |
|
|
|
private final String _representation; |
|
|
|
private final int _code; |
|
|
|
|
|
|
|
private static CmpOp op(String rep, int code) { |
|
|
|
return new CmpOp(rep, code); |
|
|
|
} |
|
|
|
private CmpOp(String representation, int code) { |
|
|
|
_representation = representation; |
|
|
|
_code = code; |
|
|
|
} |
|
|
|
/** |
|
|
|
* @return number of characters used to represent this operator |
|
|
|
*/ |
|
|
|
public int getLength() { |
|
|
|
return _representation.length(); |
|
|
|
} |
|
|
|
public int getCode() { |
|
|
|
return _code; |
|
|
|
} |
|
|
|
public static CmpOp getOperator(String value) { |
|
|
|
int len = value.length(); |
|
|
|
if (len < 1) { |
|
|
|
return OP_NONE; |
|
|
|
} |
|
|
|
|
|
|
|
char firstChar = value.charAt(0); |
|
|
|
|
|
|
|
switch(firstChar) { |
|
|
|
case '=': |
|
|
|
return OP_EQ; |
|
|
|
case '>': |
|
|
|
if (len > 1) { |
|
|
|
switch(value.charAt(1)) { |
|
|
|
case '=': |
|
|
|
return OP_GE; |
|
|
|
} |
|
|
|
} |
|
|
|
return OP_GT; |
|
|
|
case '<': |
|
|
|
if (len > 1) { |
|
|
|
switch(value.charAt(1)) { |
|
|
|
case '=': |
|
|
|
return OP_LE; |
|
|
|
case '>': |
|
|
|
return OP_NE; |
|
|
|
} |
|
|
|
} |
|
|
|
return OP_LT; |
|
|
|
} |
|
|
|
return OP_NONE; |
|
|
|
} |
|
|
|
public boolean evaluate(boolean cmpResult) { |
|
|
|
switch (_code) { |
|
|
|
case NONE: |
|
|
|
case EQ: |
|
|
|
return cmpResult; |
|
|
|
case NE: |
|
|
|
return !cmpResult; |
|
|
|
} |
|
|
|
throw new RuntimeException("Cannot call boolean evaluate on non-equality operator '" |
|
|
|
+ _representation + "'"); |
|
|
|
} |
|
|
|
public boolean evaluate(int cmpResult) { |
|
|
|
switch (_code) { |
|
|
|
case NONE: |
|
|
|
case EQ: |
|
|
|
return cmpResult == 0; |
|
|
|
case NE: return cmpResult != 0; |
|
|
|
case LT: return cmpResult < 0; |
|
|
|
case LE: return cmpResult <= 0; |
|
|
|
case GT: return cmpResult > 0; |
|
|
|
case GE: return cmpResult >= 0; |
|
|
|
} |
|
|
|
throw new RuntimeException("Cannot call boolean evaluate on non-equality operator '" |
|
|
|
+ _representation + "'"); |
|
|
|
} |
|
|
|
public String toString() { |
|
|
|
StringBuffer sb = new StringBuffer(64); |
|
|
|
sb.append(getClass().getName()); |
|
|
|
sb.append(" [").append(_representation).append("]"); |
|
|
|
return sb.toString(); |
|
|
|
} |
|
|
|
public String getRepresentation() { |
|
|
|
return _representation; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static abstract class MatcherBase implements I_MatchPredicate { |
|
|
|
private final CmpOp _operator; |
|
|
|
|
|
|
|
MatcherBase(CmpOp operator) { |
|
|
|
_operator = operator; |
|
|
|
} |
|
|
|
protected final int getCode() { |
|
|
|
return _operator.getCode(); |
|
|
|
} |
|
|
|
protected final boolean evaluate(int cmpResult) { |
|
|
|
return _operator.evaluate(cmpResult); |
|
|
|
} |
|
|
|
protected final boolean evaluate(boolean cmpResult) { |
|
|
|
return _operator.evaluate(cmpResult); |
|
|
|
} |
|
|
|
@Override |
|
|
|
public final String toString() { |
|
|
|
StringBuffer sb = new StringBuffer(64); |
|
|
|
sb.append(getClass().getName()).append(" ["); |
|
|
|
sb.append(_operator.getRepresentation()); |
|
|
|
sb.append(getValueText()); |
|
|
|
sb.append("]"); |
|
|
|
return sb.toString(); |
|
|
|
} |
|
|
|
protected abstract String getValueText(); |
|
|
|
} |
|
|
|
|
|
|
|
private static final class NumberMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final double _value; |
|
|
|
|
|
|
|
public NumberMatcher(double value, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = value; |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
return String.valueOf(_value); |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
double testValue; |
|
|
|
if(x instanceof StringEval) { |
|
|
|
// if the target(x) is a string, but parses as a number |
|
|
|
// it may still count as a match, only for the equality operator |
|
|
|
switch (getCode()) { |
|
|
|
case CmpOp.EQ: |
|
|
|
case CmpOp.NONE: |
|
|
|
break; |
|
|
|
case CmpOp.NE: |
|
|
|
// Always matches (inconsistent with above two cases). |
|
|
|
// for example '<>123' matches '123', '4', 'abc', etc |
|
|
|
return true; |
|
|
|
default: |
|
|
|
// never matches (also inconsistent with above three cases). |
|
|
|
// for example '>5' does not match '6', |
|
|
|
return false; |
|
|
|
} |
|
|
|
StringEval se = (StringEval)x; |
|
|
|
Double val = OperandResolver.parseDouble(se.getStringValue()); |
|
|
|
if(val == null) { |
|
|
|
// x is text that is not a number |
|
|
|
return false; |
|
|
|
} |
|
|
|
return _value == val.doubleValue(); |
|
|
|
} else if((x instanceof NumberEval)) { |
|
|
|
NumberEval ne = (NumberEval) x; |
|
|
|
testValue = ne.getNumberValue(); |
|
|
|
private static final class CmpOp { |
|
|
|
public static final int NONE = 0; |
|
|
|
public static final int EQ = 1; |
|
|
|
public static final int NE = 2; |
|
|
|
public static final int LE = 3; |
|
|
|
public static final int LT = 4; |
|
|
|
public static final int GT = 5; |
|
|
|
public static final int GE = 6; |
|
|
|
|
|
|
|
public static final CmpOp OP_NONE = op("", NONE); |
|
|
|
public static final CmpOp OP_EQ = op("=", EQ); |
|
|
|
public static final CmpOp OP_NE = op("<>", NE); |
|
|
|
public static final CmpOp OP_LE = op("<=", LE); |
|
|
|
public static final CmpOp OP_LT = op("<", LT); |
|
|
|
public static final CmpOp OP_GT = op(">", GT); |
|
|
|
public static final CmpOp OP_GE = op(">=", GE); |
|
|
|
private final String _representation; |
|
|
|
private final int _code; |
|
|
|
|
|
|
|
private static CmpOp op(String rep, int code) { |
|
|
|
return new CmpOp(rep, code); |
|
|
|
} |
|
|
|
private CmpOp(String representation, int code) { |
|
|
|
_representation = representation; |
|
|
|
_code = code; |
|
|
|
} |
|
|
|
/** |
|
|
|
* @return number of characters used to represent this operator |
|
|
|
*/ |
|
|
|
public int getLength() { |
|
|
|
return _representation.length(); |
|
|
|
} |
|
|
|
public int getCode() { |
|
|
|
return _code; |
|
|
|
} |
|
|
|
public static CmpOp getOperator(String value) { |
|
|
|
int len = value.length(); |
|
|
|
if (len < 1) { |
|
|
|
return OP_NONE; |
|
|
|
} |
|
|
|
|
|
|
|
char firstChar = value.charAt(0); |
|
|
|
|
|
|
|
switch(firstChar) { |
|
|
|
case '=': |
|
|
|
return OP_EQ; |
|
|
|
case '>': |
|
|
|
if (len > 1) { |
|
|
|
switch(value.charAt(1)) { |
|
|
|
case '=': |
|
|
|
return OP_GE; |
|
|
|
} |
|
|
|
} |
|
|
|
return OP_GT; |
|
|
|
case '<': |
|
|
|
if (len > 1) { |
|
|
|
switch(value.charAt(1)) { |
|
|
|
case '=': |
|
|
|
return OP_LE; |
|
|
|
case '>': |
|
|
|
return OP_NE; |
|
|
|
} |
|
|
|
} |
|
|
|
return OP_LT; |
|
|
|
} |
|
|
|
return OP_NONE; |
|
|
|
} |
|
|
|
public boolean evaluate(boolean cmpResult) { |
|
|
|
switch (_code) { |
|
|
|
case NONE: |
|
|
|
case EQ: |
|
|
|
return cmpResult; |
|
|
|
case NE: |
|
|
|
return !cmpResult; |
|
|
|
} |
|
|
|
throw new RuntimeException("Cannot call boolean evaluate on non-equality operator '" |
|
|
|
+ _representation + "'"); |
|
|
|
} |
|
|
|
public boolean evaluate(int cmpResult) { |
|
|
|
switch (_code) { |
|
|
|
case NONE: |
|
|
|
case EQ: |
|
|
|
return cmpResult == 0; |
|
|
|
case NE: return cmpResult != 0; |
|
|
|
case LT: return cmpResult < 0; |
|
|
|
case LE: return cmpResult <= 0; |
|
|
|
case GT: return cmpResult > 0; |
|
|
|
case GE: return cmpResult >= 0; |
|
|
|
} |
|
|
|
throw new RuntimeException("Cannot call boolean evaluate on non-equality operator '" |
|
|
|
+ _representation + "'"); |
|
|
|
} |
|
|
|
public String toString() { |
|
|
|
StringBuffer sb = new StringBuffer(64); |
|
|
|
sb.append(getClass().getName()); |
|
|
|
sb.append(" [").append(_representation).append("]"); |
|
|
|
return sb.toString(); |
|
|
|
} |
|
|
|
public String getRepresentation() { |
|
|
|
return _representation; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static abstract class MatcherBase implements I_MatchPredicate { |
|
|
|
private final CmpOp _operator; |
|
|
|
|
|
|
|
MatcherBase(CmpOp operator) { |
|
|
|
_operator = operator; |
|
|
|
} |
|
|
|
protected final int getCode() { |
|
|
|
return _operator.getCode(); |
|
|
|
} |
|
|
|
protected final boolean evaluate(int cmpResult) { |
|
|
|
return _operator.evaluate(cmpResult); |
|
|
|
} |
|
|
|
protected final boolean evaluate(boolean cmpResult) { |
|
|
|
return _operator.evaluate(cmpResult); |
|
|
|
} |
|
|
|
@Override |
|
|
|
public final String toString() { |
|
|
|
StringBuffer sb = new StringBuffer(64); |
|
|
|
sb.append(getClass().getName()).append(" ["); |
|
|
|
sb.append(_operator.getRepresentation()); |
|
|
|
sb.append(getValueText()); |
|
|
|
sb.append("]"); |
|
|
|
return sb.toString(); |
|
|
|
} |
|
|
|
protected abstract String getValueText(); |
|
|
|
} |
|
|
|
|
|
|
|
private static final class NumberMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final double _value; |
|
|
|
|
|
|
|
public NumberMatcher(double value, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = value; |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
return String.valueOf(_value); |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
double testValue; |
|
|
|
if(x instanceof StringEval) { |
|
|
|
// if the target(x) is a string, but parses as a number |
|
|
|
// it may still count as a match, only for the equality operator |
|
|
|
switch (getCode()) { |
|
|
|
case CmpOp.EQ: |
|
|
|
case CmpOp.NONE: |
|
|
|
break; |
|
|
|
case CmpOp.NE: |
|
|
|
// Always matches (inconsistent with above two cases). |
|
|
|
// for example '<>123' matches '123', '4', 'abc', etc |
|
|
|
return true; |
|
|
|
default: |
|
|
|
// never matches (also inconsistent with above three cases). |
|
|
|
// for example '>5' does not match '6', |
|
|
|
return false; |
|
|
|
} |
|
|
|
StringEval se = (StringEval)x; |
|
|
|
Double val = OperandResolver.parseDouble(se.getStringValue()); |
|
|
|
if(val == null) { |
|
|
|
// x is text that is not a number |
|
|
|
return false; |
|
|
|
} |
|
|
|
return _value == val.doubleValue(); |
|
|
|
} else if((x instanceof NumberEval)) { |
|
|
|
NumberEval ne = (NumberEval) x; |
|
|
|
testValue = ne.getNumberValue(); |
|
|
|
} else if((x instanceof BlankEval)) { |
|
|
|
switch (getCode()) { |
|
|
|
case CmpOp.NE: |
|
|
@@ -223,48 +223,48 @@ public final class Countif extends Fixed2ArgFunction { |
|
|
|
default: |
|
|
|
return false; |
|
|
|
} |
|
|
|
} else { |
|
|
|
return false; |
|
|
|
} |
|
|
|
return evaluate(Double.compare(testValue, _value)); |
|
|
|
} |
|
|
|
} |
|
|
|
private static final class BooleanMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final int _value; |
|
|
|
|
|
|
|
public BooleanMatcher(boolean value, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = boolToInt(value); |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
return _value == 1 ? "TRUE" : "FALSE"; |
|
|
|
} |
|
|
|
|
|
|
|
private static int boolToInt(boolean value) { |
|
|
|
return value ? 1 : 0; |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
int testValue; |
|
|
|
if(x instanceof StringEval) { |
|
|
|
if (true) { // change to false to observe more intuitive behaviour |
|
|
|
// Note - Unlike with numbers, it seems that COUNTIF never matches |
|
|
|
// boolean values when the target(x) is a string |
|
|
|
return false; |
|
|
|
} |
|
|
|
@SuppressWarnings("unused") |
|
|
|
StringEval se = (StringEval)x; |
|
|
|
Boolean val = parseBoolean(se.getStringValue()); |
|
|
|
if(val == null) { |
|
|
|
// x is text that is not a boolean |
|
|
|
return false; |
|
|
|
} |
|
|
|
testValue = boolToInt(val.booleanValue()); |
|
|
|
} else if((x instanceof BoolEval)) { |
|
|
|
BoolEval be = (BoolEval) x; |
|
|
|
testValue = boolToInt(be.getBooleanValue()); |
|
|
|
} else { |
|
|
|
return false; |
|
|
|
} |
|
|
|
return evaluate(Double.compare(testValue, _value)); |
|
|
|
} |
|
|
|
} |
|
|
|
private static final class BooleanMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final int _value; |
|
|
|
|
|
|
|
public BooleanMatcher(boolean value, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = boolToInt(value); |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
return _value == 1 ? "TRUE" : "FALSE"; |
|
|
|
} |
|
|
|
|
|
|
|
private static int boolToInt(boolean value) { |
|
|
|
return value ? 1 : 0; |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
int testValue; |
|
|
|
if(x instanceof StringEval) { |
|
|
|
if (true) { // change to false to observe more intuitive behaviour |
|
|
|
// Note - Unlike with numbers, it seems that COUNTIF never matches |
|
|
|
// boolean values when the target(x) is a string |
|
|
|
return false; |
|
|
|
} |
|
|
|
@SuppressWarnings("unused") |
|
|
|
StringEval se = (StringEval)x; |
|
|
|
Boolean val = parseBoolean(se.getStringValue()); |
|
|
|
if(val == null) { |
|
|
|
// x is text that is not a boolean |
|
|
|
return false; |
|
|
|
} |
|
|
|
testValue = boolToInt(val.booleanValue()); |
|
|
|
} else if((x instanceof BoolEval)) { |
|
|
|
BoolEval be = (BoolEval) x; |
|
|
|
testValue = boolToInt(be.getBooleanValue()); |
|
|
|
} else if((x instanceof BlankEval)) { |
|
|
|
switch (getCode()) { |
|
|
|
case CmpOp.NE: |
|
|
@@ -282,280 +282,280 @@ public final class Countif extends Fixed2ArgFunction { |
|
|
|
return false; |
|
|
|
} |
|
|
|
} else { |
|
|
|
return false; |
|
|
|
} |
|
|
|
return evaluate(testValue - _value); |
|
|
|
} |
|
|
|
} |
|
|
|
public static final class ErrorMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final int _value; |
|
|
|
|
|
|
|
public ErrorMatcher(int errorCode, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = errorCode; |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
return FormulaError.forInt(_value).getString(); |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
if(x instanceof ErrorEval) { |
|
|
|
int testValue = ((ErrorEval)x).getErrorCode(); |
|
|
|
return evaluate(testValue - _value); |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
public int getValue() { |
|
|
|
return _value; |
|
|
|
} |
|
|
|
} |
|
|
|
public static final class StringMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final String _value; |
|
|
|
private final Pattern _pattern; |
|
|
|
|
|
|
|
public StringMatcher(String value, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = value; |
|
|
|
switch(operator.getCode()) { |
|
|
|
case CmpOp.NONE: |
|
|
|
case CmpOp.EQ: |
|
|
|
case CmpOp.NE: |
|
|
|
_pattern = getWildCardPattern(value); |
|
|
|
break; |
|
|
|
default: |
|
|
|
// pattern matching is never used for < > <= => |
|
|
|
_pattern = null; |
|
|
|
} |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
if (_pattern == null) { |
|
|
|
return _value; |
|
|
|
} |
|
|
|
return _pattern.pattern(); |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
if (x instanceof BlankEval) { |
|
|
|
switch(getCode()) { |
|
|
|
case CmpOp.NONE: |
|
|
|
case CmpOp.EQ: |
|
|
|
return _value.length() == 0; |
|
|
|
return false; |
|
|
|
} |
|
|
|
return evaluate(testValue - _value); |
|
|
|
} |
|
|
|
} |
|
|
|
public static final class ErrorMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final int _value; |
|
|
|
|
|
|
|
public ErrorMatcher(int errorCode, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = errorCode; |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
return FormulaError.forInt(_value).getString(); |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
if(x instanceof ErrorEval) { |
|
|
|
int testValue = ((ErrorEval)x).getErrorCode(); |
|
|
|
return evaluate(testValue - _value); |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
public int getValue() { |
|
|
|
return _value; |
|
|
|
} |
|
|
|
} |
|
|
|
public static final class StringMatcher extends MatcherBase { |
|
|
|
|
|
|
|
private final String _value; |
|
|
|
private final Pattern _pattern; |
|
|
|
|
|
|
|
public StringMatcher(String value, CmpOp operator) { |
|
|
|
super(operator); |
|
|
|
_value = value; |
|
|
|
switch(operator.getCode()) { |
|
|
|
case CmpOp.NONE: |
|
|
|
case CmpOp.EQ: |
|
|
|
case CmpOp.NE: |
|
|
|
_pattern = getWildCardPattern(value); |
|
|
|
break; |
|
|
|
default: |
|
|
|
// pattern matching is never used for < > <= => |
|
|
|
_pattern = null; |
|
|
|
} |
|
|
|
} |
|
|
|
@Override |
|
|
|
protected String getValueText() { |
|
|
|
if (_pattern == null) { |
|
|
|
return _value; |
|
|
|
} |
|
|
|
return _pattern.pattern(); |
|
|
|
} |
|
|
|
|
|
|
|
public boolean matches(ValueEval x) { |
|
|
|
if (x instanceof BlankEval) { |
|
|
|
switch(getCode()) { |
|
|
|
case CmpOp.NONE: |
|
|
|
case CmpOp.EQ: |
|
|
|
return _value.length() == 0; |
|
|
|
case CmpOp.NE: |
|
|
|
// pred '<>' matches empty string but not blank cell |
|
|
|
// pred '<>ABC' matches blank and 'not ABC' |
|
|
|
return _value.length() != 0; |
|
|
|
} |
|
|
|
// no other criteria matches a blank cell |
|
|
|
return false; |
|
|
|
} |
|
|
|
if(!(x instanceof StringEval)) { |
|
|
|
// must always be string |
|
|
|
// even if match str is wild, but contains only digits |
|
|
|
// e.g. '4*7', NumberEval(4567) does not match |
|
|
|
return false; |
|
|
|
} |
|
|
|
String testedValue = ((StringEval) x).getStringValue(); |
|
|
|
if (testedValue.length() < 1 && _value.length() < 1) { |
|
|
|
// odd case: criteria '=' behaves differently to criteria '' |
|
|
|
|
|
|
|
switch(getCode()) { |
|
|
|
case CmpOp.NONE: return true; |
|
|
|
case CmpOp.EQ: return false; |
|
|
|
case CmpOp.NE: return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (_pattern != null) { |
|
|
|
return evaluate(_pattern.matcher(testedValue).matches()); |
|
|
|
} |
|
|
|
} |
|
|
|
// no other criteria matches a blank cell |
|
|
|
return false; |
|
|
|
} |
|
|
|
if(!(x instanceof StringEval)) { |
|
|
|
// must always be string |
|
|
|
// even if match str is wild, but contains only digits |
|
|
|
// e.g. '4*7', NumberEval(4567) does not match |
|
|
|
return false; |
|
|
|
} |
|
|
|
String testedValue = ((StringEval) x).getStringValue(); |
|
|
|
if (testedValue.length() < 1 && _value.length() < 1) { |
|
|
|
// odd case: criteria '=' behaves differently to criteria '' |
|
|
|
|
|
|
|
switch(getCode()) { |
|
|
|
case CmpOp.NONE: return true; |
|
|
|
case CmpOp.EQ: return false; |
|
|
|
case CmpOp.NE: return true; |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (_pattern != null) { |
|
|
|
return evaluate(_pattern.matcher(testedValue).matches()); |
|
|
|
} |
|
|
|
// String criteria in COUNTIF are case insensitive: |
|
|
|
// for example, the string "apples" and the string "APPLES" will match the same cells. |
|
|
|
return evaluate(testedValue.compareToIgnoreCase(_value)); |
|
|
|
} |
|
|
|
/** |
|
|
|
* Translates Excel countif wildcard strings into java regex strings |
|
|
|
* @return <code>null</code> if the specified value contains no special wildcard characters. |
|
|
|
*/ |
|
|
|
public static Pattern getWildCardPattern(String value) { |
|
|
|
int len = value.length(); |
|
|
|
StringBuffer sb = new StringBuffer(len); |
|
|
|
boolean hasWildCard = false; |
|
|
|
for(int i=0; i<len; i++) { |
|
|
|
char ch = value.charAt(i); |
|
|
|
switch(ch) { |
|
|
|
case '?': //Any single character |
|
|
|
hasWildCard = true; |
|
|
|
// match exactly one character |
|
|
|
sb.append('.'); |
|
|
|
continue; |
|
|
|
case '*': //Zero or more characters |
|
|
|
hasWildCard = true; |
|
|
|
// match one or more occurrences of any character |
|
|
|
sb.append(".*"); |
|
|
|
continue; |
|
|
|
case '~': |
|
|
|
if (i+1<len) { |
|
|
|
ch = value.charAt(i+1); |
|
|
|
switch (ch) { |
|
|
|
case '?': |
|
|
|
case '*': |
|
|
|
hasWildCard = true; |
|
|
|
sb.append('[').append(ch).append(']'); |
|
|
|
i++; // Note - incrementing loop variable here |
|
|
|
continue; |
|
|
|
} |
|
|
|
} |
|
|
|
// else not '~?' or '~*' |
|
|
|
sb.append('~'); // just plain '~' |
|
|
|
continue; |
|
|
|
case '.': |
|
|
|
case '$': |
|
|
|
case '^': |
|
|
|
case '[': |
|
|
|
case ']': |
|
|
|
case '(': |
|
|
|
case ')': |
|
|
|
// escape literal characters that would have special meaning in regex |
|
|
|
sb.append("\\").append(ch); |
|
|
|
continue; |
|
|
|
} |
|
|
|
sb.append(ch); |
|
|
|
} |
|
|
|
if (hasWildCard) { |
|
|
|
return Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE); |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { |
|
|
|
|
|
|
|
I_MatchPredicate mp = createCriteriaPredicate(arg1, srcRowIndex, srcColumnIndex); |
|
|
|
if(mp == null) { |
|
|
|
// If the criteria arg is a reference to a blank cell, countif always returns zero. |
|
|
|
return NumberEval.ZERO; |
|
|
|
} |
|
|
|
double result = countMatchingCellsInArea(arg0, mp); |
|
|
|
return new NumberEval(result); |
|
|
|
} |
|
|
|
/** |
|
|
|
* @return the number of evaluated cells in the range that match the specified criteria |
|
|
|
*/ |
|
|
|
private double countMatchingCellsInArea(ValueEval rangeArg, I_MatchPredicate criteriaPredicate) { |
|
|
|
|
|
|
|
if (rangeArg instanceof RefEval) { |
|
|
|
return CountUtils.countMatchingCellsInRef((RefEval) rangeArg, criteriaPredicate); |
|
|
|
} else if (rangeArg instanceof ThreeDEval) { |
|
|
|
return CountUtils.countMatchingCellsInArea((ThreeDEval) rangeArg, criteriaPredicate); |
|
|
|
} else { |
|
|
|
throw new IllegalArgumentException("Bad range arg type (" + rangeArg.getClass().getName() + ")"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Creates a criteria predicate object for the supplied criteria arg |
|
|
|
* @return <code>null</code> if the arg evaluates to blank. |
|
|
|
*/ |
|
|
|
/* package */ static I_MatchPredicate createCriteriaPredicate(ValueEval arg, int srcRowIndex, int srcColumnIndex) { |
|
|
|
|
|
|
|
ValueEval evaluatedCriteriaArg = evaluateCriteriaArg(arg, srcRowIndex, srcColumnIndex); |
|
|
|
|
|
|
|
if(evaluatedCriteriaArg instanceof NumberEval) { |
|
|
|
return new NumberMatcher(((NumberEval)evaluatedCriteriaArg).getNumberValue(), CmpOp.OP_NONE); |
|
|
|
} |
|
|
|
if(evaluatedCriteriaArg instanceof BoolEval) { |
|
|
|
return new BooleanMatcher(((BoolEval)evaluatedCriteriaArg).getBooleanValue(), CmpOp.OP_NONE); |
|
|
|
} |
|
|
|
|
|
|
|
if(evaluatedCriteriaArg instanceof StringEval) { |
|
|
|
return createGeneralMatchPredicate((StringEval)evaluatedCriteriaArg); |
|
|
|
} |
|
|
|
if(evaluatedCriteriaArg instanceof ErrorEval) { |
|
|
|
return new ErrorMatcher(((ErrorEval)evaluatedCriteriaArg).getErrorCode(), CmpOp.OP_NONE); |
|
|
|
} |
|
|
|
if(evaluatedCriteriaArg == BlankEval.instance) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
throw new RuntimeException("Unexpected type for criteria (" |
|
|
|
+ evaluatedCriteriaArg.getClass().getName() + ")"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* |
|
|
|
* @return the de-referenced criteria arg (possibly {@link ErrorEval}) |
|
|
|
*/ |
|
|
|
private static ValueEval evaluateCriteriaArg(ValueEval arg, int srcRowIndex, int srcColumnIndex) { |
|
|
|
try { |
|
|
|
return OperandResolver.getSingleValue(arg, srcRowIndex, srcColumnIndex); |
|
|
|
} catch (EvaluationException e) { |
|
|
|
return e.getErrorEval(); |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* When the second argument is a string, many things are possible |
|
|
|
*/ |
|
|
|
private static I_MatchPredicate createGeneralMatchPredicate(StringEval stringEval) { |
|
|
|
String value = stringEval.getStringValue(); |
|
|
|
CmpOp operator = CmpOp.getOperator(value); |
|
|
|
value = value.substring(operator.getLength()); |
|
|
|
|
|
|
|
Boolean booleanVal = parseBoolean(value); |
|
|
|
if(booleanVal != null) { |
|
|
|
return new BooleanMatcher(booleanVal.booleanValue(), operator); |
|
|
|
} |
|
|
|
|
|
|
|
Double doubleVal = OperandResolver.parseDouble(value); |
|
|
|
if(doubleVal != null) { |
|
|
|
return new NumberMatcher(doubleVal.doubleValue(), operator); |
|
|
|
} |
|
|
|
ErrorEval ee = parseError(value); |
|
|
|
if (ee != null) { |
|
|
|
return new ErrorMatcher(ee.getErrorCode(), operator); |
|
|
|
} |
|
|
|
|
|
|
|
//else - just a plain string with no interpretation. |
|
|
|
return new StringMatcher(value, operator); |
|
|
|
} |
|
|
|
private static ErrorEval parseError(String value) { |
|
|
|
if (value.length() < 4 || value.charAt(0) != '#') { |
|
|
|
return null; |
|
|
|
} |
|
|
|
if (value.equals("#NULL!")) return ErrorEval.NULL_INTERSECTION; |
|
|
|
if (value.equals("#DIV/0!")) return ErrorEval.DIV_ZERO; |
|
|
|
if (value.equals("#VALUE!")) return ErrorEval.VALUE_INVALID; |
|
|
|
if (value.equals("#REF!")) return ErrorEval.REF_INVALID; |
|
|
|
if (value.equals("#NAME?")) return ErrorEval.NAME_INVALID; |
|
|
|
if (value.equals("#NUM!")) return ErrorEval.NUM_ERROR; |
|
|
|
if (value.equals("#N/A")) return ErrorEval.NA; |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
/** |
|
|
|
* Boolean literals ('TRUE', 'FALSE') treated similarly but NOT same as numbers. |
|
|
|
*/ |
|
|
|
/* package */ static Boolean parseBoolean(String strRep) { |
|
|
|
if (strRep.length() < 1) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
switch(strRep.charAt(0)) { |
|
|
|
case 't': |
|
|
|
case 'T': |
|
|
|
if("TRUE".equalsIgnoreCase(strRep)) { |
|
|
|
return Boolean.TRUE; |
|
|
|
} |
|
|
|
break; |
|
|
|
case 'f': |
|
|
|
case 'F': |
|
|
|
if("FALSE".equalsIgnoreCase(strRep)) { |
|
|
|
return Boolean.FALSE; |
|
|
|
} |
|
|
|
break; |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
return evaluate(testedValue.compareToIgnoreCase(_value)); |
|
|
|
} |
|
|
|
/** |
|
|
|
* Translates Excel countif wildcard strings into java regex strings |
|
|
|
* @return <code>null</code> if the specified value contains no special wildcard characters. |
|
|
|
*/ |
|
|
|
public static Pattern getWildCardPattern(String value) { |
|
|
|
int len = value.length(); |
|
|
|
StringBuffer sb = new StringBuffer(len); |
|
|
|
boolean hasWildCard = false; |
|
|
|
for(int i=0; i<len; i++) { |
|
|
|
char ch = value.charAt(i); |
|
|
|
switch(ch) { |
|
|
|
case '?': //Any single character |
|
|
|
hasWildCard = true; |
|
|
|
// match exactly one character |
|
|
|
sb.append('.'); |
|
|
|
continue; |
|
|
|
case '*': //Zero or more characters |
|
|
|
hasWildCard = true; |
|
|
|
// match one or more occurrences of any character |
|
|
|
sb.append(".*"); |
|
|
|
continue; |
|
|
|
case '~': |
|
|
|
if (i+1<len) { |
|
|
|
ch = value.charAt(i+1); |
|
|
|
switch (ch) { |
|
|
|
case '?': |
|
|
|
case '*': |
|
|
|
hasWildCard = true; |
|
|
|
sb.append('[').append(ch).append(']'); |
|
|
|
i++; // Note - incrementing loop variable here |
|
|
|
continue; |
|
|
|
} |
|
|
|
} |
|
|
|
// else not '~?' or '~*' |
|
|
|
sb.append('~'); // just plain '~' |
|
|
|
continue; |
|
|
|
case '.': |
|
|
|
case '$': |
|
|
|
case '^': |
|
|
|
case '[': |
|
|
|
case ']': |
|
|
|
case '(': |
|
|
|
case ')': |
|
|
|
// escape literal characters that would have special meaning in regex |
|
|
|
sb.append("\\").append(ch); |
|
|
|
continue; |
|
|
|
} |
|
|
|
sb.append(ch); |
|
|
|
} |
|
|
|
if (hasWildCard) { |
|
|
|
return Pattern.compile(sb.toString(), Pattern.CASE_INSENSITIVE); |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
public ValueEval evaluate(int srcRowIndex, int srcColumnIndex, ValueEval arg0, ValueEval arg1) { |
|
|
|
|
|
|
|
I_MatchPredicate mp = createCriteriaPredicate(arg1, srcRowIndex, srcColumnIndex); |
|
|
|
if(mp == null) { |
|
|
|
// If the criteria arg is a reference to a blank cell, countif always returns zero. |
|
|
|
return NumberEval.ZERO; |
|
|
|
} |
|
|
|
double result = countMatchingCellsInArea(arg0, mp); |
|
|
|
return new NumberEval(result); |
|
|
|
} |
|
|
|
/** |
|
|
|
* @return the number of evaluated cells in the range that match the specified criteria |
|
|
|
*/ |
|
|
|
private double countMatchingCellsInArea(ValueEval rangeArg, I_MatchPredicate criteriaPredicate) { |
|
|
|
|
|
|
|
if (rangeArg instanceof RefEval) { |
|
|
|
return CountUtils.countMatchingCellsInRef((RefEval) rangeArg, criteriaPredicate); |
|
|
|
} else if (rangeArg instanceof ThreeDEval) { |
|
|
|
return CountUtils.countMatchingCellsInArea((ThreeDEval) rangeArg, criteriaPredicate); |
|
|
|
} else { |
|
|
|
throw new IllegalArgumentException("Bad range arg type (" + rangeArg.getClass().getName() + ")"); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Creates a criteria predicate object for the supplied criteria arg |
|
|
|
* @return <code>null</code> if the arg evaluates to blank. |
|
|
|
*/ |
|
|
|
/* package */ static I_MatchPredicate createCriteriaPredicate(ValueEval arg, int srcRowIndex, int srcColumnIndex) { |
|
|
|
|
|
|
|
ValueEval evaluatedCriteriaArg = evaluateCriteriaArg(arg, srcRowIndex, srcColumnIndex); |
|
|
|
|
|
|
|
if(evaluatedCriteriaArg instanceof NumberEval) { |
|
|
|
return new NumberMatcher(((NumberEval)evaluatedCriteriaArg).getNumberValue(), CmpOp.OP_NONE); |
|
|
|
} |
|
|
|
if(evaluatedCriteriaArg instanceof BoolEval) { |
|
|
|
return new BooleanMatcher(((BoolEval)evaluatedCriteriaArg).getBooleanValue(), CmpOp.OP_NONE); |
|
|
|
} |
|
|
|
|
|
|
|
if(evaluatedCriteriaArg instanceof StringEval) { |
|
|
|
return createGeneralMatchPredicate((StringEval)evaluatedCriteriaArg); |
|
|
|
} |
|
|
|
if(evaluatedCriteriaArg instanceof ErrorEval) { |
|
|
|
return new ErrorMatcher(((ErrorEval)evaluatedCriteriaArg).getErrorCode(), CmpOp.OP_NONE); |
|
|
|
} |
|
|
|
if(evaluatedCriteriaArg == BlankEval.instance) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
throw new RuntimeException("Unexpected type for criteria (" |
|
|
|
+ evaluatedCriteriaArg.getClass().getName() + ")"); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* |
|
|
|
* @return the de-referenced criteria arg (possibly {@link ErrorEval}) |
|
|
|
*/ |
|
|
|
private static ValueEval evaluateCriteriaArg(ValueEval arg, int srcRowIndex, int srcColumnIndex) { |
|
|
|
try { |
|
|
|
return OperandResolver.getSingleValue(arg, srcRowIndex, srcColumnIndex); |
|
|
|
} catch (EvaluationException e) { |
|
|
|
return e.getErrorEval(); |
|
|
|
} |
|
|
|
} |
|
|
|
/** |
|
|
|
* When the second argument is a string, many things are possible |
|
|
|
*/ |
|
|
|
private static I_MatchPredicate createGeneralMatchPredicate(StringEval stringEval) { |
|
|
|
String value = stringEval.getStringValue(); |
|
|
|
CmpOp operator = CmpOp.getOperator(value); |
|
|
|
value = value.substring(operator.getLength()); |
|
|
|
|
|
|
|
Boolean booleanVal = parseBoolean(value); |
|
|
|
if(booleanVal != null) { |
|
|
|
return new BooleanMatcher(booleanVal.booleanValue(), operator); |
|
|
|
} |
|
|
|
|
|
|
|
Double doubleVal = OperandResolver.parseDouble(value); |
|
|
|
if(doubleVal != null) { |
|
|
|
return new NumberMatcher(doubleVal.doubleValue(), operator); |
|
|
|
} |
|
|
|
ErrorEval ee = parseError(value); |
|
|
|
if (ee != null) { |
|
|
|
return new ErrorMatcher(ee.getErrorCode(), operator); |
|
|
|
} |
|
|
|
|
|
|
|
//else - just a plain string with no interpretation. |
|
|
|
return new StringMatcher(value, operator); |
|
|
|
} |
|
|
|
private static ErrorEval parseError(String value) { |
|
|
|
if (value.length() < 4 || value.charAt(0) != '#') { |
|
|
|
return null; |
|
|
|
} |
|
|
|
if (value.equals("#NULL!")) return ErrorEval.NULL_INTERSECTION; |
|
|
|
if (value.equals("#DIV/0!")) return ErrorEval.DIV_ZERO; |
|
|
|
if (value.equals("#VALUE!")) return ErrorEval.VALUE_INVALID; |
|
|
|
if (value.equals("#REF!")) return ErrorEval.REF_INVALID; |
|
|
|
if (value.equals("#NAME?")) return ErrorEval.NAME_INVALID; |
|
|
|
if (value.equals("#NUM!")) return ErrorEval.NUM_ERROR; |
|
|
|
if (value.equals("#N/A")) return ErrorEval.NA; |
|
|
|
|
|
|
|
return null; |
|
|
|
} |
|
|
|
/** |
|
|
|
* Boolean literals ('TRUE', 'FALSE') treated similarly but NOT same as numbers. |
|
|
|
*/ |
|
|
|
/* package */ static Boolean parseBoolean(String strRep) { |
|
|
|
if (strRep.length() < 1) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
switch(strRep.charAt(0)) { |
|
|
|
case 't': |
|
|
|
case 'T': |
|
|
|
if("TRUE".equalsIgnoreCase(strRep)) { |
|
|
|
return Boolean.TRUE; |
|
|
|
} |
|
|
|
break; |
|
|
|
case 'f': |
|
|
|
case 'F': |
|
|
|
if("FALSE".equalsIgnoreCase(strRep)) { |
|
|
|
return Boolean.FALSE; |
|
|
|
} |
|
|
|
break; |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |