]> source.dussan.org Git - poi.git/commitdiff
Patch 45289 - finished support for special comparison operators in COUNTIF
authorJosh Micich <josh@apache.org>
Fri, 11 Jul 2008 07:59:44 +0000 (07:59 +0000)
committerJosh Micich <josh@apache.org>
Fri, 11 Jul 2008 07:59:44 +0000 (07:59 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@675853 13f79535-47bb-0310-9956-ffa450edef68

src/documentation/content/xdocs/changes.xml
src/documentation/content/xdocs/status.xml
src/java/org/apache/poi/hssf/record/formula/functions/Countif.java
src/java/org/apache/poi/hssf/record/formula/functions/Offset.java
src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls
src/testcases/org/apache/poi/hssf/data/countifExamples.xls [new file with mode: 0644]
src/testcases/org/apache/poi/hssf/record/formula/functions/TestCountFuncs.java

index e484ebbf1a6835c37d1a78d393855492d2d45504..a9cee30b256e2ffbe5fdbd6631fe98aebb716c97 100644 (file)
@@ -37,6 +37,7 @@
 
                <!-- Don't forget to update status.xml too! -->
         <release version="3.1.1-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="fix">45289 - finished support for special comparison operators in COUNTIF</action>
            <action dev="POI-DEVELOPERS" type="fix">45126 - Avoid generating multiple NamedRanges with the same name, which Excel dislikes</action>
            <action dev="POI-DEVELOPERS" type="fix">Fix cell.getRichStringCellValue() for formula cells with string results</action>
            <action dev="POI-DEVELOPERS" type="fix">45365 - Handle more excel number formatting rules in FormatTrackingHSSFListener / XLS2CSVmra</action>
index 767b33521b64562f4b79d1bab62b20f2605c9b71..6d5d51ea81dc869c745bb653766e6803d142adee 100644 (file)
@@ -34,6 +34,7 @@
        <!-- Don't forget to update changes.xml too! -->
     <changes>
         <release version="3.1.1-alpha1" date="2008-??-??">
+           <action dev="POI-DEVELOPERS" type="fix">45289 - finished support for special comparison operators in COUNTIF</action>
            <action dev="POI-DEVELOPERS" type="fix">45126 - Avoid generating multiple NamedRanges with the same name, which Excel dislikes</action>
            <action dev="POI-DEVELOPERS" type="fix">Fix cell.getRichStringCellValue() for formula cells with string results</action>
            <action dev="POI-DEVELOPERS" type="fix">45365 - Handle more excel number formatting rules in FormatTrackingHSSFListener / XLS2CSVmra</action>
index 2e445a8bf6bb45ef0d27a620db603886fc179696..902a991b378c47aedf712209e10f21876b5c9084 100644 (file)
 * limitations under the License.
 */
 
-
 package org.apache.poi.hssf.record.formula.functions;
 
+import java.util.regex.Pattern;
+
 import org.apache.poi.hssf.record.formula.eval.AreaEval;
+import org.apache.poi.hssf.record.formula.eval.BlankEval;
 import org.apache.poi.hssf.record.formula.eval.BoolEval;
 import org.apache.poi.hssf.record.formula.eval.ErrorEval;
 import org.apache.poi.hssf.record.formula.eval.Eval;
 import org.apache.poi.hssf.record.formula.eval.NumberEval;
+import org.apache.poi.hssf.record.formula.eval.OperandResolver;
 import org.apache.poi.hssf.record.formula.eval.RefEval;
 import org.apache.poi.hssf.record.formula.eval.StringEval;
 import org.apache.poi.hssf.record.formula.eval.ValueEval;
@@ -40,85 +43,288 @@ import org.apache.poi.hssf.record.formula.eval.ValueEval;
  * @author Josh Micich
  */
 public final class Countif implements Function {
-       
+
+       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();
+               }
+       }
+
        /**
         * Common interface for the matching criteria.
         */
-       private interface I_MatchPredicate {
+       /* package */ interface I_MatchPredicate {
                boolean matches(Eval x);
        }
-       
+
        private static final class NumberMatcher implements I_MatchPredicate {
 
                private final double _value;
+               private final CmpOp _operator;
 
-               public NumberMatcher(double value) {
+               public NumberMatcher(double value, CmpOp operator) {
                        _value = value;
+                       _operator = operator;
                }
 
                public boolean matches(Eval 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
                                StringEval se = (StringEval)x;
-                               Double val = parseDouble(se.getStringValue());
+                               Double val = OperandResolver.parseDouble(se.getStringValue());
                                if(val == null) {
                                        // x is text that is not a number
                                        return false;
                                }
-                               return val.doubleValue() == _value;
-                       }
-                       if(!(x instanceof NumberEval)) {
+                               testValue = val.doubleValue();
+                       } else if((x instanceof NumberEval)) {
+                               NumberEval ne = (NumberEval) x;
+                               testValue = ne.getNumberValue();
+                       } else {
                                return false;
                        }
-                       NumberEval ne = (NumberEval) x;
-                       return ne.getNumberValue() == _value;
+                       return _operator.evaluate(Double.compare(testValue, _value));
                }
        }
        private static final class BooleanMatcher implements I_MatchPredicate {
 
-               private final boolean _value;
+               private final int _value;
+               private final CmpOp _operator;
 
-               public BooleanMatcher(boolean value) {
-                       _value = value;
+               public BooleanMatcher(boolean value, CmpOp operator) {
+                       _value = boolToInt(value);
+                       _operator = operator;
+               }
+
+               private static int boolToInt(boolean value) {
+                       return value ? 1 : 0;
                }
 
                public boolean matches(Eval 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;
+                               }
                                StringEval se = (StringEval)x;
                                Boolean val = parseBoolean(se.getStringValue());
                                if(val == null) {
                                        // x is text that is not a boolean
                                        return false;
                                }
-                               if (true) { // change to false to observe more intuitive behaviour
-                                       // Note - Unlike with numbers, it seems that COUNTA never matches 
-                                       // boolean values when the target(x) is a string
-                                       return false;
-                               }
-                               return val.booleanValue() == _value;
-                       }
-                       if(!(x instanceof BoolEval)) {
+                               testValue = boolToInt(val.booleanValue());
+                       } else if((x instanceof BoolEval)) {
+                               BoolEval be = (BoolEval) x;
+                               testValue = boolToInt(be.getBooleanValue());
+                       } else {
                                return false;
                        }
-                       BoolEval be = (BoolEval) x;
-                       return be.getBooleanValue() == _value;
+                       return _operator.evaluate(testValue - _value);
                }
        }
        private static final class StringMatcher implements I_MatchPredicate {
 
                private final String _value;
+               private final CmpOp _operator;
+               private final Pattern _pattern;
 
-               public StringMatcher(String value) {
+               public StringMatcher(String value, CmpOp operator) {
                        _value = value;
+                       _operator = operator;
+                       switch(operator.getCode()) {
+                               case CmpOp.NONE:
+                               case CmpOp.EQ:
+                               case CmpOp.NE:
+                                       _pattern = getWildCardPattern(value);
+                                       break;
+                               default:
+                                       _pattern = null;
+                       }
                }
 
                public boolean matches(Eval x) {
+                       if (x instanceof BlankEval) {
+                               switch(_operator.getCode()) {
+                                       case CmpOp.NONE:
+                                       case CmpOp.EQ:
+                                               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;
                        }
-                       StringEval se = (StringEval) x;
-                       return se.getStringValue() == _value;
+                       String testedValue = ((StringEval) x).getStringValue();
+                       if (testedValue.length() < 1 && _value.length() < 1) {
+                               // odd case: criteria '=' behaves differently to criteria ''
+
+                               switch(_operator.getCode()) {
+                                       case CmpOp.NONE: return true;
+                                       case CmpOp.EQ:   return false;
+                                       case CmpOp.NE:   return true;
+                               }
+                               return false;
+                       }
+                       if (_pattern != null) {
+                               return _operator.evaluate(_pattern.matcher(testedValue).matches());
+                       }
+                       return _operator.evaluate(testedValue.compareTo(_value));
+               }
+               /**
+                * Translates Excel countif wildcard strings into java regex strings
+                * @return <code>null</code> if the specified value contains no special wildcard characters.
+                */
+               private 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 '?':
+                                               hasWildCard = true;
+                                               // match exactly one character
+                                               sb.append('.');
+                                               continue;
+                                       case '*':
+                                               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());
+                       }
+                       return null;
                }
        }
 
@@ -132,8 +338,7 @@ public final class Countif implements Function {
                                // perhaps this should be an exception
                                return ErrorEval.VALUE_INVALID;
                }
-               
-               AreaEval range = (AreaEval) args[0];
+
                Eval criteriaArg = args[1];
                if(criteriaArg instanceof RefEval) {
                        // criteria is not a literal value, but a cell reference
@@ -144,31 +349,47 @@ public final class Countif implements Function {
                        // other non literal tokens such as function calls, have been fully evaluated
                        // for example COUNTIF(B2:D4, COLUMN(E1))
                }
+               if(criteriaArg instanceof BlankEval) {
+                       // If the criteria arg is a reference to a blank cell, countif always returns zero.
+                       return NumberEval.ZERO;
+               }
                I_MatchPredicate mp = createCriteriaPredicate(criteriaArg);
-               return countMatchingCellsInArea(range, mp);
+               return countMatchingCellsInArea(args[0], mp);
        }
        /**
         * @return the number of evaluated cells in the range that match the specified criteria
         */
-       private Eval countMatchingCellsInArea(AreaEval range, I_MatchPredicate criteriaPredicate) {
-               ValueEval[] values = range.getValues();
+       private Eval countMatchingCellsInArea(Eval rangeArg, I_MatchPredicate criteriaPredicate) {
                int result = 0;
-               for (int i = 0; i < values.length; i++) {
-                       if(criteriaPredicate.matches(values[i])) {
+               if (rangeArg instanceof RefEval) {
+                       RefEval refEval = (RefEval) rangeArg;
+                       if(criteriaPredicate.matches(refEval.getInnerValueEval())) {
                                result++;
                        }
+               } else if (rangeArg instanceof AreaEval) {
+
+                       AreaEval range = (AreaEval) rangeArg;
+                       ValueEval[] values = range.getValues();
+                       for (int i = 0; i < values.length; i++) {
+                               if(criteriaPredicate.matches(values[i])) {
+                                       result++;
+                               }
+                       }
+               } else {
+                       throw new IllegalArgumentException("Bad range arg type (" + rangeArg.getClass().getName() + ")");
                }
                return new NumberEval(result);
        }
-       
-       private static I_MatchPredicate createCriteriaPredicate(Eval evaluatedCriteriaArg) {
+
+       /* package */ static I_MatchPredicate createCriteriaPredicate(Eval evaluatedCriteriaArg) {
+
                if(evaluatedCriteriaArg instanceof NumberEval) {
-                       return new NumberMatcher(((NumberEval)evaluatedCriteriaArg).getNumberValue());
+                       return new NumberMatcher(((NumberEval)evaluatedCriteriaArg).getNumberValue(), CmpOp.OP_NONE);
                }
                if(evaluatedCriteriaArg instanceof BoolEval) {
-                       return new BooleanMatcher(((BoolEval)evaluatedCriteriaArg).getBooleanValue());
+                       return new BooleanMatcher(((BoolEval)evaluatedCriteriaArg).getBooleanValue(), CmpOp.OP_NONE);
                }
-               
+
                if(evaluatedCriteriaArg instanceof StringEval) {
                        return createGeneralMatchPredicate((StringEval)evaluatedCriteriaArg);
                }
@@ -181,50 +402,29 @@ public final class Countif implements Function {
         */
        private static I_MatchPredicate createGeneralMatchPredicate(StringEval stringEval) {
                String value = stringEval.getStringValue();
-               char firstChar = value.charAt(0);
+               CmpOp operator = CmpOp.getOperator(value);
+               value = value.substring(operator.getLength());
+
                Boolean booleanVal = parseBoolean(value);
                if(booleanVal != null) {
-                       return new BooleanMatcher(booleanVal.booleanValue());
+                       return new BooleanMatcher(booleanVal.booleanValue(), operator);
                }
-               
-               Double doubleVal = parseDouble(value);
+
+               Double doubleVal = OperandResolver.parseDouble(value);
                if(doubleVal != null) {
-                       return new NumberMatcher(doubleVal.doubleValue());
-               }
-               switch(firstChar) {
-                       case '>':
-                       case '<':
-                       case '=':
-                               throw new RuntimeException("Incomplete code - criteria expressions such as '"
-                                               + value + "' not supported yet");
+                       return new NumberMatcher(doubleVal.doubleValue(), operator);
                }
-               
-               //else - just a plain string with no interpretation.
-               return new StringMatcher(value);
-       }
 
-       /**
-        * Under certain circumstances COUNTA will equate a plain number with a string representation of that number
-        */
-       /* package */ static Double parseDouble(String strRep) {
-               if(!Character.isDigit(strRep.charAt(0))) {
-                       // avoid using NumberFormatException to tell when string is not a number
-                       return null;
-               }
-               // TODO - support notation like '1E3' (==1000)
-               
-               double val;
-               try {
-                       val = Double.parseDouble(strRep);
-               } catch (NumberFormatException e) {
-                       return null;
-               }
-               return new Double(val);
+               //else - just a plain string with no interpretation.
+               return new StringMatcher(value, operator);
        }
        /**
         * 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':
index 9497a5f21a908ae2baaed4d1cb81f57c19863340..f61434d7404cca8cc565854d6b5bfe86721f863f 100644 (file)
@@ -25,7 +25,9 @@ import org.apache.poi.hssf.record.formula.eval.AreaEval;
 import org.apache.poi.hssf.record.formula.eval.BoolEval;
 import org.apache.poi.hssf.record.formula.eval.ErrorEval;
 import org.apache.poi.hssf.record.formula.eval.Eval;
+import org.apache.poi.hssf.record.formula.eval.EvaluationException;
 import org.apache.poi.hssf.record.formula.eval.NumericValueEval;
+import org.apache.poi.hssf.record.formula.eval.OperandResolver;
 import org.apache.poi.hssf.record.formula.eval.Ref3DEval;
 import org.apache.poi.hssf.record.formula.eval.RefEval;
 import org.apache.poi.hssf.record.formula.eval.StringEval;
@@ -55,21 +57,6 @@ public final class Offset implements FreeRefFunction {
        private static final int LAST_VALID_COLUMN_INDEX = 0xFF;
        
 
-       /**
-        * Exceptions are used within this class to help simplify flow control when error conditions
-        * are encountered 
-        */
-       private static final class EvalEx extends Exception {
-               private final ErrorEval _error;
-
-               public EvalEx(ErrorEval error) {
-                       _error = error;
-               }
-               public ErrorEval getError() {
-                       return _error;
-               }
-       }
-       
        /** 
         * A one dimensional base + offset.  Represents either a row range or a column range.
         * Two instances of this class together specify an area range.
@@ -133,8 +120,7 @@ public final class Offset implements FreeRefFunction {
                        return sb.toString();
                }
        }
-       
-       
+
        /**
         * Encapsulates either an area or cell reference which may be 2d or 3d.
         */
@@ -175,19 +161,15 @@ public final class Offset implements FreeRefFunction {
                public int getWidth() {
                        return _width;
                }
-
                public int getHeight() {
                        return _height;
                }
-
                public int getFirstRowIndex() {
                        return _firstRowIndex;
                }
-
                public int getFirstColumnIndex() {
                        return _firstColumnIndex;
                }
-
                public boolean isIs3d() {
                        return _externalSheetIndex > 0;
                }
@@ -198,7 +180,6 @@ public final class Offset implements FreeRefFunction {
                        }
                        return (short) _externalSheetIndex;
                }
-
        }
        
        public ValueEval evaluate(Eval[] args, int srcCellRow, short srcCellCol, HSSFWorkbook workbook, HSSFSheet sheet) {
@@ -207,7 +188,6 @@ public final class Offset implements FreeRefFunction {
                        return ErrorEval.VALUE_INVALID;
                }
                
-               
                try {
                        BaseRef baseRef = evaluateBaseRef(args[0]);
                        int rowOffset = evaluateIntArg(args[1], srcCellRow, srcCellCol);
@@ -227,24 +207,23 @@ public final class Offset implements FreeRefFunction {
                        LinearOffsetRange rowOffsetRange = new LinearOffsetRange(rowOffset, height);
                        LinearOffsetRange colOffsetRange = new LinearOffsetRange(columnOffset, width);
                        return createOffset(baseRef, rowOffsetRange, colOffsetRange, workbook, sheet);
-               } catch (EvalEx e) {
-                       return e.getError();
+               } catch (EvaluationException e) {
+                       return e.getErrorEval();
                }
        }
 
-
        private static AreaEval createOffset(BaseRef baseRef, 
                        LinearOffsetRange rowOffsetRange, LinearOffsetRange colOffsetRange, 
-                       HSSFWorkbook workbook, HSSFSheet sheet) throws EvalEx {
+                       HSSFWorkbook workbook, HSSFSheet sheet) throws EvaluationException {
 
                LinearOffsetRange rows = rowOffsetRange.normaliseAndTranslate(baseRef.getFirstRowIndex());
                LinearOffsetRange cols = colOffsetRange.normaliseAndTranslate(baseRef.getFirstColumnIndex());
                
                if(rows.isOutOfBounds(0, LAST_VALID_ROW_INDEX)) {
-                       throw new EvalEx(ErrorEval.REF_INVALID);
+                       throw new EvaluationException(ErrorEval.REF_INVALID);
                }
                if(cols.isOutOfBounds(0, LAST_VALID_COLUMN_INDEX)) {
-                       throw new EvalEx(ErrorEval.REF_INVALID);
+                       throw new EvaluationException(ErrorEval.REF_INVALID);
                }
                if(baseRef.isIs3d()) {
                        Area3DPtg a3dp = new Area3DPtg(rows.getFirstIndex(), rows.getLastIndex(), 
@@ -260,8 +239,7 @@ public final class Offset implements FreeRefFunction {
                return HSSFFormulaEvaluator.evaluateAreaPtg(sheet, workbook, ap);
        }
 
-
-       private static BaseRef evaluateBaseRef(Eval eval) throws EvalEx {
+       private static BaseRef evaluateBaseRef(Eval eval) throws EvaluationException {
                
                if(eval instanceof RefEval) {
                        return new BaseRef((RefEval)eval);
@@ -270,16 +248,15 @@ public final class Offset implements FreeRefFunction {
                        return new BaseRef((AreaEval)eval);
                }
                if (eval instanceof ErrorEval) {
-                       throw new EvalEx((ErrorEval) eval);
+                       throw new EvaluationException((ErrorEval) eval);
                }
-               throw new EvalEx(ErrorEval.VALUE_INVALID);
+               throw new EvaluationException(ErrorEval.VALUE_INVALID);
        }
 
-
        /**
         * OFFSET's numeric arguments (2..5) have similar processing rules
         */
-       private static int evaluateIntArg(Eval eval, int srcCellRow, short srcCellCol) throws EvalEx {
+       private static int evaluateIntArg(Eval eval, int srcCellRow, short srcCellCol) throws EvaluationException {
 
                double d = evaluateDoubleArg(eval, srcCellRow, srcCellCol);
                return convertDoubleToInt(d);
@@ -295,18 +272,17 @@ public final class Offset implements FreeRefFunction {
                return (int)Math.floor(d);
        }
        
-       
-       private static double evaluateDoubleArg(Eval eval, int srcCellRow, short srcCellCol) throws EvalEx {
-               ValueEval ve = evaluateSingleValue(eval, srcCellRow, srcCellCol);
+       private static double evaluateDoubleArg(Eval eval, int srcCellRow, short srcCellCol) throws EvaluationException {
+               ValueEval ve = OperandResolver.getSingleValue(eval, srcCellRow, srcCellCol);
                
                if (ve instanceof NumericValueEval) {
                        return ((NumericValueEval) ve).getNumberValue();
                }
                if (ve instanceof StringEval) {
                        StringEval se = (StringEval) ve;
-                       Double d = parseDouble(se.getStringValue());
+                       Double d = OperandResolver.parseDouble(se.getStringValue());
                        if(d == null) {
-                               throw new EvalEx(ErrorEval.VALUE_INVALID);
+                               throw new EvaluationException(ErrorEval.VALUE_INVALID);
                        }
                        return d.doubleValue();
                }
@@ -319,44 +295,4 @@ public final class Offset implements FreeRefFunction {
                }
                throw new RuntimeException("Unexpected eval type (" + ve.getClass().getName() + ")");
        }
-       
-       private static Double parseDouble(String s) {
-               // TODO - find a home for this method
-               // TODO - support various number formats: sign char, dollars, commas
-               // OFFSET and COUNTIF seem to handle these
-               return Countif.parseDouble(s);
-       }
-       
-       private static ValueEval evaluateSingleValue(Eval eval, int srcCellRow, short srcCellCol) throws EvalEx {
-               if(eval instanceof RefEval) {
-                       return ((RefEval)eval).getInnerValueEval();
-               }
-               if(eval instanceof AreaEval) {
-                       return chooseSingleElementFromArea((AreaEval)eval, srcCellRow, srcCellCol);
-               }
-               if (eval instanceof ValueEval) {
-                       return (ValueEval) eval;
-               }
-               throw new RuntimeException("Unexpected eval type (" + eval.getClass().getName() + ")");
-       }
-
-       // TODO - this code seems to get repeated a bit
-       private static ValueEval chooseSingleElementFromArea(AreaEval ae, int srcCellRow, short srcCellCol) throws EvalEx {
-               if (ae.isColumn()) {
-                       if (ae.isRow()) {
-                               return ae.getValues()[0];
-                       }
-                       if (!ae.containsRow(srcCellRow)) {
-                               throw new EvalEx(ErrorEval.VALUE_INVALID);
-                       }
-                       return ae.getValueAt(srcCellRow, ae.getFirstColumn());
-               }
-               if (!ae.isRow()) {
-                       throw new EvalEx(ErrorEval.VALUE_INVALID);
-               }
-               if (!ae.containsColumn(srcCellCol)) {
-                       throw new EvalEx(ErrorEval.VALUE_INVALID);
-               }
-               return ae.getValueAt(ae.getFirstRow(), srcCellCol);
-       }
 }
index 7be92c5fa43389d199e7ad4e75b75e1c7a2043f7..eba6607ade6da210856ebd818f93e1676e9ce275 100644 (file)
Binary files a/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls and b/src/testcases/org/apache/poi/hssf/data/FormulaEvalTestData.xls differ
diff --git a/src/testcases/org/apache/poi/hssf/data/countifExamples.xls b/src/testcases/org/apache/poi/hssf/data/countifExamples.xls
new file mode 100644 (file)
index 0000000..b15bd16
Binary files /dev/null and b/src/testcases/org/apache/poi/hssf/data/countifExamples.xls differ
index 1ec657dfe2490a410758d80b56ac67060eb921b7..4d6468a819d83f21acdf52b8a90853a89218201b 100755 (executable)
 
 package org.apache.poi.hssf.record.formula.functions;
 
+import junit.framework.AssertionFailedError;
 import junit.framework.TestCase;
 
+import org.apache.poi.hssf.HSSFTestDataSamples;
 import org.apache.poi.hssf.record.formula.AreaPtg;
 import org.apache.poi.hssf.record.formula.RefPtg;
 import org.apache.poi.hssf.record.formula.eval.Area2DEval;
@@ -31,6 +33,13 @@ import org.apache.poi.hssf.record.formula.eval.NumberEval;
 import org.apache.poi.hssf.record.formula.eval.Ref2DEval;
 import org.apache.poi.hssf.record.formula.eval.StringEval;
 import org.apache.poi.hssf.record.formula.eval.ValueEval;
+import org.apache.poi.hssf.record.formula.functions.Countif.I_MatchPredicate;
+import org.apache.poi.hssf.usermodel.HSSFCell;
+import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator;
+import org.apache.poi.hssf.usermodel.HSSFRow;
+import org.apache.poi.hssf.usermodel.HSSFSheet;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.hssf.usermodel.HSSFFormulaEvaluator.CellValue;
 
 /**
  * Test cases for COUNT(), COUNTA() COUNTIF(), COUNTBLANK()
@@ -146,4 +155,154 @@ public final class TestCountFuncs extends TestCase {
                double result = NumericFunctionInvoker.invoke(new Countif(), args);
                assertEquals(expected, result, 0);
        }
+       
+       public void testCountIfEmptyStringCriteria() {
+               I_MatchPredicate mp;
+               
+               // pred '=' matches blank cell but not empty string
+               mp = Countif.createCriteriaPredicate(new StringEval("="));
+               confirmPredicate(false, mp, "");
+               confirmPredicate(true, mp, null);
+
+               // pred '' matches both blank cell but not empty string
+               mp = Countif.createCriteriaPredicate(new StringEval(""));
+               confirmPredicate(true, mp, "");
+               confirmPredicate(true, mp, null);
+               
+               // pred '<>' matches empty string but not blank cell 
+               mp = Countif.createCriteriaPredicate(new StringEval("<>"));
+               confirmPredicate(false, mp, null);
+               confirmPredicate(true, mp, "");
+       }
+       
+       public void testCountifComparisons() {
+               I_MatchPredicate mp;
+               
+               mp = Countif.createCriteriaPredicate(new StringEval(">5"));
+               confirmPredicate(false, mp, 4);
+               confirmPredicate(false, mp, 5);
+               confirmPredicate(true, mp, 6);
+               
+               mp = Countif.createCriteriaPredicate(new StringEval("<=5"));
+               confirmPredicate(true, mp, 4);
+               confirmPredicate(true, mp, 5);
+               confirmPredicate(false, mp, 6);
+               confirmPredicate(true, mp, "4.9");
+               confirmPredicate(false, mp, "4.9t");
+               confirmPredicate(false, mp, "5.1");
+               confirmPredicate(false, mp, null);
+
+               mp = Countif.createCriteriaPredicate(new StringEval("=abc"));
+               confirmPredicate(true, mp, "abc");
+               
+               mp = Countif.createCriteriaPredicate(new StringEval("=42"));
+               confirmPredicate(false, mp, 41);
+               confirmPredicate(true, mp, 42);
+               confirmPredicate(true, mp, "42");
+
+               mp = Countif.createCriteriaPredicate(new StringEval(">abc"));
+               confirmPredicate(false, mp, 4);
+               confirmPredicate(false, mp, "abc");
+               confirmPredicate(true, mp, "abd");
+
+               mp = Countif.createCriteriaPredicate(new StringEval(">4t3"));
+               confirmPredicate(false, mp, 4);
+               confirmPredicate(false, mp, 500);
+               confirmPredicate(true, mp, "500");
+               confirmPredicate(true, mp, "4t4");
+       }
+       
+       public void testWildCards() {
+               I_MatchPredicate mp;
+               
+               mp = Countif.createCriteriaPredicate(new StringEval("a*b"));
+               confirmPredicate(false, mp, "abc");
+               confirmPredicate(true, mp, "ab");
+               confirmPredicate(true, mp, "axxb");
+               confirmPredicate(false, mp, "xab");
+               
+               mp = Countif.createCriteriaPredicate(new StringEval("a?b"));
+               confirmPredicate(false, mp, "abc");
+               confirmPredicate(false, mp, "ab");
+               confirmPredicate(false, mp, "axxb");
+               confirmPredicate(false, mp, "xab");
+               confirmPredicate(true, mp, "axb");
+               
+               mp = Countif.createCriteriaPredicate(new StringEval("a~?"));
+               confirmPredicate(false, mp, "a~a");
+               confirmPredicate(false, mp, "a~?");
+               confirmPredicate(true, mp, "a?");
+               
+               mp = Countif.createCriteriaPredicate(new StringEval("~*a"));
+               confirmPredicate(false, mp, "~aa");
+               confirmPredicate(false, mp, "~*a");
+               confirmPredicate(true, mp, "*a");
+
+               mp = Countif.createCriteriaPredicate(new StringEval("12?12"));
+               confirmPredicate(false, mp, 12812);
+               confirmPredicate(true, mp, "12812");
+               confirmPredicate(false, mp, "128812");
+       }
+       public void testNotQuiteWildCards() {
+               I_MatchPredicate mp;
+       
+               // make sure special reg-ex chars are treated like normal chars 
+               mp = Countif.createCriteriaPredicate(new StringEval("a.b"));
+               confirmPredicate(false, mp, "aab");
+               confirmPredicate(true, mp, "a.b");
+
+               
+               mp = Countif.createCriteriaPredicate(new StringEval("a~b"));
+               confirmPredicate(false, mp, "ab");
+               confirmPredicate(false, mp, "axb");
+               confirmPredicate(false, mp, "a~~b");
+               confirmPredicate(true, mp, "a~b");
+               
+               mp = Countif.createCriteriaPredicate(new StringEval(">a*b"));
+               confirmPredicate(false, mp, "a(b");
+               confirmPredicate(true, mp, "aab");
+               confirmPredicate(false, mp, "a*a");
+               confirmPredicate(true, mp, "a*c");
+       }
+       
+       private static void confirmPredicate(boolean expectedResult, I_MatchPredicate matchPredicate, int value) {
+               assertEquals(expectedResult, matchPredicate.matches(new NumberEval(value)));
+       }
+       private static void confirmPredicate(boolean expectedResult, I_MatchPredicate matchPredicate, String value) {
+               Eval ev = value == null ? (Eval)BlankEval.INSTANCE : new StringEval(value); 
+               assertEquals(expectedResult, matchPredicate.matches(ev));
+       }
+       
+       public void testCountifFromSpreadsheet() {
+               final String FILE_NAME = "countifExamples.xls";
+               final int START_ROW_IX = 1;
+               final int COL_IX_ACTUAL = 2;
+               final int COL_IX_EXPECTED = 3;
+               
+               int failureCount = 0;
+               HSSFWorkbook wb = HSSFTestDataSamples.openSampleWorkbook(FILE_NAME);
+               HSSFSheet sheet = wb.getSheetAt(0);
+               HSSFFormulaEvaluator fe = new HSSFFormulaEvaluator(sheet, wb);
+               int maxRow = sheet.getLastRowNum();
+               for (int rowIx=START_ROW_IX; rowIx<maxRow; rowIx++) {
+                       HSSFRow row = sheet.getRow(rowIx);
+                       if(row == null) {
+                               continue;
+                       }
+                       HSSFCell cell = row.getCell(COL_IX_ACTUAL);
+                       fe.setCurrentRow(row);
+                       CellValue cv = fe.evaluate(cell);
+                       double actualValue = cv.getNumberValue();
+                       double expectedValue = row.getCell(COL_IX_EXPECTED).getNumericCellValue();
+                       if (actualValue != expectedValue) {
+                               System.err.println("Problem with test case on row " + (rowIx+1) + " "
+                                               + "Expected = (" + expectedValue + ") Actual=(" + actualValue + ") ");
+                               failureCount++;
+                       }
+               }
+               
+               if (failureCount > 0) {
+                       throw new AssertionFailedError(failureCount + " countif evaluations failed. See stderr for more details");
+               }
+       }
 }