diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java b/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java index b52ea9102..83a5b2569 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java @@ -29,6 +29,8 @@ import org.apache.poi.hssf.record.cf.FontFormatting; 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; @@ -57,6 +59,22 @@ public final class HSSFConditionalFormattingRule implements ConditionalFormattin 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; } @@ -236,6 +254,18 @@ public final class HSSFConditionalFormattingRule implements ConditionalFormattin 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 */ diff --git a/src/java/org/apache/poi/ss/formula/ConditionalFormattingEvaluator.java b/src/java/org/apache/poi/ss/formula/ConditionalFormattingEvaluator.java new file mode 100644 index 000000000..81af9b084 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/ConditionalFormattingEvaluator.java @@ -0,0 +1,282 @@ +/* ==================================================================== + 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.

+ * + * 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. + *

+ * + */ +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. + *

+ * Instead we need a cached version that is discarded when definitions change. + *

+ * 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> formats = new HashMap>(); + + /** + * Evaluating rules for cells in their region(s) is expensive, so we want to cache them, + * and empty/reevaluate the cache when values change. + *

+ * Rule lists are in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF) + *

+ * CellReference implements equals(). + */ + private final Map> values = new HashMap>(); + + 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. + *

+ * 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 getRules(Sheet sheet) { + final String sheetName = sheet.getSheetName(); + List rules = formats.get(sheetName); + if (rules == null && ! formats.containsKey(sheetName)) { + final SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting(); + final int count = scf.getNumConditionalFormattings(); + rules = new ArrayList(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 CTCfRule in XSSFConditionalFormattingRule. + *

+ * 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. + *

+ * 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 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 CTCfRule in XSSFConditionalFormattingRule. + *

+ * 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. + *

+ * 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 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 getConditionalFormattingForCell(Cell cell, CellReference ref) { + List rules = values.get(ref); + + if (rules == null) { + // compute and cache them + rules = new ArrayList(); + /* + * 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 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 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. + *

+ * We could overload this with convenience methods taking a sheet name and sheet index as well. + *

+ * @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 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 getMatchingCells(EvaluationConditionalFormatRule rule) { + final List cells = new ArrayList(); + 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 cellRules = getConditionalFormattingForCell(cell); + if (cellRules.contains(rule)) cells.add(cell); + } + } + } + return Collections.unmodifiableList(cells); + } +} diff --git a/src/java/org/apache/poi/ss/formula/DataValidationEvaluator.java b/src/java/org/apache/poi/ss/formula/DataValidationEvaluator.java new file mode 100644 index 000000000..b69334e44 --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/DataValidationEvaluator.java @@ -0,0 +1,563 @@ +/* ==================================================================== + 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.

+ * + * 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. + *

+ * 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. + *

+ * 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> validations = new HashMap>(); + + 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 getValidations(Sheet sheet) { + List 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 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. + *

+ * For all other validation types, or no validation at all, this method + * returns null. + *

+ * 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 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 getValidationValuesForConstraint(DataValidationContext context) { + final DataValidationConstraint val = context.getValidation().getValidationConstraint(); + if (val.getValidationType() != ValidationType.LIST) return null; + + String formula = val.getFormula1(); + + final List values = new ArrayList(); + + 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. + *

+ * 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 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(); + } + + } +} diff --git a/src/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java b/src/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java new file mode 100644 index 000000000..2ae9a965f --- /dev/null +++ b/src/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java @@ -0,0 +1,700 @@ +/* ==================================================================== + 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. + *

+ * 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. + *

+ * 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 { + + 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> meaningfulRegionValues = new HashMap>(); + + 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. + *

+ * 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 evaluate(List allValues) { + List 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(allValues); + + return new HashSet(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 evaluate(List allValues) { + List values = allValues; + Collections.sort(values); + + final Set unique = new HashSet(); + + 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 evaluate(List allValues) { + List values = allValues; + Collections.sort(values); + + final Set dup = new HashSet(); + + 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 values = new ArrayList(getMeaningfulValues(region, false, new ValueFunction() { + public Set evaluate(List allValues) { + List 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 avgSet = new LinkedHashSet(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 getMeaningfulValues(CellRangeAddress region, boolean withText, ValueFunction func) { + Set values = meaningfulRegionValues.get(region); + if (values != null) return values; + + List allValues = new ArrayList((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 evaluate(List 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 > boolean isValid(C cellValue, C v1, C v2) { + return false; + } + }, + BETWEEN { + public > boolean isValid(C cellValue, C v1, C v2) { + return cellValue.compareTo(v1) >= 0 && cellValue.compareTo(v2) <= 0; + } + }, + NOT_BETWEEN { + public > boolean isValid(C cellValue, C v1, C v2) { + return cellValue.compareTo(v1) < 0 || cellValue.compareTo(v2) > 0; + } + }, + EQUAL { + public > 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 > 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 > boolean isValid(C cellValue, C v1, C v2) { + return cellValue.compareTo(v1) > 0; + } + }, + LESS_THAN { + public > boolean isValid(C cellValue, C v1, C v2) { + return cellValue.compareTo(v1) < 0; + } + }, + GREATER_OR_EQUAL { + public > boolean isValid(C cellValue, C v1, C v2) { + return cellValue.compareTo(v1) >= 0; + } + }, + LESS_OR_EQUAL { + public > 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 > 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 { + + 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()); + } + } +} diff --git a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java index 6c4c17eb3..2110e5742 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationWorkbook.java @@ -17,6 +17,7 @@ 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; @@ -75,6 +76,7 @@ public interface EvaluationWorkbook { String resolveNameXText(NameXPtg ptg); Ptg[] getFormulaTokens(EvaluationCell cell); UDFFinder getUDFFinder(); + SpreadsheetVersion getSpreadsheetVersion(); /** * Propagated from {@link WorkbookEvaluator#clearAllCachedResultValues()} to clear locally cached data. diff --git a/src/java/org/apache/poi/ss/formula/FormulaParser.java b/src/java/org/apache/poi/ss/formula/FormulaParser.java index 27499501f..cdfc3536d 100644 --- a/src/java/org/apache/poi/ss/formula/FormulaParser.java +++ b/src/java/org/apache/poi/ss/formula/FormulaParser.java @@ -785,6 +785,7 @@ public final class FormulaParser { actualEndRow = _rowIndex; } else { // Really no special quantifiers actualStartRow++; + if (tbl.isHasTotalsRow()) actualEndRow--; } } diff --git a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java index 3fad35edc..b72e74685 100644 --- a/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java +++ b/src/java/org/apache/poi/ss/formula/WorkbookEvaluator.java @@ -25,6 +25,7 @@ import java.util.Map; 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; @@ -45,39 +46,11 @@ import org.apache.poi.ss.formula.functions.Choose; 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; @@ -753,6 +726,107 @@ public final class WorkbookEvaluator { 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. diff --git a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java index adc3e4f07..6713ec25e 100644 --- a/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java +++ b/src/java/org/apache/poi/ss/formula/eval/forked/ForkedEvaluationWorkbook.java @@ -20,6 +20,7 @@ package org.apache.poi.ss.formula.eval.forked; 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; @@ -155,6 +156,10 @@ final class ForkedEvaluationWorkbook implements EvaluationWorkbook { 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() diff --git a/src/java/org/apache/poi/ss/usermodel/ConditionFilterData.java b/src/java/org/apache/poi/ss/usermodel/ConditionFilterData.java new file mode 100644 index 000000000..2d532e31f --- /dev/null +++ b/src/java/org/apache/poi/ss/usermodel/ConditionFilterData.java @@ -0,0 +1,57 @@ +/* + * ==================================================================== + * 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(); + +} diff --git a/src/java/org/apache/poi/ss/usermodel/ConditionFilterType.java b/src/java/org/apache/poi/ss/usermodel/ConditionFilterType.java new file mode 100644 index 000000000..639991558 --- /dev/null +++ b/src/java/org/apache/poi/ss/usermodel/ConditionFilterType.java @@ -0,0 +1,46 @@ +/* + * ==================================================================== + * 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. + *

+ * Instance names match the constants from STCfType 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, + ; +} diff --git a/src/java/org/apache/poi/ss/usermodel/ConditionalFormattingRule.java b/src/java/org/apache/poi/ss/usermodel/ConditionalFormattingRule.java index 635aaa21f..fa705e61d 100644 --- a/src/java/org/apache/poi/ss/usermodel/ConditionalFormattingRule.java +++ b/src/java/org/apache/poi/ss/usermodel/ConditionalFormattingRule.java @@ -83,6 +83,32 @@ public interface ConditionalFormattingRule { * @return the type of condition */ ConditionType getConditionType(); + + /** + * This is null if + *

+ * {@link #getConditionType()} != {@link ConditionType#FILTER} + *

+ * This is always {@link ConditionFilterType#FILTER} for HSSF rules of type {@link ConditionType#FILTER}. + *

+ * 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 + *

+ * {@link #getConditionFilterType()} == null + *

+ * This means it is always null for HSSF, which does not define the extended condition types. + *

+ * 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 @@ -119,4 +145,25 @@ public interface ConditionalFormattingRule { * @return the second formula */ String getFormula2(); + + /** + * HSSF just returns 0, XSSF uses the value stored in the model if present, + * otherwise uses 0. + *

+ * If priority is 0, just use definition order, as that's how HSSF rules are evaluated. + *

+ * 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 Microsoft Excel help + */ + boolean getStopIfTrue(); } diff --git a/src/java/org/apache/poi/ss/usermodel/ConditionalFormattingRule.java.svntmp b/src/java/org/apache/poi/ss/usermodel/ConditionalFormattingRule.java.svntmp new file mode 100644 index 000000000..aa16c7761 --- /dev/null +++ b/src/java/org/apache/poi/ss/usermodel/ConditionalFormattingRule.java.svntmp @@ -0,0 +1,169 @@ +/* + * ==================================================================== + * 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; + +/** + * Represents a description of a conditional formatting rule + */ +public interface ConditionalFormattingRule { + /** + * Create a new border formatting structure if it does not exist, + * otherwise just return existing object. + * + * @return - border formatting object, never returns null. + */ + BorderFormatting createBorderFormatting(); + + /** + * @return - border formatting object if defined, null otherwise + */ + BorderFormatting getBorderFormatting(); + + /** + * Create a new font formatting structure if it does not exist, + * otherwise just return existing object. + * + * @return - font formatting object, never returns null. + */ + FontFormatting createFontFormatting(); + + /** + * @return - font formatting object if defined, null otherwise + */ + FontFormatting getFontFormatting(); + + /** + * Create a new pattern formatting structure if it does not exist, + * otherwise just return existing object. + * + * @return - pattern formatting object, never returns null. + */ + PatternFormatting createPatternFormatting(); + + /** + * @return - pattern formatting object if defined, null otherwise + */ + PatternFormatting getPatternFormatting(); + + /** + * @return - databar / data-bar formatting object if defined, null otherwise + */ + DataBarFormatting getDataBarFormatting(); + + /** + * @return - icon / multi-state formatting object if defined, null otherwise + */ + IconMultiStateFormatting getMultiStateFormatting(); + + /** + * @return color scale / color grate formatting object if defined, null otherwise + */ + ColorScaleFormatting getColorScaleFormatting(); + + /** + * Type of conditional formatting rule. + * + * @return the type of condition + */ + ConditionType getConditionType(); + + /** + * This is null if + *

+ * {@link #getConditionType()} != {@link ConditionType#FILTER} + *

+ * This is always {@link ConditionFilterType#FILTER} for HSSF rules of type {@link ConditionType#FILTER}. + *

+ * 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 + *

+ * {@link #getConditionFilterType()} == null + *

+ * This means it is always null for HSSF, which does not define the extended condition types. + *

+ * 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 + * {@link ConditionType#CELL_VALUE_IS} + *

+ * MUST be a constant from {@link ComparisonOperator} + *

+ * + * @return the conditional format operator + */ + byte getComparisonOperation(); + + /** + * The formula used to evaluate the first operand for the conditional formatting rule. + *

+ * If the condition type is {@link ConditionType#CELL_VALUE_IS}, + * this field is the first operand of the comparison. + * If type is {@link ConditionType#FORMULA}, this formula is used + * to determine if the conditional formatting is applied. + *

+ *

+ * If comparison type is {@link ConditionType#FORMULA} the formula MUST be a Boolean function + *

+ * + * @return the first formula + */ + String getFormula1(); + + /** + * The formula used to evaluate the second operand of the comparison when + * comparison type is {@link ConditionType#CELL_VALUE_IS} and operator + * is either {@link ComparisonOperator#BETWEEN} or {@link ComparisonOperator#NOT_BETWEEN} + * + * @return the second formula + */ + String getFormula2(); + + /** + * HSSF just returns 0, XSSF uses the value stored in the model if present, + * otherwise uses 0. + *

+ * If priority is 0, just use definition order, as that's how HSSF rules are evaluated. + *

+ * 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 Microsoft Excel help + */ + boolean getStopIfTrue(); +} diff --git a/src/java/org/apache/poi/ss/util/CellRangeAddressBase.java b/src/java/org/apache/poi/ss/util/CellRangeAddressBase.java index f94444d9a..e4dc4aeee 100644 --- a/src/java/org/apache/poi/ss/util/CellRangeAddressBase.java +++ b/src/java/org/apache/poi/ss/util/CellRangeAddressBase.java @@ -18,6 +18,7 @@ package org.apache.poi.ss.util; import org.apache.poi.ss.SpreadsheetVersion; +import org.apache.poi.ss.usermodel.Cell; /** @@ -125,6 +126,34 @@ public abstract class CellRangeAddressBase { _firstCol <= colInd && colInd <= _lastCol; //containsColumn } + /** + * Determines if the given {@link CellReference} lies within the bounds + * of this range. + *

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. + *

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 * diff --git a/src/java/org/apache/poi/ss/util/SheetUtil.java b/src/java/org/apache/poi/ss/util/SheetUtil.java index f234380fe..98c307f3e 100644 --- a/src/java/org/apache/poi/ss/util/SheetUtil.java +++ b/src/java/org/apache/poi/ss/util/SheetUtil.java @@ -351,6 +351,29 @@ public class SheetUtil { return cr.isInRange(rowIx, colIx); } + /** + * Return the cell, without taking account of merged regions. + *

+ * Use {@link #getCellWithMerges(Sheet, int, int)} if you want the top left + * cell from merged regions instead when the reference is a merged cell. + *

+ * 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. @@ -361,22 +384,22 @@ public class SheetUtil { * then will return the cell itself. *

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()); } diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionFilterData.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionFilterData.java new file mode 100644 index 000000000..59bfb9406 --- /dev/null +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionFilterData.java @@ -0,0 +1,57 @@ +/* + * ==================================================================== + * 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(); + } + +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java index 6c5c36479..69816b7fa 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java @@ -30,13 +30,14 @@ import org.apache.poi.xssf.model.StylesTable; 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 typeLookup = new HashMap(); + private static Map filterTypeLookup = new HashMap(); static { typeLookup.put(STCfType.CELL_IS, ConditionType.CELL_VALUE_IS); typeLookup.put(STCfType.EXPRESSION, ConditionType.FORMULA); @@ -58,8 +59,27 @@ public class XSSFConditionalFormattingRule implements ConditionalFormattingRule 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; @@ -89,6 +109,16 @@ public class XSSFConditionalFormattingRule implements ConditionalFormattingRule 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. @@ -303,6 +333,18 @@ public class XSSFConditionalFormattingRule implements ConditionalFormattingRule 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} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java index 61a56a102..ddf8458d0 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFSheet.java @@ -3869,7 +3869,7 @@ public class XSSFSheet extends POIXMLDocumentPart implements Sheet { 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 cr = getCellRange(range); for (XSSFCell c : cr) { diff --git a/src/ooxml/testcases/org/apache/poi/ss/usermodel/ConditionalFormattingEvalTest.java b/src/ooxml/testcases/org/apache/poi/ss/usermodel/ConditionalFormattingEvalTest.java new file mode 100644 index 000000000..77515b116 --- /dev/null +++ b/src/ooxml/testcases/org/apache/poi/ss/usermodel/ConditionalFormattingEvalTest.java @@ -0,0 +1,118 @@ +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 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 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(); + } +} diff --git a/test-data/spreadsheet/ConditionalFormattingSamples.xls b/test-data/spreadsheet/ConditionalFormattingSamples.xls new file mode 100644 index 000000000..9b24b857a Binary files /dev/null and b/test-data/spreadsheet/ConditionalFormattingSamples.xls differ diff --git a/test-data/spreadsheet/ConditionalFormattingSamples.xlsx b/test-data/spreadsheet/ConditionalFormattingSamples.xlsx new file mode 100644 index 000000000..afaa40aa9 Binary files /dev/null and b/test-data/spreadsheet/ConditionalFormattingSamples.xlsx differ diff --git a/test-data/spreadsheet/DataValidationEvaluations.xlsx b/test-data/spreadsheet/DataValidationEvaluations.xlsx new file mode 100644 index 000000000..5bf8872bf Binary files /dev/null and b/test-data/spreadsheet/DataValidationEvaluations.xlsx differ