import org.apache.poi.hssf.record.cf.IconMultiStateFormatting;
import org.apache.poi.hssf.record.cf.PatternFormatting;
import org.apache.poi.ss.formula.ptg.Ptg;
+import org.apache.poi.ss.usermodel.ConditionFilterData;
+import org.apache.poi.ss.usermodel.ConditionFilterType;
import org.apache.poi.ss.usermodel.ConditionType;
import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
cfRuleRecord = pRuleRecord;
}
+ /**
+ * we don't know priority for these, other than definition/model order, which appears to be what Excel uses.
+ * @see org.apache.poi.ss.usermodel.ConditionalFormattingRule#getPriority()
+ */
+ public int getPriority() {
+ return 0;
+ }
+
+ /**
+ * Always true for HSSF files, per Microsoft Excel documentation
+ * @see org.apache.poi.ss.usermodel.ConditionalFormattingRule#getStopIfTrue()
+ */
+ public boolean getStopIfTrue() {
+ return true;
+ }
+
CFRuleBase getCfRuleRecord() {
return cfRuleRecord;
}
return ConditionType.forId(code);
}
+ /**
+ * always null (not a filter condition) or {@link ConditionFilterType#FILTER} if it is.
+ * @see org.apache.poi.ss.usermodel.ConditionalFormattingRule#getConditionFilterType()
+ */
+ public ConditionFilterType getConditionFilterType() {
+ return getConditionType() == ConditionType.FILTER ? ConditionFilterType.FILTER : null;
+ }
+
+ public ConditionFilterData getFilterConfiguration() {
+ return null;
+ }
+
/**
* @return - the comparisionoperatation for the cfrule
*/
--- /dev/null
+/* ====================================================================
+ 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.ConditionalFormatting;
+import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.SheetConditionalFormatting;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.ss.util.CellRangeAddressBase;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.ss.util.SheetUtil;
+
+/**
+ * Evaluates Conditional Formatting constraints.<p/>
+ *
+ * For performance reasons, this class keeps a cache of all previously evaluated rules and cells.
+ * Be sure to call {@link #clearAllCachedFormats()} if any conditional formats are modified, added, or deleted,
+ * and {@link #clearAllCachedValues()} whenever cell values change.
+ * <p/>
+ *
+ */
+public class ConditionalFormattingEvaluator {
+
+ private final WorkbookEvaluator workbookEvaluator;
+ private final Workbook workbook;
+
+ /**
+ * All the underlying structures, for both HSSF and XSSF, repeatedly go to the raw bytes/XML for the
+ * different pieces used in the ConditionalFormatting* structures. That's highly inefficient,
+ * and can cause significant lag when checking formats for large workbooks.
+ * <p/>
+ * Instead we need a cached version that is discarded when definitions change.
+ * <p/>
+ * Sheets don't implement equals, and since its an interface,
+ * there's no guarantee instances won't be recreated on the fly by some implementation.
+ * So we use sheet name.
+ */
+ private final Map<String, List<EvaluationConditionalFormatRule>> formats = new HashMap<String, List<EvaluationConditionalFormatRule>>();
+
+ /**
+ * Evaluating rules for cells in their region(s) is expensive, so we want to cache them,
+ * and empty/reevaluate the cache when values change.
+ * <p/>
+ * Rule lists are in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF)
+ * <p/>
+ * CellReference implements equals().
+ */
+ private final Map<CellReference, List<EvaluationConditionalFormatRule>> values = new HashMap<CellReference, List<EvaluationConditionalFormatRule>>();
+
+ public ConditionalFormattingEvaluator(Workbook wb, WorkbookEvaluatorProvider provider) {
+ this.workbook = wb;
+ this.workbookEvaluator = provider._getWorkbookEvaluator();
+ }
+
+ protected WorkbookEvaluator getWorkbookEvaluator() {
+ return workbookEvaluator;
+ }
+
+ /**
+ * Call this whenever rules are added, reordered, or removed, or a rule formula is changed
+ * (not the formula inputs but the formula expression itself)
+ */
+ public void clearAllCachedFormats() {
+ formats.clear();
+ }
+
+ /**
+ * Call this whenever cell values change in the workbook, so condional formats are re-evaluated
+ * for all cells.
+ * <p/>
+ * TODO: eventually this should work like {@link EvaluationCache#notifyUpdateCell(int, int, EvaluationCell)}
+ * and only clear values that need recalculation based on the formula dependency tree.
+ */
+ public void clearAllCachedValues() {
+ values.clear();
+ }
+
+ /**
+ * lazy load by sheet since reading can be expensive
+ *
+ * @param sheet
+ * @return unmodifiable list of rules
+ */
+ protected List<EvaluationConditionalFormatRule> getRules(Sheet sheet) {
+ final String sheetName = sheet.getSheetName();
+ List<EvaluationConditionalFormatRule> rules = formats.get(sheetName);
+ if (rules == null && ! formats.containsKey(sheetName)) {
+ final SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting();
+ final int count = scf.getNumConditionalFormattings();
+ rules = new ArrayList<EvaluationConditionalFormatRule>(count);
+ formats.put(sheetName, rules);
+ for (int i=0; i < count; i++) {
+ ConditionalFormatting f = scf.getConditionalFormattingAt(i);
+ //optimization, as this may be expensive for lots of ranges
+ final CellRangeAddress[] regions = f.getFormattingRanges();
+ for (int r=0; r < f.getNumberOfRules(); r++) {
+ ConditionalFormattingRule rule = f.getRule(r);
+ rules.add(new EvaluationConditionalFormatRule(workbookEvaluator, sheet, f, i, rule, r, regions));
+ }
+ }
+ // need them in formatting and priority order so logic works right
+ Collections.sort(rules);
+ }
+ return Collections.unmodifiableList(rules);
+ }
+
+ /**
+ * This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
+ * in defined "priority" order, returning the matches if any. This is a property currently
+ * not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
+ * <p/>
+ * Most cells will have zero or one applied rule, but it is possible to define multiple rules
+ * that apply at the same time to the same cell, thus the List result.
+ * <p/>
+ * Note that to properly apply conditional rules, care must be taken to offset the base
+ * formula by the relative position of the current cell, or the wrong value is checked.
+ * This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
+ *
+ * @param cell NOTE: if no sheet name is specified, this uses the workbook active sheet
+ * @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
+ * in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
+ * or null if none apply
+ */
+ public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(final CellReference cellRef) {
+ String sheetName = cellRef.getSheetName();
+ Sheet sheet = null;
+ if (sheetName == null) sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());
+ else sheet = workbook.getSheet(sheetName);
+
+ final Cell cell = SheetUtil.getCell(sheet, cellRef.getRow(), cellRef.getCol());
+
+ if (cell == null) return Collections.emptyList();
+
+ return getConditionalFormattingForCell(cell, cellRef);
+ }
+
+ /**
+ * This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
+ * in defined "priority" order, returning the matches if any. This is a property currently
+ * not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
+ * <p/>
+ * Most cells will have zero or one applied rule, but it is possible to define multiple rules
+ * that apply at the same time to the same cell, thus the List result.
+ * <p/>
+ * Note that to properly apply conditional rules, care must be taken to offset the base
+ * formula by the relative position of the current cell, or the wrong value is checked.
+ * This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
+ *
+ * @param cell
+ * @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
+ * in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
+ * or null if none apply
+ */
+ public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell) {
+ return getConditionalFormattingForCell(cell, getRef(cell));
+ }
+
+ /**
+ * We need both, and can derive one from the other, but this is to avoid duplicate work
+ *
+ * @param cell
+ * @param ref
+ * @return unmodifiable list of matching rules
+ */
+ private List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell, CellReference ref) {
+ List<EvaluationConditionalFormatRule> rules = values.get(ref);
+
+ if (rules == null) {
+ // compute and cache them
+ rules = new ArrayList<EvaluationConditionalFormatRule>();
+ /*
+ * Per Excel help:
+ * https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-e09711a3-48df-4bcb-b82c-9d8b8b22463d#__toc269129417
+ * stopIfTrue is true for all rules from HSSF files, and an explicit value for XSSF files.
+ * thus the explicit ordering of the rule lists in #getFormattingRulesForSheet(Sheet)
+ */
+ boolean stopIfTrue = false;
+ for (EvaluationConditionalFormatRule rule : getRules(cell.getSheet())) {
+
+ if (stopIfTrue) continue; // a previous rule matched and wants no more evaluations
+
+ if (rule.matches(cell)) {
+ rules.add(rule);
+ stopIfTrue = rule.getRule().getStopIfTrue();
+ }
+ }
+ Collections.sort(rules);
+ values.put(ref, rules);
+ }
+
+ return Collections.unmodifiableList(rules);
+ }
+
+ public static CellReference getRef(Cell cell) {
+ return new CellReference(cell.getSheet().getSheetName(), cell.getRowIndex(), cell.getColumnIndex(), false, false);
+ }
+
+ /**
+ * @param sheetName
+ * @return unmodifiable list of all Conditional format rules for the given sheet, if any
+ */
+ public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(String sheetName) {
+ return getFormatRulesForSheet(workbook.getSheet(sheetName));
+ }
+
+ /**
+ * @param sheet
+ * @return unmodifiable list of all Conditional format rules for the given sheet, if any
+ */
+ public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(Sheet sheet) {
+ return getRules(sheet);
+ }
+
+ /**
+ * Conditional formatting rules can apply only to cells in the sheet to which they are attached.
+ * The POI data model does not have a back-reference to the owning sheet, so it must be passed in separately.
+ * <p/>
+ * We could overload this with convenience methods taking a sheet name and sheet index as well.
+ * <p/>
+ * @param sheet containing the rule
+ * @param index of the {@link ConditionalFormatting} instance in the sheet's array
+ * @return unmodifiable List of all cells in the rule's region matching the rule's condition
+ */
+ public List<Cell> getMatchingCells(Sheet sheet, int conditionalFormattingIndex, int ruleIndex) {
+ for (EvaluationConditionalFormatRule rule : getRules(sheet)) {
+ if (rule.getSheet().equals(sheet) && rule.getFormattingIndex() == conditionalFormattingIndex && rule.getRuleIndex() == ruleIndex) {
+ return getMatchingCells(rule);
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ *
+ * @param rule
+ * @return unmodifiable List of all cells in the rule's region matching the rule's condition
+ */
+ public List<Cell> getMatchingCells(EvaluationConditionalFormatRule rule) {
+ final List<Cell> cells = new ArrayList<Cell>();
+ final Sheet sheet = rule.getSheet();
+
+ for (CellRangeAddress region : rule.getRegions()) {
+ for (int r = region.getFirstRow(); r <= region.getLastRow(); r++) {
+ final Row row = sheet.getRow(r);
+ if (row == null) continue; // no cells to check
+ for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) {
+ final Cell cell = row.getCell(c);
+ if (cell == null) continue;
+
+ List<EvaluationConditionalFormatRule> cellRules = getConditionalFormattingForCell(cell);
+ if (cellRules.contains(rule)) cells.add(cell);
+ }
+ }
+ }
+ return Collections.unmodifiableList(cells);
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+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.RefEval;
+import org.apache.poi.ss.formula.eval.StringEval;
+import org.apache.poi.ss.formula.eval.ValueEval;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.DataValidation;
+import org.apache.poi.ss.usermodel.DataValidationConstraint;
+import org.apache.poi.ss.usermodel.DataValidationConstraint.OperatorType;
+import org.apache.poi.ss.usermodel.DataValidationConstraint.ValidationType;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.util.CellRangeAddressBase;
+import org.apache.poi.ss.util.CellRangeAddressList;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.ss.util.SheetUtil;
+
+/**
+ * Evaluates Data Validation constraints.<p/>
+ *
+ * For performance reasons, this class keeps a cache of all previously retrieved {@link DataValidation} instances.
+ * Be sure to call {@link #clearAllCachedValues()} if any workbook validation definitions are
+ * added, modified, or deleted.
+ * <p/>
+ * Changing cell values should be fine, as long as the corresponding {@link WorkbookEvaluator#clearAllCachedResultValues()}
+ * is called as well.
+ *
+ */
+public class DataValidationEvaluator {
+
+ /**
+ * Expensive to compute, so cache them as they are retrieved.
+ * <p/>
+ * Sheets don't implement equals, and since its an interface,
+ * there's no guarantee instances won't be recreated on the fly by some implementation.
+ * So we use sheet name.
+ */
+ private final Map<String, List<? extends DataValidation>> validations = new HashMap<String, List<? extends DataValidation>>();
+
+ private final Workbook workbook;
+ private final WorkbookEvaluator workbookEvaluator;
+
+ public DataValidationEvaluator(Workbook wb, WorkbookEvaluatorProvider provider) {
+ this.workbook = wb;
+ this.workbookEvaluator = provider._getWorkbookEvaluator();
+ }
+
+ protected WorkbookEvaluator getWorkbookEvaluator() {
+ return workbookEvaluator;
+ }
+
+ public void clearAllCachedValues() {
+ validations.clear();
+ }
+
+ /**
+ * lazy load validations by sheet, since reading the CT* types is expensive
+ * @param sheet
+ * @return
+ */
+ private List<? extends DataValidation> getValidations(Sheet sheet) {
+ List<? extends DataValidation> dvs = validations.get(sheet.getSheetName());
+ if (dvs == null && !validations.containsKey(sheet.getSheetName())) {
+ dvs = sheet.getDataValidations();
+ validations.put(sheet.getSheetName(), dvs);
+ }
+ return dvs;
+ }
+
+ /**
+ * Finds and returns the {@link DataValidation} for the cell, if there is
+ * one. Lookup is based on the first match from
+ * {@link DataValidation#getRegions()} for the cell's sheet. DataValidation
+ * regions must be in the same sheet as the DataValidation. Allowed values
+ * expressions may reference other sheets, however.
+ *
+ * @param cell reference to check - use this in case the cell does not actually exist yet
+ * @return the DataValidation applicable to the given cell, or null if no
+ * validation applies
+ */
+ public DataValidation getValidationForCell(CellReference cell) {
+ return getValidationContextForCell(cell).getValidation();
+ }
+
+ public DataValidationContext getValidationContextForCell(CellReference cell) {
+ // TODO
+ final Sheet sheet = workbook.getSheet(cell.getSheetName());
+ if (sheet == null) return null;
+ final List<? extends DataValidation> dataValidations = getValidations(sheet);
+ if (dataValidations == null) return null;
+ for (DataValidation dv : dataValidations) {
+ final CellRangeAddressList regions = dv.getRegions();
+ if (regions == null) return null;
+ // current implementation can't return null
+ for (CellRangeAddressBase range : regions.getCellRangeAddresses()) {
+ if (range.isInRange(cell)) {
+ return new DataValidationContext(dv, this, range, cell);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * If {@link #getValidationForCell(Cell)} returns an instance, and the
+ * {@link ValidationType} is {@link ValidationType#LIST}, return the valid
+ * values, whether they are from a static list or cell range.
+ * <p/>
+ * For all other validation types, or no validation at all, this method
+ * returns null.
+ * <p/>
+ * This method could throw an exception if the validation type is not LIST,
+ * but since this method is mostly useful in UI contexts, null seems the
+ * easier path.
+ *
+ * @param cell reference to check - use this in case the cell does not actually exist yet
+ * @return returns an unmodifiable {@link List} of {@link ValueEval}s if applicable, or
+ * null
+ */
+ public List<ValueEval> getValidationValuesForCell(CellReference cell) {
+ DataValidationContext context = getValidationContextForCell(cell);
+
+ if (context == null) return null;
+
+ return getValidationValuesForConstraint(context);
+ }
+
+ /**
+ * static so enums can reference it without creating a whole instance
+ * @param cell
+ * @param val
+ * @return returns an unmodifiable {@link List} of {@link ValueEval}s, which may be empty
+ */
+ protected static List<ValueEval> getValidationValuesForConstraint(DataValidationContext context) {
+ final DataValidationConstraint val = context.getValidation().getValidationConstraint();
+ if (val.getValidationType() != ValidationType.LIST) return null;
+
+ String formula = val.getFormula1();
+
+ final List<ValueEval> values = new ArrayList<ValueEval>();
+
+ if (val.getExplicitListValues() != null && val.getExplicitListValues().length > 0) {
+ // assumes parsing interprets the overloaded property right for XSSF
+ for (String s : val.getExplicitListValues()) {
+ if (s != null) values.add(new StringEval(s)); // constructor throws exception on null
+ }
+ } else if (formula != null) {
+ // evaluate formula for cell refs then get their values
+ ValueEval eval = context.getEvaluator().getWorkbookEvaluator().evaluate(formula, context.getTarget(), context.getRegion());
+ // formula is a StringEval if the validation is by a fixed list. Use the explicit list later.
+ // there is no way from the model to tell if the list is fixed values or formula based.
+ if (eval instanceof TwoDEval) {
+ TwoDEval twod = (TwoDEval) eval;
+ for (int i=0; i < twod.getHeight(); i++) {
+ final ValueEval cellValue = twod.getValue(i, 0);
+ values.add(cellValue);
+ }
+ }
+ }
+ return Collections.unmodifiableList(values);
+ }
+
+ /**
+ * Use the validation returned by {@link #getValidationForCell(Cell)} if you
+ * want the error display details. This is the validation checked by this
+ * method, which attempts to replicate Excel's data validation rules.
+ * <p/>
+ * Note that to properly apply some validations, care must be taken to
+ * offset the base validation formula by the relative position of the
+ * current cell, or the wrong value is checked.
+ *
+ * @param cell
+ * @return true if the cell has no validation or the cell value passes the
+ * defined validation, false if it fails
+ */
+ public boolean isValidCell(CellReference cellRef) {
+ final DataValidationContext context = getValidationContextForCell(cellRef);
+
+ if (context == null) return true;
+
+ final Cell cell = SheetUtil.getCell(workbook.getSheet(cellRef.getSheetName()), cellRef.getRow(), cellRef.getCol());
+
+ // now we can validate the cell
+
+ // if empty, return not allowed flag
+ if ( cell == null
+ || isType(cell, CellType.BLANK)
+ || (isType(cell,CellType.STRING)
+ && (cell.getStringCellValue() == null || cell.getStringCellValue().isEmpty())
+ )
+ ) {
+ return context.getValidation().getEmptyCellAllowed();
+ }
+
+ // cell has a value
+
+ return ValidationEnum.isValid(cell, context);
+ }
+
+ /**
+ * Note that this assumes the cell cached value is up to date and in sync with data edits
+ * @param cell
+ * @param type
+ * @return true if the cell or cached cell formula result type match the given type
+ */
+ public static boolean isType(Cell cell, CellType type) {
+ final CellType cellType = cell.getCellTypeEnum();
+ return cellType == type
+ || (cellType == CellType.FORMULA
+ && cell.getCachedFormulaResultTypeEnum() == type
+ );
+ }
+
+
+ /**
+ * Not calling it ValidationType to avoid confusion for now with DataValidationConstraint.ValidationType.
+ * Definition order matches OOXML type ID indexes
+ */
+ public static enum ValidationEnum {
+ ANY {
+ public boolean isValidValue(Cell cell, DataValidationContext context) {
+ return true;
+ }
+ },
+ INTEGER {
+ public boolean isValidValue(Cell cell, DataValidationContext context) {
+ if (super.isValidValue(cell, context)) {
+ // we know it is a number in the proper range, now check if it is an int
+ final double value = cell.getNumericCellValue(); // can't get here without a valid numeric value
+ return Double.valueOf(value).compareTo(Double.valueOf((int) value)) == 0;
+ }
+ return false;
+ }
+ },
+ DECIMAL,
+ LIST {
+ public boolean isValidValue(Cell cell, DataValidationContext context) {
+ final List<ValueEval> valueList = getValidationValuesForConstraint(context);
+ if (valueList == null) return true; // special case
+
+ // compare cell value to each item
+ for (ValueEval listVal : valueList) {
+ ValueEval comp = listVal instanceof RefEval ? ((RefEval) listVal).getInnerValueEval(context.getSheetIndex()) : listVal;
+
+ // any value is valid if the list contains a blank value per Excel help
+ if (comp instanceof BlankEval) return true;
+ if (comp instanceof ErrorEval) continue; // nothing to check
+ if (comp instanceof BoolEval) {
+ if (isType(cell, CellType.BOOLEAN) && ((BoolEval) comp).getBooleanValue() == cell.getBooleanCellValue() ) {
+ return true;
+ } else {
+ continue; // check the rest
+ }
+ }
+ if (comp instanceof NumberEval) {
+ // could this have trouble with double precision/rounding errors and date/time values?
+ // do we need to allow a "close enough" double fractional range?
+ // I see 17 digits after the decimal separator in XSSF files, and for time values,
+ // there are sometimes discrepancies in the final decimal place.
+ // I don't have a validation test case yet though. - GW
+ if (isType(cell, CellType.NUMERIC) && ((NumberEval) comp).getNumberValue() == cell.getNumericCellValue()) {
+ return true;
+ } else {
+ continue; // check the rest
+ }
+ }
+ if (comp instanceof StringEval) {
+ // interestingly, in testing, a validation value of the string "TRUE" or "true"
+ // did not match a boolean cell value of TRUE - so apparently cell type matters
+ // also, Excel validation is case insensitive - "true" is valid for the list value "TRUE"
+ if (isType(cell, CellType.STRING) && ((StringEval) comp).getStringValue().equalsIgnoreCase(cell.getStringCellValue())) {
+ return true;
+ } else {
+ continue; // check the rest;
+ }
+ }
+ }
+ return false; // no matches
+ }
+ },
+ DATE,
+ TIME,
+ TEXT_LENGTH {
+ public boolean isValidValue(Cell cell, DataValidationContext context) {
+ if (! isType(cell, CellType.STRING)) return false;
+ String v = cell.getStringCellValue();
+ return isValidNumericValue(Double.valueOf(v.length()), context);
+ }
+ },
+ FORMULA {
+ /**
+ * Note the formula result must either be a boolean result, or anything not in error.
+ * If boolean, value must be true to pass, anything else valid is also passing, errors fail.
+ * @see org.apache.poi.ss.formula.DataValidationEvaluator.ValidationEnum#isValidValue(org.apache.poi.ss.usermodel.Cell, org.apache.poi.ss.usermodel.DataValidationConstraint, org.apache.poi.ss.formula.WorkbookEvaluator)
+ */
+ public boolean isValidValue(Cell cell, DataValidationContext context) {
+ ValueEval comp = context.getEvaluator().getWorkbookEvaluator().evaluate(context.getFormula1(), context.getTarget(), context.getRegion());
+ if (comp instanceof RefEval) {
+ comp = ((RefEval) comp).getInnerValueEval(((RefEval) comp).getFirstSheetIndex());
+ }
+
+ if (comp instanceof BlankEval) return true;
+ if (comp instanceof ErrorEval) return false;
+ if (comp instanceof BoolEval) {
+ return ((BoolEval) comp).getBooleanValue();
+ }
+ // empirically tested in Excel - 0=false, any other number = true/valid
+ // see test file DataValidationEvaluations.xlsx
+ if (comp instanceof NumberEval) {
+ return ((NumberEval) comp).getNumberValue() != 0;
+ }
+ return false; // anything else is false, such as text
+ }
+ },
+ ;
+
+ public boolean isValidValue(Cell cell, DataValidationContext context) {
+ return isValidNumericCell(cell, context);
+ }
+
+ /**
+ * Uses the cell value, which may be the cached formula result value.
+ * We won't re-evaluate cells here. This validation would be after the cell value was updated externally.
+ * Excel allows invalid values through methods like copy/paste, and only validates them when the user
+ * interactively edits the cell.
+ * @param cell
+ * @param dvc
+ * @param wbe
+ * @return
+ */
+ protected boolean isValidNumericCell(Cell cell, DataValidationContext context) {
+ if ( ! isType(cell, CellType.NUMERIC)) return false;
+
+ Double value = Double.valueOf(cell.getNumericCellValue());
+ return isValidNumericValue(value, context);
+ }
+
+ /**
+ *
+ * @param value
+ * @param context
+ * @return
+ */
+ protected boolean isValidNumericValue(Double value, final DataValidationContext context) {
+ try {
+ Double t1 = evalOrConstant(context.getFormula1(), context);
+ // per Excel, a blank value for a numeric validation constraint formula validates true
+ if (t1 == null) return true;
+ Double t2 = null;
+ if (context.getOperator() == OperatorType.BETWEEN || context.getOperator() == OperatorType.NOT_BETWEEN) {
+ t2 = evalOrConstant(context.getFormula2(), context);
+ // per Excel, a blank value for a numeric validation constraint formula validates true
+ if (t2 == null) return true;
+ }
+ return OperatorEnum.values()[context.getOperator()].isValid(value, t1, t2);
+ } catch (NumberFormatException e) {
+ // one or both formulas are in error, not evaluating to a number, so the validation is false per Excel's behavior.
+ return false;
+ }
+ }
+
+ /**
+ * Evaluate a numeric formula value as either a constant or numeric expression.
+ * Note that Excel treats validations with constraint formulas that evaluate to null as valid,
+ * but evaluations in error or non-numeric are marked invalid.
+ * @param formula
+ * @param context
+ * @return numeric value or null if not defined or the formula evaluates to an empty/missing cell.
+ * @throws NumberFormatException if the formula is non-numeric when it should be
+ */
+ private Double evalOrConstant(String formula, DataValidationContext context) throws NumberFormatException {
+ if (formula == null || formula.trim().isEmpty()) return null; // shouldn't happen, but just in case
+ try {
+ return Double.valueOf(formula);
+ } catch (NumberFormatException e) {
+ // must be an expression, then. Overloading by Excel in the file formats.
+ }
+ ValueEval eval = context.getEvaluator().getWorkbookEvaluator().evaluate(formula, context.getTarget(), context.getRegion());
+ if (eval instanceof RefEval) {
+ eval = ((RefEval) eval).getInnerValueEval(((RefEval) eval).getFirstSheetIndex());
+ }
+ if (eval instanceof BlankEval) return null;
+ if (eval instanceof NumberEval) return Double.valueOf(((NumberEval) eval).getNumberValue());
+ if (eval instanceof StringEval) {
+ final String value = ((StringEval) eval).getStringValue();
+ if (value == null || value.trim().isEmpty()) return null;
+ // try to parse the cell value as a double and return it
+ return Double.valueOf(value);
+ }
+ throw new NumberFormatException("Formula '" + formula + "' evaluates to something other than a number");
+ }
+
+ /**
+ * Validates against the type defined in dvc, as an index of the enum values array.
+ * @param cell
+ * @param dvc
+ * @param wbe
+ * @return true if validation passes
+ * @throws ArrayIndexOutOfBoundsException if the constraint type is an invalid index
+ */
+ public static boolean isValid(Cell cell, DataValidationContext context) {
+ return values()[context.getValidation().getValidationConstraint().getValidationType()].isValidValue(cell, context);
+ }
+
+ }
+
+ /**
+ * Not calling it OperatorType to avoid confusion for now with DataValidationConstraint.OperatorType.
+ * Definition order matches OOXML type ID indexes
+ */
+ public static enum OperatorEnum {
+ BETWEEN {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) >= 0 && cellValue.compareTo(v2) <= 0;
+ }
+ },
+ NOT_BETWEEN {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) < 0 || cellValue.compareTo(v2) > 0;
+ }
+ },
+ EQUAL {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) == 0;
+ }
+ },
+ NOT_EQUAL {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) != 0;
+ }
+ },
+ GREATER_THAN {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) > 0;
+ }
+ },
+ LESS_THAN {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) < 0;
+ }
+ },
+ GREATER_OR_EQUAL {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) >= 0;
+ }
+ },
+ LESS_OR_EQUAL {
+ public boolean isValid(Double cellValue, Double v1, Double v2) {
+ return cellValue.compareTo(v1) <= 0;
+ }
+ },
+ ;
+
+ public static final OperatorEnum IGNORED = BETWEEN;
+
+ /**
+ * Evaluates comparison using operator instance rules
+ * @param cellValue won't be null, assumption is previous checks handled that
+ * @param v1 if null, value assumed invalid, anything passes, per Excel behavior
+ * @param v2 null if not needed. If null when needed, assume anything passes, per Excel behavior
+ * @return true if the comparison is valid
+ */
+ public abstract boolean isValid(Double cellValue, Double v1, Double v2);
+ }
+
+ public static class DataValidationContext {
+ private final DataValidation dv;
+ private final DataValidationEvaluator dve;
+ private final CellRangeAddressBase region;
+ private final CellReference target;
+
+ /**
+ *
+ * @param dv
+ * @param dve
+ * @param region
+ * @param target
+ */
+ public DataValidationContext(DataValidation dv, DataValidationEvaluator dve, CellRangeAddressBase region, CellReference target) {
+ this.dv = dv;
+ this.dve = dve;
+ this.region = region;
+ this.target = target;
+ }
+ /**
+ * @return the dv
+ */
+ public DataValidation getValidation() {
+ return dv;
+ }
+ /**
+ * @return the dve
+ */
+ public DataValidationEvaluator getEvaluator() {
+ return dve;
+ }
+ /**
+ * @return the region
+ */
+ public CellRangeAddressBase getRegion() {
+ return region;
+ }
+ /**
+ * @return the target
+ */
+ public CellReference getTarget() {
+ return target;
+ }
+
+ public int getOffsetColumns() {
+ return target.getCol() - region.getFirstColumn();
+ }
+
+ public int getOffsetRows() {
+ return target.getRow() - region.getFirstRow();
+ }
+
+ public int getSheetIndex() {
+ return dve.getWorkbookEvaluator().getSheetIndex(target.getSheetName());
+ }
+
+ public String getFormula1() {
+ return dv.getValidationConstraint().getFormula1();
+ }
+
+ public String getFormula2() {
+ return dv.getValidationConstraint().getFormula2();
+ }
+
+ public int getOperator() {
+ return dv.getValidationConstraint().getOperator();
+ }
+
+ }
+}
--- /dev/null
+/* ====================================================================
+ 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;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+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.RefEval;
+import org.apache.poi.ss.formula.eval.StringEval;
+import org.apache.poi.ss.formula.eval.ValueEval;
+import org.apache.poi.ss.formula.functions.AggregateFunction;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.ConditionFilterData;
+import org.apache.poi.ss.usermodel.ConditionFilterType;
+import org.apache.poi.ss.usermodel.ConditionType;
+import org.apache.poi.ss.usermodel.ConditionalFormatting;
+import org.apache.poi.ss.usermodel.ConditionalFormattingRule;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.util.CellRangeAddress;
+
+/**
+ * Abstracted and cached version of a Conditional Format rule for use with a
+ * {@link ConditionalFormattingEvaluator}. This references a rule, its owning
+ * {@link ConditionalFormatting}, its priority order (lower index = higher priority in Excel),
+ * and the information needed to evaluate the rule for a given cell.
+ * <p/>
+ * Having this all combined and cached avoids repeated access calls to the
+ * underlying structural objects, XSSF CT* objects and HSSF raw byte structures.
+ * Those objects can be referenced from here. This object will be out of sync if
+ * anything modifies the referenced structures' evaluation properties.
+ * <p/>
+ * The assumption is that consuming applications will read the display properties once and
+ * create whatever style objects they need, caching those at the application level.
+ * Thus this class only caches values needed for evaluation, not display.
+ */
+public class EvaluationConditionalFormatRule implements Comparable<EvaluationConditionalFormatRule> {
+
+ private final WorkbookEvaluator workbookEvaluator;
+ private final Sheet sheet;
+ private final ConditionalFormatting formatting;
+ private final ConditionalFormattingRule rule;
+
+ /* cached values */
+ private final CellRangeAddress[] regions;
+ /**
+ * Depending on the rule type, it may want to know about certain values in the region when evaluating {@link #matches(Cell)},
+ * such as top 10, unique, duplicate, average, etc. This collection stores those if needed so they are not repeatedly calculated
+ */
+ private final Map<CellRangeAddress, Set<ValueAndFormat>> meaningfulRegionValues = new HashMap<CellRangeAddress, Set<ValueAndFormat>>();
+
+ private final int priority;
+ private final int formattingIndex;
+ private final int ruleIndex;
+ private final String formula1;
+ private final String formula2;
+ private final OperatorEnum operator;
+ private final ConditionType type;
+
+ /**
+ *
+ * @param workbookEvaluator
+ * @param sheet
+ * @param formatting
+ * @param formattingIndex for priority, zero based
+ * @param rule
+ * @param ruleIndex for priority, zero based, if this is an HSSF rule. Unused for XSSF rules
+ * @param regions could be read from formatting, but every call creates new objects in a new array.
+ * this allows calling it once per formatting instance, and re-using the array.
+ */
+ public EvaluationConditionalFormatRule(WorkbookEvaluator workbookEvaluator, Sheet sheet, ConditionalFormatting formatting, int formattingIndex, ConditionalFormattingRule rule, int ruleIndex, CellRangeAddress[] regions) {
+ super();
+ this.workbookEvaluator = workbookEvaluator;
+ this.sheet = sheet;
+ this.formatting = formatting;
+ this.rule = rule;
+ this.formattingIndex = formattingIndex;
+ this.ruleIndex = ruleIndex;
+
+ this.priority = rule.getPriority();
+
+ this.regions = regions;
+ formula1 = rule.getFormula1();
+ formula2 = rule.getFormula2();
+
+ operator = OperatorEnum.values()[rule.getComparisonOperation()];
+ type = rule.getConditionType();
+ }
+
+ public Sheet getSheet() {
+ return sheet;
+ }
+
+ /**
+ * @return the formatting
+ */
+ public ConditionalFormatting getFormatting() {
+ return formatting;
+ }
+
+ public int getFormattingIndex() {
+ return formattingIndex;
+ }
+
+ /**
+ * @return the rule
+ */
+ public ConditionalFormattingRule getRule() {
+ return rule;
+ }
+
+ public int getRuleIndex() {
+ return ruleIndex;
+ }
+
+ /**
+ * @return the regions
+ */
+ public CellRangeAddress[] getRegions() {
+ return regions;
+ }
+
+ /**
+ * @return the priority
+ */
+ public int getPriority() {
+ return priority;
+ }
+
+ /**
+ * @return the formula1
+ */
+ public String getFormula1() {
+ return formula1;
+ }
+
+ /**
+ * @return the formula2
+ */
+ public String getFormula2() {
+ return formula2;
+ }
+
+ /**
+ * @return the operator
+ */
+ public OperatorEnum getOperator() {
+ return operator;
+ }
+
+ /**
+ * @return the type
+ */
+ public ConditionType getType() {
+ return type;
+ }
+
+ /**
+ * Defined as equal sheet name and formatting and rule indexes
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ public boolean equals(Object obj) {
+ if (obj == null) return false;
+ if (! obj.getClass().equals(this.getClass())) return false;
+ final EvaluationConditionalFormatRule r = (EvaluationConditionalFormatRule) obj;
+ return getSheet().getSheetName().equalsIgnoreCase(r.getSheet().getSheetName())
+ && getFormattingIndex() == r.getFormattingIndex()
+ && getRuleIndex() == r.getRuleIndex();
+ }
+
+ /**
+ * Per Excel Help, XSSF rule priority is sheet-wide, not just within the owning ConditionalFormatting object.
+ * This can be seen by creating 4 rules applying to two different ranges and examining the XML.
+ * <p/>
+ * HSSF priority is based on definition/persistence order.
+ *
+ * @param o
+ * @return comparison based on sheet name, formatting index, and rule priority
+ */
+ public int compareTo(EvaluationConditionalFormatRule o) {
+ int cmp = getSheet().getSheetName().compareToIgnoreCase(o.getSheet().getSheetName());
+ if (cmp != 0) return cmp;
+
+ final int x = getPriority();
+ final int y = o.getPriority();
+ // logic from Integer.compare()
+ cmp = (x < y) ? -1 : ((x == y) ? 0 : 1);
+ if (cmp != 0) return cmp;
+
+ cmp = Integer.compare(getFormattingIndex(), o.getFormattingIndex());
+ if (cmp != 0) return cmp;
+ return Integer.compare(getRuleIndex(), o.getRuleIndex());
+ }
+
+ public int hashCode() {
+ int hash = sheet.getSheetName().hashCode();
+ hash = 31 * hash + formattingIndex;
+ hash = 31 * hash + ruleIndex;
+ return hash;
+ }
+
+ /**
+ * @param cell
+ * @return true if this rule evaluates to true for the given cell
+ */
+ /* package */ boolean matches(Cell cell) {
+ // first check that it is in one of the regions defined for this format
+ CellRangeAddress region = null;
+ for (CellRangeAddress r : regions) {
+ if (r.isInRange(cell)) {
+ region = r;
+ break;
+ }
+ }
+
+ if (region == null) return false; // cell not in range of this rule
+
+ final ConditionType ruleType = getRule().getConditionType();
+
+ // these rules apply to all cells in a region. Specific condition criteria
+ // may specify no special formatting for that value partition, but that's display logic
+ if (ruleType.equals(ConditionType.COLOR_SCALE)
+ || ruleType.equals(ConditionType.DATA_BAR)
+ || ruleType.equals(ConditionType.ICON_SET)) {
+ return true;
+ }
+
+ if (ruleType.equals(ConditionType.CELL_VALUE_IS)) {
+ return checkValue(cell, region);
+ }
+ if (ruleType.equals(ConditionType.FORMULA)) {
+ return checkFormula(cell, region);
+ }
+ if (ruleType.equals(ConditionType.FILTER)) {
+ return checkFilter(cell, region);
+ }
+
+ // TODO: anything else, we don't handle yet, such as top 10
+ return false;
+ }
+
+ /**
+ * @param cell
+ * @param region for adjusting relative formulas
+ * @return
+ */
+ private boolean checkValue(Cell cell, CellRangeAddress region) {
+ if (cell == null || DataValidationEvaluator.isType(cell, CellType.BLANK)
+ || DataValidationEvaluator.isType(cell,CellType.ERROR)
+ || (DataValidationEvaluator.isType(cell,CellType.STRING)
+ && (cell.getStringCellValue() == null || cell.getStringCellValue().isEmpty())
+ )
+ ) return false;
+
+ ValueEval eval = unwrapEval(workbookEvaluator.evaluate(rule.getFormula1(), ConditionalFormattingEvaluator.getRef(cell), region));
+
+ String f2 = rule.getFormula2();
+ ValueEval eval2 = null;
+ if (f2 != null && f2.length() > 0) {
+ eval2 = unwrapEval(workbookEvaluator.evaluate(f2, ConditionalFormattingEvaluator.getRef(cell), region));
+ }
+
+ // we assume the cell has been evaluated, and the current formula value stored
+ if (DataValidationEvaluator.isType(cell, CellType.BOOLEAN)) {
+ if (eval instanceof BoolEval && (eval2 == null || eval2 instanceof BoolEval) ) {
+ return operator.isValid(cell.getBooleanCellValue(), ((BoolEval) eval).getBooleanValue(), eval2 == null ? null : ((BoolEval) eval2).getBooleanValue());
+ }
+ return false; // wrong types
+ }
+ if (DataValidationEvaluator.isType(cell, CellType.NUMERIC)) {
+ if (eval instanceof NumberEval && (eval2 == null || eval2 instanceof NumberEval) ) {
+ return operator.isValid(cell.getNumericCellValue(), ((NumberEval) eval).getNumberValue(), eval2 == null ? null : ((NumberEval) eval2).getNumberValue());
+ }
+ return false; // wrong types
+ }
+ if (DataValidationEvaluator.isType(cell, CellType.STRING)) {
+ if (eval instanceof StringEval && (eval2 == null || eval2 instanceof StringEval) ) {
+ return operator.isValid(cell.getStringCellValue(), ((StringEval) eval).getStringValue(), eval2 == null ? null : ((StringEval) eval2).getStringValue());
+ }
+ return false; // wrong types
+ }
+
+ // should not get here, but in case...
+ return false;
+ }
+
+ private ValueEval unwrapEval(ValueEval eval) {
+ ValueEval comp = eval;
+
+ while (comp instanceof RefEval) {
+ RefEval ref = (RefEval) comp;
+ comp = ref.getInnerValueEval(ref.getFirstSheetIndex());
+ }
+ return comp;
+ }
+ /**
+ * @param cell needed for offsets from region anchor
+ * @param region for adjusting relative formulas
+ * @return true/false using the same rules as Data Validation evaluations
+ */
+ private boolean checkFormula(Cell cell, CellRangeAddress region) {
+ ValueEval comp = unwrapEval(workbookEvaluator.evaluate(rule.getFormula1(), ConditionalFormattingEvaluator.getRef(cell), region));
+
+ // Copied for now from DataValidationEvaluator.ValidationEnum.FORMULA#isValidValue()
+ if (comp instanceof BlankEval) return true;
+ if (comp instanceof ErrorEval) return false;
+ if (comp instanceof BoolEval) {
+ return ((BoolEval) comp).getBooleanValue();
+ }
+ // empirically tested in Excel - 0=false, any other number = true/valid
+ // see test file DataValidationEvaluations.xlsx
+ if (comp instanceof NumberEval) {
+ return ((NumberEval) comp).getNumberValue() != 0;
+ }
+ return false; // anything else is false, such as text
+ }
+
+ private boolean checkFilter(Cell cell, CellRangeAddress region) {
+ final ConditionFilterType filterType = rule.getConditionFilterType();
+ if (filterType == null) return false;
+
+ // TODO: this could/should be delegated to the Enum type, but that's in the usermodel package,
+ // we may not want evaluation code there. Of course, maybe the enum should go here in formula,
+ // and not be returned by the SS model, but then we need the XSSF rule to expose the raw OOXML
+ // type value, which isn't ideal either.
+ switch (filterType) {
+ case FILTER:
+ return false; // we don't evaluate HSSF filters yet
+ case TOP_10:
+ // from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range.
+ // numbers stored as text are ignored, but numbers formatted as text are treated as numbers.
+
+ final ValueAndFormat cv10 = getCellValue(cell);
+ if (! cv10.isNumber()) return false;
+
+ return getMeaningfulValues(region, false, new ValueFunction() {
+ public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
+ List<ValueAndFormat> values = allValues;
+ final ConditionFilterData conf = rule.getFilterConfiguration();
+
+ if (! conf.getBottom()) Collections.sort(values, Collections.reverseOrder());
+ else Collections.sort(values);
+
+ int limit = (int) conf.getRank();
+ if (conf.getPercent()) limit = allValues.size() * limit / 100;
+ if (allValues.size() <= limit) return new HashSet<ValueAndFormat>(allValues);
+
+ return new HashSet<ValueAndFormat>(allValues.subList(0, limit));
+ }
+ }).contains(cv10);
+ case UNIQUE_VALUES:
+ // Per Excel help, "duplicate" means matching value AND format
+ // https://support.office.com/en-us/article/Filter-for-unique-values-or-remove-duplicate-values-ccf664b0-81d6-449b-bbe1-8daaec1e83c2
+ return getMeaningfulValues(region, true, new ValueFunction() {
+ public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
+ List<ValueAndFormat> values = allValues;
+ Collections.sort(values);
+
+ final Set<ValueAndFormat> unique = new HashSet<ValueAndFormat>();
+
+ for (int i=0; i < values.size(); i++) {
+ final ValueAndFormat v = values.get(i);
+ // skip this if the current value matches the next one, or is the last one and matches the previous one
+ if ( (i < values.size()-1 && v.equals(values.get(i+1)) ) || ( i > 0 && i == values.size()-1 && v.equals(values.get(i-1)) ) ) {
+ // current value matches next value, skip both
+ i++;
+ continue;
+ }
+ unique.add(v);
+ }
+
+ return unique;
+ }
+ }).contains(getCellValue(cell));
+ case DUPLICATE_VALUES:
+ // Per Excel help, "duplicate" means matching value AND format
+ // https://support.office.com/en-us/article/Filter-for-unique-values-or-remove-duplicate-values-ccf664b0-81d6-449b-bbe1-8daaec1e83c2
+ return getMeaningfulValues(region, true, new ValueFunction() {
+ public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
+ List<ValueAndFormat> values = allValues;
+ Collections.sort(values);
+
+ final Set<ValueAndFormat> dup = new HashSet<ValueAndFormat>();
+
+ for (int i=0; i < values.size(); i++) {
+ final ValueAndFormat v = values.get(i);
+ // skip this if the current value matches the next one, or is the last one and matches the previous one
+ if ( (i < values.size()-1 && v.equals(values.get(i+1)) ) || ( i > 0 && i == values.size()-1 && v.equals(values.get(i-1)) ) ) {
+ // current value matches next value, add one
+ dup.add(v);
+ i++;
+ }
+ }
+ return dup;
+ }
+ }).contains(getCellValue(cell));
+ case ABOVE_AVERAGE:
+ // from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range.
+ // numbers stored as text are ignored, but numbers formatted as text are treated as numbers.
+
+ final ConditionFilterData conf = rule.getFilterConfiguration();
+
+ // actually ordered, so iteration order is predictable
+ List<ValueAndFormat> values = new ArrayList<ValueAndFormat>(getMeaningfulValues(region, false, new ValueFunction() {
+ public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
+ List<ValueAndFormat> values = allValues;
+ double total = 0;
+ ValueEval[] pop = new ValueEval[values.size()];
+ for (int i=0; i < values.size(); i++) {
+ ValueAndFormat v = values.get(i);
+ total += v.value.doubleValue();
+ pop[i] = new NumberEval(v.value.doubleValue());
+ }
+
+ final Set<ValueAndFormat> avgSet = new LinkedHashSet<ValueAndFormat>(1);
+ avgSet.add(new ValueAndFormat(new Double(values.size() == 0 ? 0 : total / values.size()), null));
+
+ final double stdDev = values.size() <= 1 ? 0 : ((NumberEval) AggregateFunction.STDEV.evaluate(pop, 0, 0)).getNumberValue();
+ avgSet.add(new ValueAndFormat(new Double(stdDev), null));
+ return avgSet;
+ }
+ }));
+
+ final ValueAndFormat cv = getCellValue(cell);
+ Double val = cv.isNumber() ? cv.getValue() : null;
+ if (val == null) return false;
+
+ double avg = values.get(0).value.doubleValue();
+ double stdDev = values.get(1).value.doubleValue();
+
+ /*
+ * use StdDev, aboveAverage, equalAverage to find:
+ * comparison value
+ * operator type
+ */
+
+ Double comp = new Double(conf.getStdDev() > 0 ? (avg + (conf.getAboveAverage() ? 1 : -1) * stdDev * conf.getStdDev()) : avg) ;
+
+ OperatorEnum op = null;
+ if (conf.getAboveAverage()) {
+ if (conf.getEqualAverage()) op = OperatorEnum.GREATER_OR_EQUAL;
+ else op = OperatorEnum.GREATER_THAN;
+ } else {
+ if (conf.getEqualAverage()) op = OperatorEnum.LESS_OR_EQUAL;
+ else op = OperatorEnum.LESS_THAN;
+ }
+ return op != null && op.isValid(val, comp, null);
+ case CONTAINS_TEXT:
+ // implemented both by a cfRule "text" attribute and a formula. Use the formula.
+ return checkFormula(cell, region);
+ case NOT_CONTAINS_TEXT:
+ // implemented both by a cfRule "text" attribute and a formula. Use the formula.
+ return checkFormula(cell, region);
+ case BEGINS_WITH:
+ // implemented both by a cfRule "text" attribute and a formula. Use the formula.
+ return checkFormula(cell, region);
+ case ENDS_WITH:
+ // implemented both by a cfRule "text" attribute and a formula. Use the formula.
+ return checkFormula(cell, region);
+ case CONTAINS_BLANKS:
+ try {
+ String v = cell.getStringCellValue();
+ // see TextFunction.TRIM for implementation
+ return v == null || v.trim().length() == 0;
+ } catch (Exception e) {
+ // not a valid string value, and not a blank cell (that's checked earlier)
+ return false;
+ }
+ case NOT_CONTAINS_BLANKS:
+ try {
+ String v = cell.getStringCellValue();
+ // see TextFunction.TRIM for implementation
+ return v != null && v.trim().length() > 0;
+ } catch (Exception e) {
+ // not a valid string value, but not blank
+ return true;
+ }
+ case CONTAINS_ERRORS:
+ return cell != null && DataValidationEvaluator.isType(cell, CellType.ERROR);
+ case NOT_CONTAINS_ERRORS:
+ return cell == null || ! DataValidationEvaluator.isType(cell, CellType.ERROR);
+ case TIME_PERIOD:
+ // implemented both by a cfRule "text" attribute and a formula. Use the formula.
+ return checkFormula(cell, region);
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range.
+ * numbers stored as text are ignored, but numbers formatted as text are treated as numbers.
+ *
+ * @param region
+ * @return
+ */
+ private Set<ValueAndFormat> getMeaningfulValues(CellRangeAddress region, boolean withText, ValueFunction func) {
+ Set<ValueAndFormat> values = meaningfulRegionValues.get(region);
+ if (values != null) return values;
+
+ List<ValueAndFormat> allValues = new ArrayList<ValueAndFormat>((region.getLastColumn() - region.getFirstColumn()+1) * (region.getLastRow() - region.getFirstRow() + 1));
+
+ for (int r=region.getFirstRow(); r <= region.getLastRow(); r++) {
+ final Row row = sheet.getRow(r);
+ if (row == null) continue;
+ for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) {
+ Cell cell = row.getCell(c);
+ final ValueAndFormat cv = getCellValue(cell);
+ if (cv != null && (withText || cv.isNumber()) ) allValues.add(cv);
+ }
+ }
+
+ values = func.evaluate(allValues);
+ meaningfulRegionValues.put(region, values);
+
+ return values;
+ }
+
+ private ValueAndFormat getCellValue(Cell cell) {
+ if (cell != null) {
+ final CellType type = cell.getCellTypeEnum();
+ if (type == CellType.NUMERIC || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.NUMERIC) ) {
+ return new ValueAndFormat(new Double(cell.getNumericCellValue()), cell.getCellStyle().getDataFormatString());
+ } else if (type == CellType.STRING || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.STRING) ) {
+ return new ValueAndFormat(cell.getStringCellValue(), cell.getCellStyle().getDataFormatString());
+ } else if (type == CellType.BOOLEAN || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.BOOLEAN) ) {
+ return new ValueAndFormat(cell.getStringCellValue(), cell.getCellStyle().getDataFormatString());
+ }
+ }
+ return null;
+ }
+ /**
+ * instances evaluate the values for a region and return the positive matches for the function type.
+ * TODO: when we get to use Java 8, this is obviously a Lambda Function.
+ */
+ protected interface ValueFunction {
+
+ /**
+ *
+ * @param values
+ * @return the desired values for the rules implemented by the current instance
+ */
+ Set<ValueAndFormat> evaluate(List<ValueAndFormat> values);
+ }
+
+ /**
+ * Not calling it OperatorType to avoid confusion for now with other classes.
+ * Definition order matches OOXML type ID indexes.
+ * Note that this has NO_COMPARISON as the first item, unlike the similar
+ * DataValidation operator enum. Thanks, Microsoft.
+ */
+ public static enum OperatorEnum {
+ NO_COMPARISON {
+ /** always false/invalid */
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ return false;
+ }
+ },
+ BETWEEN {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ return cellValue.compareTo(v1) >= 0 && cellValue.compareTo(v2) <= 0;
+ }
+ },
+ NOT_BETWEEN {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ return cellValue.compareTo(v1) < 0 || cellValue.compareTo(v2) > 0;
+ }
+ },
+ EQUAL {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ if (cellValue instanceof String) {
+ return ((String) cellValue).compareToIgnoreCase((String) v1) == 0;
+ }
+ return cellValue.compareTo(v1) == 0;
+ }
+ },
+ NOT_EQUAL {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ if (cellValue instanceof String) {
+ return ((String) cellValue).compareToIgnoreCase((String) v1) != 0;
+ }
+ return cellValue.compareTo(v1) != 0;
+ }
+ },
+ GREATER_THAN {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ return cellValue.compareTo(v1) > 0;
+ }
+ },
+ LESS_THAN {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ return cellValue.compareTo(v1) < 0;
+ }
+ },
+ GREATER_OR_EQUAL {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ return cellValue.compareTo(v1) >= 0;
+ }
+ },
+ LESS_OR_EQUAL {
+ public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
+ return cellValue.compareTo(v1) <= 0;
+ }
+ },
+ ;
+
+ /**
+ * Evaluates comparison using operator instance rules
+ * @param cellValue won't be null, assumption is previous checks handled that
+ * @param v1 if null, value assumed invalid, anything passes, per Excel behavior
+ * @param v2 null if not needed. If null when needed, assume anything passes, per Excel behavior
+ * @return true if the comparison is valid
+ */
+ public abstract <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2);
+ }
+
+ /**
+ * Note: this class has a natural ordering that is inconsistent with equals.
+ */
+ protected class ValueAndFormat implements Comparable<ValueAndFormat> {
+
+ private final Double value;
+ private final String string;
+ private final String format;
+
+ public ValueAndFormat(Double value, String format) {
+ this.value = value;
+ this.format = format;
+ string = null;
+ }
+
+ public ValueAndFormat(String value, String format) {
+ this.value = null;
+ this.format = format;
+ string = value;
+ }
+
+ public boolean isNumber() {
+ return value != null;
+ }
+
+ public Double getValue() {
+ return value;
+ }
+
+ public boolean equals(Object obj) {
+ ValueAndFormat o = (ValueAndFormat) obj;
+ return ( value == o.value || value.equals(o.value))
+ && ( format == o.format || format.equals(o.format))
+ && (string == o.string || string.equals(o.string));
+ }
+
+ /**
+ * Note: this class has a natural ordering that is inconsistent with equals.
+ * @param o
+ * @return value comparison
+ */
+ public int compareTo(ValueAndFormat o) {
+ if (value == null && o.value != null) return 1;
+ if (o.value == null && value != null) return -1;
+ int cmp = value == null ? 0 : value.compareTo(o.value);
+ if (cmp != 0) return cmp;
+
+ if (string == null && o.string != null) return 1;
+ if (o.string == null && string != null) return -1;
+
+ return string == null ? 0 : string.compareTo(o.string);
+ }
+
+ public int hashCode() {
+ return (string == null ? 0 : string.hashCode()) * 37 * 37 + 37 * (value == null ? 0 : value.hashCode()) + (format == null ? 0 : format.hashCode());
+ }
+ }
+}
package org.apache.poi.ss.formula;
+import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.formula.ptg.NamePtg;
import org.apache.poi.ss.formula.ptg.NameXPtg;
import org.apache.poi.ss.formula.ptg.Ptg;
String resolveNameXText(NameXPtg ptg);
Ptg[] getFormulaTokens(EvaluationCell cell);
UDFFinder getUDFFinder();
+ SpreadsheetVersion getSpreadsheetVersion();
/**
* Propagated from {@link WorkbookEvaluator#clearAllCachedResultValues()} to clear locally cached data.
actualEndRow = _rowIndex;
} else { // Really no special quantifiers
actualStartRow++;
+ if (tbl.isHasTotalsRow()) actualEndRow--;
}
}
import java.util.Stack;
import java.util.TreeSet;
+import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment.WorkbookNotFoundException;
import org.apache.poi.ss.formula.atp.AnalysisToolPak;
import org.apache.poi.ss.formula.eval.BlankEval;
import org.apache.poi.ss.formula.functions.FreeRefFunction;
import org.apache.poi.ss.formula.functions.Function;
import org.apache.poi.ss.formula.functions.IfFunc;
-import org.apache.poi.ss.formula.ptg.Area3DPtg;
-import org.apache.poi.ss.formula.ptg.Area3DPxg;
-import org.apache.poi.ss.formula.ptg.AreaErrPtg;
-import org.apache.poi.ss.formula.ptg.AreaPtg;
-import org.apache.poi.ss.formula.ptg.AttrPtg;
-import org.apache.poi.ss.formula.ptg.BoolPtg;
-import org.apache.poi.ss.formula.ptg.ControlPtg;
-import org.apache.poi.ss.formula.ptg.DeletedArea3DPtg;
-import org.apache.poi.ss.formula.ptg.DeletedRef3DPtg;
-import org.apache.poi.ss.formula.ptg.ErrPtg;
-import org.apache.poi.ss.formula.ptg.ExpPtg;
-import org.apache.poi.ss.formula.ptg.FuncVarPtg;
-import org.apache.poi.ss.formula.ptg.IntPtg;
-import org.apache.poi.ss.formula.ptg.MemAreaPtg;
-import org.apache.poi.ss.formula.ptg.MemErrPtg;
-import org.apache.poi.ss.formula.ptg.MemFuncPtg;
-import org.apache.poi.ss.formula.ptg.MissingArgPtg;
-import org.apache.poi.ss.formula.ptg.NamePtg;
-import org.apache.poi.ss.formula.ptg.NameXPtg;
-import org.apache.poi.ss.formula.ptg.NameXPxg;
-import org.apache.poi.ss.formula.ptg.NumberPtg;
-import org.apache.poi.ss.formula.ptg.OperationPtg;
-import org.apache.poi.ss.formula.ptg.Ptg;
-import org.apache.poi.ss.formula.ptg.Ref3DPtg;
-import org.apache.poi.ss.formula.ptg.Ref3DPxg;
-import org.apache.poi.ss.formula.ptg.RefErrorPtg;
-import org.apache.poi.ss.formula.ptg.RefPtg;
-import org.apache.poi.ss.formula.ptg.StringPtg;
-import org.apache.poi.ss.formula.ptg.UnionPtg;
-import org.apache.poi.ss.formula.ptg.UnknownPtg;
+import org.apache.poi.ss.formula.ptg.*;
import org.apache.poi.ss.formula.udf.AggregatingUDFFinder;
import org.apache.poi.ss.formula.udf.UDFFinder;
import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.util.CellRangeAddressBase;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.util.Internal;
import org.apache.poi.util.POILogFactory;
return _udfFinder.findFunction(functionName);
}
+ /**
+ * Evaluate a formula outside a cell value, e.g. conditional format rules or data validation expressions
+ *
+ * @param formula to evaluate
+ * @param ref defines the sheet and optionally row/column base for the formula, if it is relative
+ * @param formulaType used in some contexts to define branches of logic
+ * @return value
+ * @throws IllegalArgumentException if ref does not define a sheet name to evaluate the formula on.
+ */
+ public ValueEval evaluate(String formula, CellReference ref) {
+ final String sheetName = ref == null ? null : ref.getSheetName();
+ if (sheetName == null) throw new IllegalArgumentException("Sheet name is required");
+ final int sheetIndex = getWorkbook().getSheetIndex(sheetName);
+ final OperationEvaluationContext ec = new OperationEvaluationContext(this, getWorkbook(), sheetIndex, ref.getRow(), ref.getCol(), new EvaluationTracker(_cache));
+ Ptg[] ptgs = FormulaParser.parse(formula, (FormulaParsingWorkbook) getWorkbook(), FormulaType.CELL, sheetIndex, ref.getRow());
+ return evaluateNameFormula(ptgs, ec);
+ }
+
+ /**
+ * Some expressions need to be evaluated in terms of an offset from the top left corner of a region,
+ * such as some data validation and conditional format expressions, when those constraints apply
+ * to contiguous cells. When a relative formula is used, it must be evaluated by shifting by the target
+ * offset position relative to the top left of the range.
+ *
+ * @param formula
+ * @param target cell context for the operation
+ * @param region containing the cell
+ * @return value
+ * @throws IllegalArgumentException if target does not define a sheet name to evaluate the formula on.
+ */
+ public ValueEval evaluate(String formula, CellReference target, CellRangeAddressBase region) {
+ final String sheetName = target == null ? null : target.getSheetName();
+ if (sheetName == null) throw new IllegalArgumentException("Sheet name is required");
+
+ final int sheetIndex = getWorkbook().getSheetIndex(sheetName);
+ Ptg[] ptgs = FormulaParser.parse(formula, (FormulaParsingWorkbook) getWorkbook(), FormulaType.CELL, sheetIndex, target.getRow());
+
+ adjustRegionRelativeReference(ptgs, target, region);
+
+ final OperationEvaluationContext ec = new OperationEvaluationContext(this, getWorkbook(), sheetIndex, target.getRow(), target.getCol(), new EvaluationTracker(_cache));
+ return evaluateNameFormula(ptgs, ec);
+ }
+
+ /**
+ * Adjust formula relative references by the offset between the start of the given region and the given target cell.
+ * @param ptgs
+ * @param target cell within the region to use.
+ * @param region containing the cell
+ * @return true if any Ptg references were shifted
+ * @throws IndexOutOfBoundsException if the resulting shifted row/column indexes are over the document format limits
+ * @throws IllegalArgumentException if target is not within region.
+ */
+ protected boolean adjustRegionRelativeReference(Ptg[] ptgs, CellReference target, CellRangeAddressBase region) {
+ if (! region.isInRange(target)) {
+ throw new IllegalArgumentException(target + " is not within " + region);
+ }
+
+ return adjustRegionRelativeReference(ptgs, target.getRow() - region.getFirstRow(), target.getCol() - region.getFirstColumn());
+ }
+
+ /**
+ * Adjust the formula relative cell references by a given delta
+ * @param ptgs
+ * @param deltaRow target row offset from the top left cell of a region
+ * @param deltaColumn target column offset from the top left cell of a region
+ * @return true if any Ptg references were shifted
+ * @throws IndexOutOfBoundsException if the resulting shifted row/column indexes are over the document format limits
+ * @throws IllegalArgumentException if either of the deltas are negative, as the assumption is we are shifting formulas
+ * relative to the top left cell of a region.
+ */
+ protected boolean adjustRegionRelativeReference(Ptg[] ptgs, int deltaRow, int deltaColumn) {
+ if (deltaRow < 0) throw new IllegalArgumentException("offset row must be positive");
+ if (deltaColumn < 0) throw new IllegalArgumentException("offset column must be positive");
+ boolean shifted = false;
+ for (Ptg ptg : ptgs) {
+ // base class for cell reference "things"
+ if (ptg instanceof RefPtgBase) {
+ RefPtgBase ref = (RefPtgBase) ptg;
+ // re-calculate cell references
+ final SpreadsheetVersion version = _workbook.getSpreadsheetVersion();
+ if (ref.isRowRelative()) {
+ final int rowIndex = ref.getRow() + deltaRow;
+ if (rowIndex > version.getMaxRows()) {
+ throw new IndexOutOfBoundsException(version.name() + " files can only have " + version.getMaxRows() + " rows, but row " + rowIndex + " was requested.");
+ }
+ ref.setRow(rowIndex);
+ shifted = true;
+ }
+ if (ref.isColRelative()) {
+ final int colIndex = ref.getColumn() + deltaColumn;
+ if (colIndex > version.getMaxColumns()) {
+ throw new IndexOutOfBoundsException(version.name() + " files can only have " + version.getMaxColumns() + " columns, but column " + colIndex + " was requested.");
+ }
+ ref.setColumn(colIndex);
+ shifted = true;
+ }
+ }
+ }
+ return shifted;
+ }
+
/**
* Whether to ignore missing references to external workbooks and
* use cached formula results in the main workbook instead.
import java.util.HashMap;
import java.util.Map;
+import org.apache.poi.ss.SpreadsheetVersion;
import org.apache.poi.ss.formula.EvaluationCell;
import org.apache.poi.ss.formula.EvaluationName;
import org.apache.poi.ss.formula.EvaluationSheet;
return _masterBook.getUDFFinder();
}
+ public SpreadsheetVersion getSpreadsheetVersion() {
+ return _masterBook.getSpreadsheetVersion();
+ }
+
/* (non-Javadoc)
* leave the map alone, if it needs resetting, reusing this class is probably a bad idea.
* @see org.apache.poi.ss.formula.EvaluationSheet#clearAllCachedResultValues()
--- /dev/null
+/*
+ * ====================================================================
+ * 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.usermodel;
+
+/**
+ * These values are needed by various conditional formatting evaluation filter types
+ */
+public interface ConditionFilterData {
+
+ /**
+ * @return true if the flag is missing or set to true
+ */
+ boolean getAboveAverage();
+
+ /**
+ * @return true if the flag is set
+ */
+ boolean getBottom();
+
+ /**
+ * @return true if the flag is set
+ */
+ boolean getEqualAverage();
+
+ /**
+ * @return true if the flag is set
+ */
+ boolean getPercent();
+
+ /**
+ * @return value, or 0 if not used/defined
+ */
+ long getRank();
+
+ /**
+ * @return value, or 0 if not used/defined
+ */
+ int getStdDev();
+
+}
--- /dev/null
+/*
+ * ====================================================================
+ * 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.usermodel;
+
+/**
+ * Used primarily for XSSF conditions, which defines a multitude of additional "filter" types
+ * for conditional formatting. HSSF rules will always be null (not a filter type) or #FILTER.
+ * XSSF conditions will be null (not a filter type) or any value other than #FILTER.
+ * <p/>
+ * Instance names match the constants from <code>STCfType</code> for convenience.
+ */
+public enum ConditionFilterType {
+ /** This is the only value valid for HSSF rules */
+ FILTER,
+ TOP_10,
+ UNIQUE_VALUES,
+ DUPLICATE_VALUES,
+ CONTAINS_TEXT,
+ NOT_CONTAINS_TEXT,
+ BEGINS_WITH,
+ ENDS_WITH,
+ CONTAINS_BLANKS,
+ NOT_CONTAINS_BLANKS,
+ CONTAINS_ERRORS,
+ NOT_CONTAINS_ERRORS,
+ TIME_PERIOD,
+ ABOVE_AVERAGE,
+ ;
+}
* @return the type of condition
*/
ConditionType getConditionType();
+
+ /**
+ * This is null if
+ * <p/>
+ * <code>{@link #getConditionType()} != {@link ConditionType#FILTER}</code>
+ * <p/>
+ * This is always {@link ConditionFilterType#FILTER} for HSSF rules of type {@link ConditionType#FILTER}.
+ * <p/>
+ * For XSSF filter rules, this will indicate the specific type of filter.
+ *
+ * @return filter type for filter rules, or null if not a filter rule.
+ */
+ ConditionFilterType getConditionFilterType();
+
+ /**
+ * This is null if
+ * <p/>
+ * <code>{@link #getConditionFilterType()} == null</code>
+ * <p/>
+ * This means it is always null for HSSF, which does not define the extended condition types.
+ * <p/>
+ * This object contains the additional configuration information for XSSF filter conditions.
+ *
+ * @return
+ */
+ public ConditionFilterData getFilterConfiguration();
/**
* The comparison function used when the type of conditional formatting is set to
* @return the second formula
*/
String getFormula2();
+
+ /**
+ * HSSF just returns 0, XSSF uses the value stored in the model if present,
+ * otherwise uses 0.
+ * <p/>
+ * If priority is 0, just use definition order, as that's how HSSF rules are evaluated.
+ * <p/>
+ * If a rule is created but not yet added to a sheet, this value may not be valid.
+
+ * @return rule priority
+ */
+ int getPriority();
+
+ /**
+ * Always true for HSSF rules, optional flag for XSSF rules.
+ * See Excel help for more.
+ *
+ * @return true if conditional formatting rule processing stops when this one is true, false if not
+ * @see <a href="https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-063cde21-516e-45ca-83f5-8e8126076249">Microsoft Excel help</a>
+ */
+ boolean getStopIfTrue();
}
--- /dev/null
+/*\r
+ * ====================================================================\r
+ * Licensed to the Apache Software Foundation (ASF) under one or more\r
+ * contributor license agreements. See the NOTICE file distributed with\r
+ * this work for additional information regarding copyright ownership.\r
+ * The ASF licenses this file to You under the Apache License, Version 2.0\r
+ * (the "License"); you may not use this file except in compliance with\r
+ * the License. You may obtain a copy of the License at\r
+ *\r
+ * http://www.apache.org/licenses/LICENSE-2.0\r
+ *\r
+ * Unless required by applicable law or agreed to in writing, software\r
+ * distributed under the License is distributed on an "AS IS" BASIS,\r
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r
+ * See the License for the specific language governing permissions and\r
+ * limitations under the License.\r
+ * ====================================================================\r
+ */\r
+\r
+package org.apache.poi.ss.usermodel;\r
+\r
+/**\r
+ * Represents a description of a conditional formatting rule\r
+ */\r
+public interface ConditionalFormattingRule {\r
+ /**\r
+ * Create a new border formatting structure if it does not exist,\r
+ * otherwise just return existing object.\r
+ *\r
+ * @return - border formatting object, never returns <code>null</code>.\r
+ */\r
+ BorderFormatting createBorderFormatting();\r
+\r
+ /**\r
+ * @return - border formatting object if defined, <code>null</code> otherwise\r
+ */\r
+ BorderFormatting getBorderFormatting();\r
+\r
+ /**\r
+ * Create a new font formatting structure if it does not exist,\r
+ * otherwise just return existing object.\r
+ *\r
+ * @return - font formatting object, never returns <code>null</code>.\r
+ */\r
+ FontFormatting createFontFormatting();\r
+\r
+ /**\r
+ * @return - font formatting object if defined, <code>null</code> otherwise\r
+ */\r
+ FontFormatting getFontFormatting();\r
+\r
+ /**\r
+ * Create a new pattern formatting structure if it does not exist,\r
+ * otherwise just return existing object.\r
+ *\r
+ * @return - pattern formatting object, never returns <code>null</code>.\r
+ */\r
+ PatternFormatting createPatternFormatting();\r
+\r
+ /**\r
+ * @return - pattern formatting object if defined, <code>null</code> otherwise\r
+ */\r
+ PatternFormatting getPatternFormatting();\r
+\r
+ /**\r
+ * @return - databar / data-bar formatting object if defined, <code>null</code> otherwise\r
+ */\r
+ DataBarFormatting getDataBarFormatting();\r
+ \r
+ /**\r
+ * @return - icon / multi-state formatting object if defined, <code>null</code> otherwise\r
+ */\r
+ IconMultiStateFormatting getMultiStateFormatting();\r
+ \r
+ /**\r
+ * @return color scale / color grate formatting object if defined, <code>null</code> otherwise\r
+ */\r
+ ColorScaleFormatting getColorScaleFormatting();\r
+ \r
+ /**\r
+ * Type of conditional formatting rule.\r
+ *\r
+ * @return the type of condition\r
+ */\r
+ ConditionType getConditionType();\r
+ \r
+ /**\r
+ * This is null if \r
+ * <p/>\r
+ * <code>{@link #getConditionType()} != {@link ConditionType#FILTER}</code>\r
+ * <p/>\r
+ * This is always {@link ConditionFilterType#FILTER} for HSSF rules of type {@link ConditionType#FILTER}.\r
+ * <p/>\r
+ * For XSSF filter rules, this will indicate the specific type of filter.\r
+ * \r
+ * @return filter type for filter rules, or null if not a filter rule.\r
+ */\r
+ ConditionFilterType getConditionFilterType();\r
+ \r
+ /**\r
+ * This is null if \r
+ * <p/>\r
+ * <code>{@link #getConditionFilterType()} == null</code>\r
+ * <p/>\r
+ * This means it is always null for HSSF, which does not define the extended condition types.\r
+ * <p/>\r
+ * This object contains the additional configuration information for XSSF filter conditions.\r
+ * \r
+ * @return\r
+ */\r
+ public ConditionFilterData getFilterConfiguration();\r
+\r
+ /**\r
+ * The comparison function used when the type of conditional formatting is set to\r
+ * {@link ConditionType#CELL_VALUE_IS}\r
+ * <p>\r
+ * MUST be a constant from {@link ComparisonOperator}\r
+ * </p>\r
+ *\r
+ * @return the conditional format operator\r
+ */\r
+ byte getComparisonOperation();\r
+\r
+ /**\r
+ * The formula used to evaluate the first operand for the conditional formatting rule.\r
+ * <p>\r
+ * If the condition type is {@link ConditionType#CELL_VALUE_IS},\r
+ * this field is the first operand of the comparison.\r
+ * If type is {@link ConditionType#FORMULA}, this formula is used\r
+ * to determine if the conditional formatting is applied.\r
+ * </p>\r
+ * <p>\r
+ * If comparison type is {@link ConditionType#FORMULA} the formula MUST be a Boolean function\r
+ * </p>\r
+ *\r
+ * @return the first formula\r
+ */\r
+ String getFormula1();\r
+\r
+ /**\r
+ * The formula used to evaluate the second operand of the comparison when\r
+ * comparison type is {@link ConditionType#CELL_VALUE_IS} and operator\r
+ * is either {@link ComparisonOperator#BETWEEN} or {@link ComparisonOperator#NOT_BETWEEN}\r
+ *\r
+ * @return the second formula\r
+ */\r
+ String getFormula2();\r
+\r
+ /**\r
+ * HSSF just returns 0, XSSF uses the value stored in the model if present, \r
+ * otherwise uses 0.\r
+ * <p/>\r
+ * If priority is 0, just use definition order, as that's how HSSF rules are evaluated.\r
+ * <p/>\r
+ * If a rule is created but not yet added to a sheet, this value may not be valid.\r
+\r
+ * @return rule priority\r
+ */\r
+ int getPriority();\r
+ \r
+ /**\r
+ * Always true for HSSF rules, optional flag for XSSF rules.\r
+ * See Excel help for more.\r
+ * \r
+ * @return true if conditional formatting rule processing stops when this one is true, false if not\r
+ * @see <a href="https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-063cde21-516e-45ca-83f5-8e8126076249">Microsoft Excel help</a>\r
+ */\r
+ boolean getStopIfTrue();\r
+}\r
package org.apache.poi.ss.util;
import org.apache.poi.ss.SpreadsheetVersion;
+import org.apache.poi.ss.usermodel.Cell;
/**
_firstCol <= colInd && colInd <= _lastCol; //containsColumn
}
+ /**
+ * Determines if the given {@link CellReference} lies within the bounds
+ * of this range.
+ * <p/>NOTE: It is up to the caller to ensure the reference is
+ * for the correct sheet, since this instance doesn't have a sheet reference.
+ *
+ * @param ref the CellReference to check
+ * @return True if the reference lies within the bounds, false otherwise.
+ * @see #intersects(CellRangeAddressBase) for checking if two ranges overlap
+ */
+ public boolean isInRange(CellReference ref) {
+ return isInRange(ref.getRow(), ref.getCol());
+ }
+
+ /**
+ * Determines if the given {@link Cell} lies within the bounds
+ * of this range.
+ * <p/>NOTE: It is up to the caller to ensure the reference is
+ * for the correct sheet, since this instance doesn't have a sheet reference.
+ *
+ * @param cell the Cell to check
+ * @return True if the cell lies within the bounds, false otherwise.
+ * @see #intersects(CellRangeAddressBase) for checking if two ranges overlap
+ */
+ public boolean isInRange(Cell cell) {
+ return isInRange(cell.getRowIndex(), cell.getColumnIndex());
+ }
+
/**
* Check if the row is in the specified cell range
*
return cr.isInRange(rowIx, colIx);
}
+ /**
+ * Return the cell, without taking account of merged regions.
+ * <p/>
+ * Use {@link #getCellWithMerges(Sheet, int, int)} if you want the top left
+ * cell from merged regions instead when the reference is a merged cell.
+ * <p/>
+ * Use this where you want to know if the given cell is explicitly defined
+ * or not.
+ *
+ * @param sheet
+ * @param rowIx
+ * @param colIx
+ * @return cell at the given location, or null if not defined
+ * @throws NullPointerException if sheet is null
+ */
+ public static Cell getCell(Sheet sheet, int rowIx, int colIx) {
+ Row r = sheet.getRow(rowIx);
+ if (r != null) {
+ return r.getCell(colIx);
+ }
+ return null;
+ }
+
/**
* Return the cell, taking account of merged regions. Allows you to find the
* cell who's contents are shown in a given position in the sheet.
* then will return the cell itself.
* <p>If there is no cell defined at the given co-ordinates, will return
* null.
+ *
+ * @param sheet
+ * @param rowIx
+ * @param colIx
+ * @return cell at the given location, its base merged cell, or null if not defined
+ * @throws NullPointerException if sheet is null
*/
public static Cell getCellWithMerges(Sheet sheet, int rowIx, int colIx) {
- Row r = sheet.getRow(rowIx);
- if (r != null) {
- Cell c = r.getCell(colIx);
- if (c != null) {
- // Normal, non-merged cell
- return c;
- }
- }
+ final Cell c = getCell(sheet, rowIx, colIx);
+ if (c != null) return c;
for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) {
if (mergedRegion.isInRange(rowIx, colIx)) {
// The cell wanted is in this merged range
// Return the primary (top-left) cell for the range
- r = sheet.getRow(mergedRegion.getFirstRow());
+ Row r = sheet.getRow(mergedRegion.getFirstRow());
if (r != null) {
return r.getCell(mergedRegion.getFirstColumn());
}
--- /dev/null
+/*
+ * ====================================================================
+ * 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.xssf.usermodel;
+
+import org.apache.poi.ss.usermodel.ConditionFilterData;
+import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCfRule;
+
+public class XSSFConditionFilterData implements ConditionFilterData {
+
+ private final CTCfRule _cfRule;
+
+ /*package*/ XSSFConditionFilterData(CTCfRule cfRule) {
+ _cfRule = cfRule;
+ }
+
+ public boolean getAboveAverage() {
+ return _cfRule.getAboveAverage();
+ }
+
+ public boolean getBottom() {
+ return _cfRule.getBottom();
+ }
+
+ public boolean getEqualAverage() {
+ return _cfRule.getEqualAverage();
+ }
+
+ public boolean getPercent() {
+ return _cfRule.getPercent();
+ }
+
+ public long getRank() {
+ return _cfRule.getRank();
+ }
+
+ public int getStdDev() {
+ return _cfRule.getStdDev();
+ }
+
+}
import org.openxmlformats.schemas.spreadsheetml.x2006.main.*;
/**
- * XSSF suport for Conditional Formatting rules
+ * XSSF support for Conditional Formatting rules
*/
public class XSSFConditionalFormattingRule implements ConditionalFormattingRule {
private final CTCfRule _cfRule;
private XSSFSheet _sh;
private static Map<STCfType.Enum, ConditionType> typeLookup = new HashMap<STCfType.Enum, ConditionType>();
+ private static Map<STCfType.Enum, ConditionFilterType> filterTypeLookup = new HashMap<STCfType.Enum, ConditionFilterType>();
static {
typeLookup.put(STCfType.CELL_IS, ConditionType.CELL_VALUE_IS);
typeLookup.put(STCfType.EXPRESSION, ConditionType.FORMULA);
typeLookup.put(STCfType.NOT_CONTAINS_ERRORS, ConditionType.FILTER);
typeLookup.put(STCfType.TIME_PERIOD, ConditionType.FILTER);
typeLookup.put(STCfType.ABOVE_AVERAGE, ConditionType.FILTER);
+
+ filterTypeLookup.put(STCfType.TOP_10, ConditionFilterType.TOP_10);
+ filterTypeLookup.put(STCfType.UNIQUE_VALUES, ConditionFilterType.UNIQUE_VALUES);
+ filterTypeLookup.put(STCfType.DUPLICATE_VALUES, ConditionFilterType.DUPLICATE_VALUES);
+ filterTypeLookup.put(STCfType.CONTAINS_TEXT, ConditionFilterType.CONTAINS_TEXT);
+ filterTypeLookup.put(STCfType.NOT_CONTAINS_TEXT, ConditionFilterType.NOT_CONTAINS_TEXT);
+ filterTypeLookup.put(STCfType.BEGINS_WITH, ConditionFilterType.BEGINS_WITH);
+ filterTypeLookup.put(STCfType.ENDS_WITH, ConditionFilterType.ENDS_WITH);
+ filterTypeLookup.put(STCfType.CONTAINS_BLANKS, ConditionFilterType.CONTAINS_BLANKS);
+ filterTypeLookup.put(STCfType.NOT_CONTAINS_BLANKS, ConditionFilterType.NOT_CONTAINS_BLANKS);
+ filterTypeLookup.put(STCfType.CONTAINS_ERRORS, ConditionFilterType.CONTAINS_ERRORS);
+ filterTypeLookup.put(STCfType.NOT_CONTAINS_ERRORS, ConditionFilterType.NOT_CONTAINS_ERRORS);
+ filterTypeLookup.put(STCfType.TIME_PERIOD, ConditionFilterType.TIME_PERIOD);
+ filterTypeLookup.put(STCfType.ABOVE_AVERAGE, ConditionFilterType.ABOVE_AVERAGE);
+
}
+ /**
+ * NOTE: does not set priority, so this assumes the rule will not be added to the sheet yet
+ * @param sh
+ */
/*package*/ XSSFConditionalFormattingRule(XSSFSheet sh){
_cfRule = CTCfRule.Factory.newInstance();
_sh = sh;
return dxf;
}
+ public int getPriority() {
+ final int priority = _cfRule.getPriority();
+ // priorities start at 1, if it is less, it is undefined, use definition order in caller
+ return priority >=1 ? priority : 0;
+ }
+
+ public boolean getStopIfTrue() {
+ return _cfRule.getStopIfTrue();
+ }
+
/**
* Create a new border formatting structure if it does not exist,
* otherwise just return existing object.
return typeLookup.get(_cfRule.getType());
}
+ /**
+ * Will return null if {@link #getConditionType()} != {@link ConditionType#FILTER}
+ * @see org.apache.poi.ss.usermodel.ConditionalFormattingRule#getConditionFilterType()
+ */
+ public ConditionFilterType getConditionFilterType() {
+ return filterTypeLookup.get(_cfRule.getType());
+ }
+
+ public ConditionFilterData getFilterConfiguration() {
+ return new XSSFConditionFilterData(_cfRule);
+ }
+
/**
* The comparison function used when the type of conditional formatting is set to
* {@link ConditionType#CELL_VALUE_IS}
throw new IllegalArgumentException("Specified cell does not belong to this sheet.");
}
for (CellRangeAddress range : arrayFormulas) {
- if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) {
+ if (range.isInRange(cell)) {
arrayFormulas.remove(range);
CellRange<XSSFCell> cr = getCellRange(range);
for (XSSFCell c : cr) {
--- /dev/null
+package org.apache.poi.ss.usermodel;
+
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.poi.ss.formula.ConditionalFormattingEvaluator;
+import org.apache.poi.ss.formula.EvaluationConditionalFormatRule;
+import org.apache.poi.ss.util.CellReference;
+import org.apache.poi.xssf.XSSFTestDataSamples;
+import org.apache.poi.xssf.usermodel.XSSFColor;
+import org.apache.poi.xssf.usermodel.XSSFFormulaEvaluator;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+public class ConditionalFormattingEvalTest {
+
+ private XSSFWorkbook wb;
+ private Sheet sheet;
+ private XSSFFormulaEvaluator formulaEval;
+ private ConditionalFormattingEvaluator cfe;
+ private CellReference ref;
+ private List<EvaluationConditionalFormatRule> rules;
+
+ @Before
+ public void openWB() {
+ wb = XSSFTestDataSamples.openSampleWorkbook("ConditionalFormattingSamples.xlsx");
+ formulaEval = new XSSFFormulaEvaluator(wb);
+ cfe = new ConditionalFormattingEvaluator(wb, formulaEval);
+ }
+
+ @After
+ public void closeWB() {
+ formulaEval = null;
+ cfe = null;
+ ref = null;
+ rules = null;
+ try {
+ if (wb != null) wb.close();
+ } catch (IOException e) {
+ // keep going, this shouldn't cancel things
+ e.printStackTrace();
+ }
+ }
+
+ @Test
+ public void testFormattingEvaluation() {
+ sheet = wb.getSheet("Products1");
+
+ getRulesFor(12, 1);
+ assertEquals("wrong # of rules for " + ref, 1, rules.size());
+ assertEquals("wrong bg color for " + ref, "FFFFEB9C", getColor(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()));
+ assertFalse("should not be italic " + ref, rules.get(0).getRule().getFontFormatting().isItalic());
+
+ getRulesFor(16, 3);
+ assertEquals("wrong # of rules for " + ref, 1, rules.size());
+ assertEquals("wrong bg color for " + ref, 0.7999816888943144d, getTint(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()), 0.000000000000001);
+
+ getRulesFor(12, 3);
+ assertEquals("wrong # of rules for " + ref, 0, rules.size());
+
+ sheet = wb.getSheet("Products2");
+
+ getRulesFor(15,1);
+ assertEquals("wrong # of rules for " + ref, 1, rules.size());
+ assertEquals("wrong bg color for " + ref, "FFFFEB9C", getColor(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()));
+
+ getRulesFor(20,3);
+ assertEquals("wrong # of rules for " + ref, 0, rules.size());
+
+ // now change a cell value that's an input for the rules
+ Cell cell = sheet.getRow(1).getCell(6);
+ cell.setCellValue("Dairy");
+ formulaEval.notifyUpdateCell(cell);
+ cell = sheet.getRow(4).getCell(6);
+ cell.setCellValue(500);
+ formulaEval.notifyUpdateCell(cell);
+ // need to throw away all evaluations, since we don't know how value changes may have affected format formulas
+ cfe.clearAllCachedValues();
+
+ // test that the conditional validation evaluations changed
+ getRulesFor(15,1);
+ assertEquals("wrong # of rules for " + ref, 0, rules.size());
+
+ getRulesFor(20,3);
+ assertEquals("wrong # of rules for " + ref, 1, rules.size());
+ assertEquals("wrong bg color for " + ref, 0.7999816888943144d, getTint(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()), 0.000000000000001);
+
+ getRulesFor(20,1);
+ assertEquals("wrong # of rules for " + ref, 1, rules.size());
+ assertEquals("wrong bg color for " + ref, "FFFFEB9C", getColor(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()));
+
+ sheet = wb.getSheet("Book tour");
+
+ getRulesFor(8,2);
+ assertEquals("wrong # of rules for " + ref, 1, rules.size());
+
+ }
+
+ private List<EvaluationConditionalFormatRule> getRulesFor(int row, int col) {
+ ref = new CellReference(sheet.getSheetName(), row, col, false, false);
+ return rules = cfe.getConditionalFormattingForCell(ref);
+ }
+
+ private String getColor(Color color) {
+ final XSSFColor c = XSSFColor.toXSSFColor(color);
+ return c.getARGBHex();
+ }
+
+ private double getTint(Color color) {
+ final XSSFColor c = XSSFColor.toXSSFColor(color);
+ return c.getTint();
+ }
+}