org.apache.poi.ss.formula.functions.Countif Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of apache-poi Show documentation
Show all versions of apache-poi Show documentation
The Apache Commons Codec package contains simple encoder and decoders for
various formats such as Base64 and Hexadecimal. In addition to these
widely used encoders and decoders, the codec package also maintains a
collection of phonetic encoding utilities.
/* ==================================================================== 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 java.util.regex.Pattern; import org.apache.poi.ss.formula.ThreeDEval; 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.OperandResolver; import org.apache.poi.ss.formula.eval.RefEval; import org.apache.poi.ss.formula.eval.StringEval; import org.apache.poi.ss.formula.eval.ValueEval; import org.apache.poi.ss.formula.functions.CountUtils.I_MatchPredicate; import org.apache.poi.ss.usermodel.FormulaError; /** * Implementation for the function COUNTIF *
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. * @return* Syntax: COUNTIF ( range, criteria ) *
*
* */ 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 + "'"); } @Override public String toString() { StringBuilder sb = new StringBuilder(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() { return getClass().getName() + " [" + _operator.getRepresentation() + getValueText() + "]"; } 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); } @Override 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: // Excel counts blank values in range as not equal to any value. See Bugzilla 51498 return true; 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; } @Override public boolean matches(ValueEval x) { int testValue; if(x instanceof StringEval) { // Note - Unlike with numbers, it seems that COUNTIF never matches // boolean values when the target(x) is a string return false; // uncomment to observe more intuitive behaviour // 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: // Excel counts blank values in range as not equal to any value. See Bugzilla 51498 return true; default: return false; } } else if((x instanceof NumberEval)) { switch (getCode()) { case CmpOp.NE: // not-equals comparison of a number to boolean always returnes false return true; default: 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(); } @Override 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(); } @Override 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()); } // 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* range is the range of cells to be counted based on the criteria * criteria is used to determine which cells to count null
if the specified value contains no special wildcard characters. */ public static Pattern getWildCardPattern(String value) { int len = value.length(); StringBuilder sb = new StringBuilder(len); boolean hasWildCard = false; for(int i=0; inull null
to represent blank values */ /* 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; } }
© 2015 - 2024 Weber Informatics LLC | Privacy Policy