mirror of https://github.com/apache/poi.git
Add evaluation of data validation rules and conditional formatting
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1782894 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
parent
d66307cb67
commit
99390a5759
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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.<p/>
|
||||
*
|
||||
* For performance reasons, this class keeps a cache of all previously evaluated rules and cells.
|
||||
* Be sure to call {@link #clearAllCachedFormats()} if any conditional formats are modified, added, or deleted,
|
||||
* and {@link #clearAllCachedValues()} whenever cell values change.
|
||||
* <p/>
|
||||
*
|
||||
*/
|
||||
public class ConditionalFormattingEvaluator {
|
||||
|
||||
private final WorkbookEvaluator workbookEvaluator;
|
||||
private final Workbook workbook;
|
||||
|
||||
/**
|
||||
* All the underlying structures, for both HSSF and XSSF, repeatedly go to the raw bytes/XML for the
|
||||
* different pieces used in the ConditionalFormatting* structures. That's highly inefficient,
|
||||
* and can cause significant lag when checking formats for large workbooks.
|
||||
* <p/>
|
||||
* Instead we need a cached version that is discarded when definitions change.
|
||||
* <p/>
|
||||
* Sheets don't implement equals, and since its an interface,
|
||||
* there's no guarantee instances won't be recreated on the fly by some implementation.
|
||||
* So we use sheet name.
|
||||
*/
|
||||
private final Map<String, List<EvaluationConditionalFormatRule>> formats = new HashMap<String, List<EvaluationConditionalFormatRule>>();
|
||||
|
||||
/**
|
||||
* Evaluating rules for cells in their region(s) is expensive, so we want to cache them,
|
||||
* and empty/reevaluate the cache when values change.
|
||||
* <p/>
|
||||
* Rule lists are in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF)
|
||||
* <p/>
|
||||
* CellReference implements equals().
|
||||
*/
|
||||
private final Map<CellReference, List<EvaluationConditionalFormatRule>> values = new HashMap<CellReference, List<EvaluationConditionalFormatRule>>();
|
||||
|
||||
public ConditionalFormattingEvaluator(Workbook wb, WorkbookEvaluatorProvider provider) {
|
||||
this.workbook = wb;
|
||||
this.workbookEvaluator = provider._getWorkbookEvaluator();
|
||||
}
|
||||
|
||||
protected WorkbookEvaluator getWorkbookEvaluator() {
|
||||
return workbookEvaluator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this whenever rules are added, reordered, or removed, or a rule formula is changed
|
||||
* (not the formula inputs but the formula expression itself)
|
||||
*/
|
||||
public void clearAllCachedFormats() {
|
||||
formats.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this whenever cell values change in the workbook, so condional formats are re-evaluated
|
||||
* for all cells.
|
||||
* <p/>
|
||||
* TODO: eventually this should work like {@link EvaluationCache#notifyUpdateCell(int, int, EvaluationCell)}
|
||||
* and only clear values that need recalculation based on the formula dependency tree.
|
||||
*/
|
||||
public void clearAllCachedValues() {
|
||||
values.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* lazy load by sheet since reading can be expensive
|
||||
*
|
||||
* @param sheet
|
||||
* @return unmodifiable list of rules
|
||||
*/
|
||||
protected List<EvaluationConditionalFormatRule> getRules(Sheet sheet) {
|
||||
final String sheetName = sheet.getSheetName();
|
||||
List<EvaluationConditionalFormatRule> rules = formats.get(sheetName);
|
||||
if (rules == null && ! formats.containsKey(sheetName)) {
|
||||
final SheetConditionalFormatting scf = sheet.getSheetConditionalFormatting();
|
||||
final int count = scf.getNumConditionalFormattings();
|
||||
rules = new ArrayList<EvaluationConditionalFormatRule>(count);
|
||||
formats.put(sheetName, rules);
|
||||
for (int i=0; i < count; i++) {
|
||||
ConditionalFormatting f = scf.getConditionalFormattingAt(i);
|
||||
//optimization, as this may be expensive for lots of ranges
|
||||
final CellRangeAddress[] regions = f.getFormattingRanges();
|
||||
for (int r=0; r < f.getNumberOfRules(); r++) {
|
||||
ConditionalFormattingRule rule = f.getRule(r);
|
||||
rules.add(new EvaluationConditionalFormatRule(workbookEvaluator, sheet, f, i, rule, r, regions));
|
||||
}
|
||||
}
|
||||
// need them in formatting and priority order so logic works right
|
||||
Collections.sort(rules);
|
||||
}
|
||||
return Collections.unmodifiableList(rules);
|
||||
}
|
||||
|
||||
/**
|
||||
* This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
|
||||
* in defined "priority" order, returning the matches if any. This is a property currently
|
||||
* not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
|
||||
* <p/>
|
||||
* Most cells will have zero or one applied rule, but it is possible to define multiple rules
|
||||
* that apply at the same time to the same cell, thus the List result.
|
||||
* <p/>
|
||||
* Note that to properly apply conditional rules, care must be taken to offset the base
|
||||
* formula by the relative position of the current cell, or the wrong value is checked.
|
||||
* This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
|
||||
*
|
||||
* @param cell NOTE: if no sheet name is specified, this uses the workbook active sheet
|
||||
* @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
|
||||
* in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
|
||||
* or null if none apply
|
||||
*/
|
||||
public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(final CellReference cellRef) {
|
||||
String sheetName = cellRef.getSheetName();
|
||||
Sheet sheet = null;
|
||||
if (sheetName == null) sheet = workbook.getSheetAt(workbook.getActiveSheetIndex());
|
||||
else sheet = workbook.getSheet(sheetName);
|
||||
|
||||
final Cell cell = SheetUtil.getCell(sheet, cellRef.getRow(), cellRef.getCol());
|
||||
|
||||
if (cell == null) return Collections.emptyList();
|
||||
|
||||
return getConditionalFormattingForCell(cell, cellRef);
|
||||
}
|
||||
|
||||
/**
|
||||
* This checks all applicable {@link ConditionalFormattingRule}s for the cell's sheet,
|
||||
* in defined "priority" order, returning the matches if any. This is a property currently
|
||||
* not exposed from <code>CTCfRule</code> in <code>XSSFConditionalFormattingRule</code>.
|
||||
* <p/>
|
||||
* Most cells will have zero or one applied rule, but it is possible to define multiple rules
|
||||
* that apply at the same time to the same cell, thus the List result.
|
||||
* <p/>
|
||||
* Note that to properly apply conditional rules, care must be taken to offset the base
|
||||
* formula by the relative position of the current cell, or the wrong value is checked.
|
||||
* This is handled by {@link WorkbookEvaluator#evaluate(String, CellReference, CellRangeAddressBase)}.
|
||||
*
|
||||
* @param cell
|
||||
* @return Unmodifiable List of {@link EvaluationConditionalFormattingRule}s that apply to the current cell value,
|
||||
* in priority order, as evaluated by Excel (smallest priority # for XSSF, definition order for HSSF),
|
||||
* or null if none apply
|
||||
*/
|
||||
public List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell) {
|
||||
return getConditionalFormattingForCell(cell, getRef(cell));
|
||||
}
|
||||
|
||||
/**
|
||||
* We need both, and can derive one from the other, but this is to avoid duplicate work
|
||||
*
|
||||
* @param cell
|
||||
* @param ref
|
||||
* @return unmodifiable list of matching rules
|
||||
*/
|
||||
private List<EvaluationConditionalFormatRule> getConditionalFormattingForCell(Cell cell, CellReference ref) {
|
||||
List<EvaluationConditionalFormatRule> rules = values.get(ref);
|
||||
|
||||
if (rules == null) {
|
||||
// compute and cache them
|
||||
rules = new ArrayList<EvaluationConditionalFormatRule>();
|
||||
/*
|
||||
* Per Excel help:
|
||||
* https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-e09711a3-48df-4bcb-b82c-9d8b8b22463d#__toc269129417
|
||||
* stopIfTrue is true for all rules from HSSF files, and an explicit value for XSSF files.
|
||||
* thus the explicit ordering of the rule lists in #getFormattingRulesForSheet(Sheet)
|
||||
*/
|
||||
boolean stopIfTrue = false;
|
||||
for (EvaluationConditionalFormatRule rule : getRules(cell.getSheet())) {
|
||||
|
||||
if (stopIfTrue) continue; // a previous rule matched and wants no more evaluations
|
||||
|
||||
if (rule.matches(cell)) {
|
||||
rules.add(rule);
|
||||
stopIfTrue = rule.getRule().getStopIfTrue();
|
||||
}
|
||||
}
|
||||
Collections.sort(rules);
|
||||
values.put(ref, rules);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(rules);
|
||||
}
|
||||
|
||||
public static CellReference getRef(Cell cell) {
|
||||
return new CellReference(cell.getSheet().getSheetName(), cell.getRowIndex(), cell.getColumnIndex(), false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sheetName
|
||||
* @return unmodifiable list of all Conditional format rules for the given sheet, if any
|
||||
*/
|
||||
public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(String sheetName) {
|
||||
return getFormatRulesForSheet(workbook.getSheet(sheetName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sheet
|
||||
* @return unmodifiable list of all Conditional format rules for the given sheet, if any
|
||||
*/
|
||||
public List<EvaluationConditionalFormatRule> getFormatRulesForSheet(Sheet sheet) {
|
||||
return getRules(sheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditional formatting rules can apply only to cells in the sheet to which they are attached.
|
||||
* The POI data model does not have a back-reference to the owning sheet, so it must be passed in separately.
|
||||
* <p/>
|
||||
* We could overload this with convenience methods taking a sheet name and sheet index as well.
|
||||
* <p/>
|
||||
* @param sheet containing the rule
|
||||
* @param index of the {@link ConditionalFormatting} instance in the sheet's array
|
||||
* @return unmodifiable List of all cells in the rule's region matching the rule's condition
|
||||
*/
|
||||
public List<Cell> getMatchingCells(Sheet sheet, int conditionalFormattingIndex, int ruleIndex) {
|
||||
for (EvaluationConditionalFormatRule rule : getRules(sheet)) {
|
||||
if (rule.getSheet().equals(sheet) && rule.getFormattingIndex() == conditionalFormattingIndex && rule.getRuleIndex() == ruleIndex) {
|
||||
return getMatchingCells(rule);
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param rule
|
||||
* @return unmodifiable List of all cells in the rule's region matching the rule's condition
|
||||
*/
|
||||
public List<Cell> getMatchingCells(EvaluationConditionalFormatRule rule) {
|
||||
final List<Cell> cells = new ArrayList<Cell>();
|
||||
final Sheet sheet = rule.getSheet();
|
||||
|
||||
for (CellRangeAddress region : rule.getRegions()) {
|
||||
for (int r = region.getFirstRow(); r <= region.getLastRow(); r++) {
|
||||
final Row row = sheet.getRow(r);
|
||||
if (row == null) continue; // no cells to check
|
||||
for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) {
|
||||
final Cell cell = row.getCell(c);
|
||||
if (cell == null) continue;
|
||||
|
||||
List<EvaluationConditionalFormatRule> cellRules = getConditionalFormattingForCell(cell);
|
||||
if (cellRules.contains(rule)) cells.add(cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(cells);
|
||||
}
|
||||
}
|
|
@ -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.<p/>
|
||||
*
|
||||
* For performance reasons, this class keeps a cache of all previously retrieved {@link DataValidation} instances.
|
||||
* Be sure to call {@link #clearAllCachedValues()} if any workbook validation definitions are
|
||||
* added, modified, or deleted.
|
||||
* <p/>
|
||||
* Changing cell values should be fine, as long as the corresponding {@link WorkbookEvaluator#clearAllCachedResultValues()}
|
||||
* is called as well.
|
||||
*
|
||||
*/
|
||||
public class DataValidationEvaluator {
|
||||
|
||||
/**
|
||||
* Expensive to compute, so cache them as they are retrieved.
|
||||
* <p/>
|
||||
* Sheets don't implement equals, and since its an interface,
|
||||
* there's no guarantee instances won't be recreated on the fly by some implementation.
|
||||
* So we use sheet name.
|
||||
*/
|
||||
private final Map<String, List<? extends DataValidation>> validations = new HashMap<String, List<? extends DataValidation>>();
|
||||
|
||||
private final Workbook workbook;
|
||||
private final WorkbookEvaluator workbookEvaluator;
|
||||
|
||||
public DataValidationEvaluator(Workbook wb, WorkbookEvaluatorProvider provider) {
|
||||
this.workbook = wb;
|
||||
this.workbookEvaluator = provider._getWorkbookEvaluator();
|
||||
}
|
||||
|
||||
protected WorkbookEvaluator getWorkbookEvaluator() {
|
||||
return workbookEvaluator;
|
||||
}
|
||||
|
||||
public void clearAllCachedValues() {
|
||||
validations.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* lazy load validations by sheet, since reading the CT* types is expensive
|
||||
* @param sheet
|
||||
* @return
|
||||
*/
|
||||
private List<? extends DataValidation> getValidations(Sheet sheet) {
|
||||
List<? extends DataValidation> dvs = validations.get(sheet.getSheetName());
|
||||
if (dvs == null && !validations.containsKey(sheet.getSheetName())) {
|
||||
dvs = sheet.getDataValidations();
|
||||
validations.put(sheet.getSheetName(), dvs);
|
||||
}
|
||||
return dvs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns the {@link DataValidation} for the cell, if there is
|
||||
* one. Lookup is based on the first match from
|
||||
* {@link DataValidation#getRegions()} for the cell's sheet. DataValidation
|
||||
* regions must be in the same sheet as the DataValidation. Allowed values
|
||||
* expressions may reference other sheets, however.
|
||||
*
|
||||
* @param cell reference to check - use this in case the cell does not actually exist yet
|
||||
* @return the DataValidation applicable to the given cell, or null if no
|
||||
* validation applies
|
||||
*/
|
||||
public DataValidation getValidationForCell(CellReference cell) {
|
||||
return getValidationContextForCell(cell).getValidation();
|
||||
}
|
||||
|
||||
public DataValidationContext getValidationContextForCell(CellReference cell) {
|
||||
// TODO
|
||||
final Sheet sheet = workbook.getSheet(cell.getSheetName());
|
||||
if (sheet == null) return null;
|
||||
final List<? extends DataValidation> dataValidations = getValidations(sheet);
|
||||
if (dataValidations == null) return null;
|
||||
for (DataValidation dv : dataValidations) {
|
||||
final CellRangeAddressList regions = dv.getRegions();
|
||||
if (regions == null) return null;
|
||||
// current implementation can't return null
|
||||
for (CellRangeAddressBase range : regions.getCellRangeAddresses()) {
|
||||
if (range.isInRange(cell)) {
|
||||
return new DataValidationContext(dv, this, range, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* If {@link #getValidationForCell(Cell)} returns an instance, and the
|
||||
* {@link ValidationType} is {@link ValidationType#LIST}, return the valid
|
||||
* values, whether they are from a static list or cell range.
|
||||
* <p/>
|
||||
* For all other validation types, or no validation at all, this method
|
||||
* returns null.
|
||||
* <p/>
|
||||
* This method could throw an exception if the validation type is not LIST,
|
||||
* but since this method is mostly useful in UI contexts, null seems the
|
||||
* easier path.
|
||||
*
|
||||
* @param cell reference to check - use this in case the cell does not actually exist yet
|
||||
* @return returns an unmodifiable {@link List} of {@link ValueEval}s if applicable, or
|
||||
* null
|
||||
*/
|
||||
public List<ValueEval> getValidationValuesForCell(CellReference cell) {
|
||||
DataValidationContext context = getValidationContextForCell(cell);
|
||||
|
||||
if (context == null) return null;
|
||||
|
||||
return getValidationValuesForConstraint(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* static so enums can reference it without creating a whole instance
|
||||
* @param cell
|
||||
* @param val
|
||||
* @return returns an unmodifiable {@link List} of {@link ValueEval}s, which may be empty
|
||||
*/
|
||||
protected static List<ValueEval> getValidationValuesForConstraint(DataValidationContext context) {
|
||||
final DataValidationConstraint val = context.getValidation().getValidationConstraint();
|
||||
if (val.getValidationType() != ValidationType.LIST) return null;
|
||||
|
||||
String formula = val.getFormula1();
|
||||
|
||||
final List<ValueEval> values = new ArrayList<ValueEval>();
|
||||
|
||||
if (val.getExplicitListValues() != null && val.getExplicitListValues().length > 0) {
|
||||
// assumes parsing interprets the overloaded property right for XSSF
|
||||
for (String s : val.getExplicitListValues()) {
|
||||
if (s != null) values.add(new StringEval(s)); // constructor throws exception on null
|
||||
}
|
||||
} else if (formula != null) {
|
||||
// evaluate formula for cell refs then get their values
|
||||
ValueEval eval = context.getEvaluator().getWorkbookEvaluator().evaluate(formula, context.getTarget(), context.getRegion());
|
||||
// formula is a StringEval if the validation is by a fixed list. Use the explicit list later.
|
||||
// there is no way from the model to tell if the list is fixed values or formula based.
|
||||
if (eval instanceof TwoDEval) {
|
||||
TwoDEval twod = (TwoDEval) eval;
|
||||
for (int i=0; i < twod.getHeight(); i++) {
|
||||
final ValueEval cellValue = twod.getValue(i, 0);
|
||||
values.add(cellValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the validation returned by {@link #getValidationForCell(Cell)} if you
|
||||
* want the error display details. This is the validation checked by this
|
||||
* method, which attempts to replicate Excel's data validation rules.
|
||||
* <p/>
|
||||
* Note that to properly apply some validations, care must be taken to
|
||||
* offset the base validation formula by the relative position of the
|
||||
* current cell, or the wrong value is checked.
|
||||
*
|
||||
* @param cell
|
||||
* @return true if the cell has no validation or the cell value passes the
|
||||
* defined validation, false if it fails
|
||||
*/
|
||||
public boolean isValidCell(CellReference cellRef) {
|
||||
final DataValidationContext context = getValidationContextForCell(cellRef);
|
||||
|
||||
if (context == null) return true;
|
||||
|
||||
final Cell cell = SheetUtil.getCell(workbook.getSheet(cellRef.getSheetName()), cellRef.getRow(), cellRef.getCol());
|
||||
|
||||
// now we can validate the cell
|
||||
|
||||
// if empty, return not allowed flag
|
||||
if ( cell == null
|
||||
|| isType(cell, CellType.BLANK)
|
||||
|| (isType(cell,CellType.STRING)
|
||||
&& (cell.getStringCellValue() == null || cell.getStringCellValue().isEmpty())
|
||||
)
|
||||
) {
|
||||
return context.getValidation().getEmptyCellAllowed();
|
||||
}
|
||||
|
||||
// cell has a value
|
||||
|
||||
return ValidationEnum.isValid(cell, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that this assumes the cell cached value is up to date and in sync with data edits
|
||||
* @param cell
|
||||
* @param type
|
||||
* @return true if the cell or cached cell formula result type match the given type
|
||||
*/
|
||||
public static boolean isType(Cell cell, CellType type) {
|
||||
final CellType cellType = cell.getCellTypeEnum();
|
||||
return cellType == type
|
||||
|| (cellType == CellType.FORMULA
|
||||
&& cell.getCachedFormulaResultTypeEnum() == type
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Not calling it ValidationType to avoid confusion for now with DataValidationConstraint.ValidationType.
|
||||
* Definition order matches OOXML type ID indexes
|
||||
*/
|
||||
public static enum ValidationEnum {
|
||||
ANY {
|
||||
public boolean isValidValue(Cell cell, DataValidationContext context) {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
INTEGER {
|
||||
public boolean isValidValue(Cell cell, DataValidationContext context) {
|
||||
if (super.isValidValue(cell, context)) {
|
||||
// we know it is a number in the proper range, now check if it is an int
|
||||
final double value = cell.getNumericCellValue(); // can't get here without a valid numeric value
|
||||
return Double.valueOf(value).compareTo(Double.valueOf((int) value)) == 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
DECIMAL,
|
||||
LIST {
|
||||
public boolean isValidValue(Cell cell, DataValidationContext context) {
|
||||
final List<ValueEval> valueList = getValidationValuesForConstraint(context);
|
||||
if (valueList == null) return true; // special case
|
||||
|
||||
// compare cell value to each item
|
||||
for (ValueEval listVal : valueList) {
|
||||
ValueEval comp = listVal instanceof RefEval ? ((RefEval) listVal).getInnerValueEval(context.getSheetIndex()) : listVal;
|
||||
|
||||
// any value is valid if the list contains a blank value per Excel help
|
||||
if (comp instanceof BlankEval) return true;
|
||||
if (comp instanceof ErrorEval) continue; // nothing to check
|
||||
if (comp instanceof BoolEval) {
|
||||
if (isType(cell, CellType.BOOLEAN) && ((BoolEval) comp).getBooleanValue() == cell.getBooleanCellValue() ) {
|
||||
return true;
|
||||
} else {
|
||||
continue; // check the rest
|
||||
}
|
||||
}
|
||||
if (comp instanceof NumberEval) {
|
||||
// could this have trouble with double precision/rounding errors and date/time values?
|
||||
// do we need to allow a "close enough" double fractional range?
|
||||
// I see 17 digits after the decimal separator in XSSF files, and for time values,
|
||||
// there are sometimes discrepancies in the final decimal place.
|
||||
// I don't have a validation test case yet though. - GW
|
||||
if (isType(cell, CellType.NUMERIC) && ((NumberEval) comp).getNumberValue() == cell.getNumericCellValue()) {
|
||||
return true;
|
||||
} else {
|
||||
continue; // check the rest
|
||||
}
|
||||
}
|
||||
if (comp instanceof StringEval) {
|
||||
// interestingly, in testing, a validation value of the string "TRUE" or "true"
|
||||
// did not match a boolean cell value of TRUE - so apparently cell type matters
|
||||
// also, Excel validation is case insensitive - "true" is valid for the list value "TRUE"
|
||||
if (isType(cell, CellType.STRING) && ((StringEval) comp).getStringValue().equalsIgnoreCase(cell.getStringCellValue())) {
|
||||
return true;
|
||||
} else {
|
||||
continue; // check the rest;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // no matches
|
||||
}
|
||||
},
|
||||
DATE,
|
||||
TIME,
|
||||
TEXT_LENGTH {
|
||||
public boolean isValidValue(Cell cell, DataValidationContext context) {
|
||||
if (! isType(cell, CellType.STRING)) return false;
|
||||
String v = cell.getStringCellValue();
|
||||
return isValidNumericValue(Double.valueOf(v.length()), context);
|
||||
}
|
||||
},
|
||||
FORMULA {
|
||||
/**
|
||||
* Note the formula result must either be a boolean result, or anything not in error.
|
||||
* If boolean, value must be true to pass, anything else valid is also passing, errors fail.
|
||||
* @see org.apache.poi.ss.formula.DataValidationEvaluator.ValidationEnum#isValidValue(org.apache.poi.ss.usermodel.Cell, org.apache.poi.ss.usermodel.DataValidationConstraint, org.apache.poi.ss.formula.WorkbookEvaluator)
|
||||
*/
|
||||
public boolean isValidValue(Cell cell, DataValidationContext context) {
|
||||
ValueEval comp = context.getEvaluator().getWorkbookEvaluator().evaluate(context.getFormula1(), context.getTarget(), context.getRegion());
|
||||
if (comp instanceof RefEval) {
|
||||
comp = ((RefEval) comp).getInnerValueEval(((RefEval) comp).getFirstSheetIndex());
|
||||
}
|
||||
|
||||
if (comp instanceof BlankEval) return true;
|
||||
if (comp instanceof ErrorEval) return false;
|
||||
if (comp instanceof BoolEval) {
|
||||
return ((BoolEval) comp).getBooleanValue();
|
||||
}
|
||||
// empirically tested in Excel - 0=false, any other number = true/valid
|
||||
// see test file DataValidationEvaluations.xlsx
|
||||
if (comp instanceof NumberEval) {
|
||||
return ((NumberEval) comp).getNumberValue() != 0;
|
||||
}
|
||||
return false; // anything else is false, such as text
|
||||
}
|
||||
},
|
||||
;
|
||||
|
||||
public boolean isValidValue(Cell cell, DataValidationContext context) {
|
||||
return isValidNumericCell(cell, context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the cell value, which may be the cached formula result value.
|
||||
* We won't re-evaluate cells here. This validation would be after the cell value was updated externally.
|
||||
* Excel allows invalid values through methods like copy/paste, and only validates them when the user
|
||||
* interactively edits the cell.
|
||||
* @param cell
|
||||
* @param dvc
|
||||
* @param wbe
|
||||
* @return
|
||||
*/
|
||||
protected boolean isValidNumericCell(Cell cell, DataValidationContext context) {
|
||||
if ( ! isType(cell, CellType.NUMERIC)) return false;
|
||||
|
||||
Double value = Double.valueOf(cell.getNumericCellValue());
|
||||
return isValidNumericValue(value, context);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
protected boolean isValidNumericValue(Double value, final DataValidationContext context) {
|
||||
try {
|
||||
Double t1 = evalOrConstant(context.getFormula1(), context);
|
||||
// per Excel, a blank value for a numeric validation constraint formula validates true
|
||||
if (t1 == null) return true;
|
||||
Double t2 = null;
|
||||
if (context.getOperator() == OperatorType.BETWEEN || context.getOperator() == OperatorType.NOT_BETWEEN) {
|
||||
t2 = evalOrConstant(context.getFormula2(), context);
|
||||
// per Excel, a blank value for a numeric validation constraint formula validates true
|
||||
if (t2 == null) return true;
|
||||
}
|
||||
return OperatorEnum.values()[context.getOperator()].isValid(value, t1, t2);
|
||||
} catch (NumberFormatException e) {
|
||||
// one or both formulas are in error, not evaluating to a number, so the validation is false per Excel's behavior.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a numeric formula value as either a constant or numeric expression.
|
||||
* Note that Excel treats validations with constraint formulas that evaluate to null as valid,
|
||||
* but evaluations in error or non-numeric are marked invalid.
|
||||
* @param formula
|
||||
* @param context
|
||||
* @return numeric value or null if not defined or the formula evaluates to an empty/missing cell.
|
||||
* @throws NumberFormatException if the formula is non-numeric when it should be
|
||||
*/
|
||||
private Double evalOrConstant(String formula, DataValidationContext context) throws NumberFormatException {
|
||||
if (formula == null || formula.trim().isEmpty()) return null; // shouldn't happen, but just in case
|
||||
try {
|
||||
return Double.valueOf(formula);
|
||||
} catch (NumberFormatException e) {
|
||||
// must be an expression, then. Overloading by Excel in the file formats.
|
||||
}
|
||||
ValueEval eval = context.getEvaluator().getWorkbookEvaluator().evaluate(formula, context.getTarget(), context.getRegion());
|
||||
if (eval instanceof RefEval) {
|
||||
eval = ((RefEval) eval).getInnerValueEval(((RefEval) eval).getFirstSheetIndex());
|
||||
}
|
||||
if (eval instanceof BlankEval) return null;
|
||||
if (eval instanceof NumberEval) return Double.valueOf(((NumberEval) eval).getNumberValue());
|
||||
if (eval instanceof StringEval) {
|
||||
final String value = ((StringEval) eval).getStringValue();
|
||||
if (value == null || value.trim().isEmpty()) return null;
|
||||
// try to parse the cell value as a double and return it
|
||||
return Double.valueOf(value);
|
||||
}
|
||||
throw new NumberFormatException("Formula '" + formula + "' evaluates to something other than a number");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates against the type defined in dvc, as an index of the enum values array.
|
||||
* @param cell
|
||||
* @param dvc
|
||||
* @param wbe
|
||||
* @return true if validation passes
|
||||
* @throws ArrayIndexOutOfBoundsException if the constraint type is an invalid index
|
||||
*/
|
||||
public static boolean isValid(Cell cell, DataValidationContext context) {
|
||||
return values()[context.getValidation().getValidationConstraint().getValidationType()].isValidValue(cell, context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Not calling it OperatorType to avoid confusion for now with DataValidationConstraint.OperatorType.
|
||||
* Definition order matches OOXML type ID indexes
|
||||
*/
|
||||
public static enum OperatorEnum {
|
||||
BETWEEN {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) >= 0 && cellValue.compareTo(v2) <= 0;
|
||||
}
|
||||
},
|
||||
NOT_BETWEEN {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) < 0 || cellValue.compareTo(v2) > 0;
|
||||
}
|
||||
},
|
||||
EQUAL {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) == 0;
|
||||
}
|
||||
},
|
||||
NOT_EQUAL {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) != 0;
|
||||
}
|
||||
},
|
||||
GREATER_THAN {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) > 0;
|
||||
}
|
||||
},
|
||||
LESS_THAN {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) < 0;
|
||||
}
|
||||
},
|
||||
GREATER_OR_EQUAL {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) >= 0;
|
||||
}
|
||||
},
|
||||
LESS_OR_EQUAL {
|
||||
public boolean isValid(Double cellValue, Double v1, Double v2) {
|
||||
return cellValue.compareTo(v1) <= 0;
|
||||
}
|
||||
},
|
||||
;
|
||||
|
||||
public static final OperatorEnum IGNORED = BETWEEN;
|
||||
|
||||
/**
|
||||
* Evaluates comparison using operator instance rules
|
||||
* @param cellValue won't be null, assumption is previous checks handled that
|
||||
* @param v1 if null, value assumed invalid, anything passes, per Excel behavior
|
||||
* @param v2 null if not needed. If null when needed, assume anything passes, per Excel behavior
|
||||
* @return true if the comparison is valid
|
||||
*/
|
||||
public abstract boolean isValid(Double cellValue, Double v1, Double v2);
|
||||
}
|
||||
|
||||
public static class DataValidationContext {
|
||||
private final DataValidation dv;
|
||||
private final DataValidationEvaluator dve;
|
||||
private final CellRangeAddressBase region;
|
||||
private final CellReference target;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param dv
|
||||
* @param dve
|
||||
* @param region
|
||||
* @param target
|
||||
*/
|
||||
public DataValidationContext(DataValidation dv, DataValidationEvaluator dve, CellRangeAddressBase region, CellReference target) {
|
||||
this.dv = dv;
|
||||
this.dve = dve;
|
||||
this.region = region;
|
||||
this.target = target;
|
||||
}
|
||||
/**
|
||||
* @return the dv
|
||||
*/
|
||||
public DataValidation getValidation() {
|
||||
return dv;
|
||||
}
|
||||
/**
|
||||
* @return the dve
|
||||
*/
|
||||
public DataValidationEvaluator getEvaluator() {
|
||||
return dve;
|
||||
}
|
||||
/**
|
||||
* @return the region
|
||||
*/
|
||||
public CellRangeAddressBase getRegion() {
|
||||
return region;
|
||||
}
|
||||
/**
|
||||
* @return the target
|
||||
*/
|
||||
public CellReference getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
public int getOffsetColumns() {
|
||||
return target.getCol() - region.getFirstColumn();
|
||||
}
|
||||
|
||||
public int getOffsetRows() {
|
||||
return target.getRow() - region.getFirstRow();
|
||||
}
|
||||
|
||||
public int getSheetIndex() {
|
||||
return dve.getWorkbookEvaluator().getSheetIndex(target.getSheetName());
|
||||
}
|
||||
|
||||
public String getFormula1() {
|
||||
return dv.getValidationConstraint().getFormula1();
|
||||
}
|
||||
|
||||
public String getFormula2() {
|
||||
return dv.getValidationConstraint().getFormula2();
|
||||
}
|
||||
|
||||
public int getOperator() {
|
||||
return dv.getValidationConstraint().getOperator();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
* <p/>
|
||||
* Having this all combined and cached avoids repeated access calls to the
|
||||
* underlying structural objects, XSSF CT* objects and HSSF raw byte structures.
|
||||
* Those objects can be referenced from here. This object will be out of sync if
|
||||
* anything modifies the referenced structures' evaluation properties.
|
||||
* <p/>
|
||||
* The assumption is that consuming applications will read the display properties once and
|
||||
* create whatever style objects they need, caching those at the application level.
|
||||
* Thus this class only caches values needed for evaluation, not display.
|
||||
*/
|
||||
public class EvaluationConditionalFormatRule implements Comparable<EvaluationConditionalFormatRule> {
|
||||
|
||||
private final WorkbookEvaluator workbookEvaluator;
|
||||
private final Sheet sheet;
|
||||
private final ConditionalFormatting formatting;
|
||||
private final ConditionalFormattingRule rule;
|
||||
|
||||
/* cached values */
|
||||
private final CellRangeAddress[] regions;
|
||||
/**
|
||||
* Depending on the rule type, it may want to know about certain values in the region when evaluating {@link #matches(Cell)},
|
||||
* such as top 10, unique, duplicate, average, etc. This collection stores those if needed so they are not repeatedly calculated
|
||||
*/
|
||||
private final Map<CellRangeAddress, Set<ValueAndFormat>> meaningfulRegionValues = new HashMap<CellRangeAddress, Set<ValueAndFormat>>();
|
||||
|
||||
private final int priority;
|
||||
private final int formattingIndex;
|
||||
private final int ruleIndex;
|
||||
private final String formula1;
|
||||
private final String formula2;
|
||||
private final OperatorEnum operator;
|
||||
private final ConditionType type;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param workbookEvaluator
|
||||
* @param sheet
|
||||
* @param formatting
|
||||
* @param formattingIndex for priority, zero based
|
||||
* @param rule
|
||||
* @param ruleIndex for priority, zero based, if this is an HSSF rule. Unused for XSSF rules
|
||||
* @param regions could be read from formatting, but every call creates new objects in a new array.
|
||||
* this allows calling it once per formatting instance, and re-using the array.
|
||||
*/
|
||||
public EvaluationConditionalFormatRule(WorkbookEvaluator workbookEvaluator, Sheet sheet, ConditionalFormatting formatting, int formattingIndex, ConditionalFormattingRule rule, int ruleIndex, CellRangeAddress[] regions) {
|
||||
super();
|
||||
this.workbookEvaluator = workbookEvaluator;
|
||||
this.sheet = sheet;
|
||||
this.formatting = formatting;
|
||||
this.rule = rule;
|
||||
this.formattingIndex = formattingIndex;
|
||||
this.ruleIndex = ruleIndex;
|
||||
|
||||
this.priority = rule.getPriority();
|
||||
|
||||
this.regions = regions;
|
||||
formula1 = rule.getFormula1();
|
||||
formula2 = rule.getFormula2();
|
||||
|
||||
operator = OperatorEnum.values()[rule.getComparisonOperation()];
|
||||
type = rule.getConditionType();
|
||||
}
|
||||
|
||||
public Sheet getSheet() {
|
||||
return sheet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the formatting
|
||||
*/
|
||||
public ConditionalFormatting getFormatting() {
|
||||
return formatting;
|
||||
}
|
||||
|
||||
public int getFormattingIndex() {
|
||||
return formattingIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the rule
|
||||
*/
|
||||
public ConditionalFormattingRule getRule() {
|
||||
return rule;
|
||||
}
|
||||
|
||||
public int getRuleIndex() {
|
||||
return ruleIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the regions
|
||||
*/
|
||||
public CellRangeAddress[] getRegions() {
|
||||
return regions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the priority
|
||||
*/
|
||||
public int getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the formula1
|
||||
*/
|
||||
public String getFormula1() {
|
||||
return formula1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the formula2
|
||||
*/
|
||||
public String getFormula2() {
|
||||
return formula2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the operator
|
||||
*/
|
||||
public OperatorEnum getOperator() {
|
||||
return operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the type
|
||||
*/
|
||||
public ConditionType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defined as equal sheet name and formatting and rule indexes
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) return false;
|
||||
if (! obj.getClass().equals(this.getClass())) return false;
|
||||
final EvaluationConditionalFormatRule r = (EvaluationConditionalFormatRule) obj;
|
||||
return getSheet().getSheetName().equalsIgnoreCase(r.getSheet().getSheetName())
|
||||
&& getFormattingIndex() == r.getFormattingIndex()
|
||||
&& getRuleIndex() == r.getRuleIndex();
|
||||
}
|
||||
|
||||
/**
|
||||
* Per Excel Help, XSSF rule priority is sheet-wide, not just within the owning ConditionalFormatting object.
|
||||
* This can be seen by creating 4 rules applying to two different ranges and examining the XML.
|
||||
* <p/>
|
||||
* HSSF priority is based on definition/persistence order.
|
||||
*
|
||||
* @param o
|
||||
* @return comparison based on sheet name, formatting index, and rule priority
|
||||
*/
|
||||
public int compareTo(EvaluationConditionalFormatRule o) {
|
||||
int cmp = getSheet().getSheetName().compareToIgnoreCase(o.getSheet().getSheetName());
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
final int x = getPriority();
|
||||
final int y = o.getPriority();
|
||||
// logic from Integer.compare()
|
||||
cmp = (x < y) ? -1 : ((x == y) ? 0 : 1);
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
cmp = Integer.compare(getFormattingIndex(), o.getFormattingIndex());
|
||||
if (cmp != 0) return cmp;
|
||||
return Integer.compare(getRuleIndex(), o.getRuleIndex());
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
int hash = sheet.getSheetName().hashCode();
|
||||
hash = 31 * hash + formattingIndex;
|
||||
hash = 31 * hash + ruleIndex;
|
||||
return hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cell
|
||||
* @return true if this rule evaluates to true for the given cell
|
||||
*/
|
||||
/* package */ boolean matches(Cell cell) {
|
||||
// first check that it is in one of the regions defined for this format
|
||||
CellRangeAddress region = null;
|
||||
for (CellRangeAddress r : regions) {
|
||||
if (r.isInRange(cell)) {
|
||||
region = r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (region == null) return false; // cell not in range of this rule
|
||||
|
||||
final ConditionType ruleType = getRule().getConditionType();
|
||||
|
||||
// these rules apply to all cells in a region. Specific condition criteria
|
||||
// may specify no special formatting for that value partition, but that's display logic
|
||||
if (ruleType.equals(ConditionType.COLOR_SCALE)
|
||||
|| ruleType.equals(ConditionType.DATA_BAR)
|
||||
|| ruleType.equals(ConditionType.ICON_SET)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ruleType.equals(ConditionType.CELL_VALUE_IS)) {
|
||||
return checkValue(cell, region);
|
||||
}
|
||||
if (ruleType.equals(ConditionType.FORMULA)) {
|
||||
return checkFormula(cell, region);
|
||||
}
|
||||
if (ruleType.equals(ConditionType.FILTER)) {
|
||||
return checkFilter(cell, region);
|
||||
}
|
||||
|
||||
// TODO: anything else, we don't handle yet, such as top 10
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cell
|
||||
* @param region for adjusting relative formulas
|
||||
* @return
|
||||
*/
|
||||
private boolean checkValue(Cell cell, CellRangeAddress region) {
|
||||
if (cell == null || DataValidationEvaluator.isType(cell, CellType.BLANK)
|
||||
|| DataValidationEvaluator.isType(cell,CellType.ERROR)
|
||||
|| (DataValidationEvaluator.isType(cell,CellType.STRING)
|
||||
&& (cell.getStringCellValue() == null || cell.getStringCellValue().isEmpty())
|
||||
)
|
||||
) return false;
|
||||
|
||||
ValueEval eval = unwrapEval(workbookEvaluator.evaluate(rule.getFormula1(), ConditionalFormattingEvaluator.getRef(cell), region));
|
||||
|
||||
String f2 = rule.getFormula2();
|
||||
ValueEval eval2 = null;
|
||||
if (f2 != null && f2.length() > 0) {
|
||||
eval2 = unwrapEval(workbookEvaluator.evaluate(f2, ConditionalFormattingEvaluator.getRef(cell), region));
|
||||
}
|
||||
|
||||
// we assume the cell has been evaluated, and the current formula value stored
|
||||
if (DataValidationEvaluator.isType(cell, CellType.BOOLEAN)) {
|
||||
if (eval instanceof BoolEval && (eval2 == null || eval2 instanceof BoolEval) ) {
|
||||
return operator.isValid(cell.getBooleanCellValue(), ((BoolEval) eval).getBooleanValue(), eval2 == null ? null : ((BoolEval) eval2).getBooleanValue());
|
||||
}
|
||||
return false; // wrong types
|
||||
}
|
||||
if (DataValidationEvaluator.isType(cell, CellType.NUMERIC)) {
|
||||
if (eval instanceof NumberEval && (eval2 == null || eval2 instanceof NumberEval) ) {
|
||||
return operator.isValid(cell.getNumericCellValue(), ((NumberEval) eval).getNumberValue(), eval2 == null ? null : ((NumberEval) eval2).getNumberValue());
|
||||
}
|
||||
return false; // wrong types
|
||||
}
|
||||
if (DataValidationEvaluator.isType(cell, CellType.STRING)) {
|
||||
if (eval instanceof StringEval && (eval2 == null || eval2 instanceof StringEval) ) {
|
||||
return operator.isValid(cell.getStringCellValue(), ((StringEval) eval).getStringValue(), eval2 == null ? null : ((StringEval) eval2).getStringValue());
|
||||
}
|
||||
return false; // wrong types
|
||||
}
|
||||
|
||||
// should not get here, but in case...
|
||||
return false;
|
||||
}
|
||||
|
||||
private ValueEval unwrapEval(ValueEval eval) {
|
||||
ValueEval comp = eval;
|
||||
|
||||
while (comp instanceof RefEval) {
|
||||
RefEval ref = (RefEval) comp;
|
||||
comp = ref.getInnerValueEval(ref.getFirstSheetIndex());
|
||||
}
|
||||
return comp;
|
||||
}
|
||||
/**
|
||||
* @param cell needed for offsets from region anchor
|
||||
* @param region for adjusting relative formulas
|
||||
* @return true/false using the same rules as Data Validation evaluations
|
||||
*/
|
||||
private boolean checkFormula(Cell cell, CellRangeAddress region) {
|
||||
ValueEval comp = unwrapEval(workbookEvaluator.evaluate(rule.getFormula1(), ConditionalFormattingEvaluator.getRef(cell), region));
|
||||
|
||||
// Copied for now from DataValidationEvaluator.ValidationEnum.FORMULA#isValidValue()
|
||||
if (comp instanceof BlankEval) return true;
|
||||
if (comp instanceof ErrorEval) return false;
|
||||
if (comp instanceof BoolEval) {
|
||||
return ((BoolEval) comp).getBooleanValue();
|
||||
}
|
||||
// empirically tested in Excel - 0=false, any other number = true/valid
|
||||
// see test file DataValidationEvaluations.xlsx
|
||||
if (comp instanceof NumberEval) {
|
||||
return ((NumberEval) comp).getNumberValue() != 0;
|
||||
}
|
||||
return false; // anything else is false, such as text
|
||||
}
|
||||
|
||||
private boolean checkFilter(Cell cell, CellRangeAddress region) {
|
||||
final ConditionFilterType filterType = rule.getConditionFilterType();
|
||||
if (filterType == null) return false;
|
||||
|
||||
// TODO: this could/should be delegated to the Enum type, but that's in the usermodel package,
|
||||
// we may not want evaluation code there. Of course, maybe the enum should go here in formula,
|
||||
// and not be returned by the SS model, but then we need the XSSF rule to expose the raw OOXML
|
||||
// type value, which isn't ideal either.
|
||||
switch (filterType) {
|
||||
case FILTER:
|
||||
return false; // we don't evaluate HSSF filters yet
|
||||
case TOP_10:
|
||||
// from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range.
|
||||
// numbers stored as text are ignored, but numbers formatted as text are treated as numbers.
|
||||
|
||||
final ValueAndFormat cv10 = getCellValue(cell);
|
||||
if (! cv10.isNumber()) return false;
|
||||
|
||||
return getMeaningfulValues(region, false, new ValueFunction() {
|
||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
|
||||
List<ValueAndFormat> values = allValues;
|
||||
final ConditionFilterData conf = rule.getFilterConfiguration();
|
||||
|
||||
if (! conf.getBottom()) Collections.sort(values, Collections.reverseOrder());
|
||||
else Collections.sort(values);
|
||||
|
||||
int limit = (int) conf.getRank();
|
||||
if (conf.getPercent()) limit = allValues.size() * limit / 100;
|
||||
if (allValues.size() <= limit) return new HashSet<ValueAndFormat>(allValues);
|
||||
|
||||
return new HashSet<ValueAndFormat>(allValues.subList(0, limit));
|
||||
}
|
||||
}).contains(cv10);
|
||||
case UNIQUE_VALUES:
|
||||
// Per Excel help, "duplicate" means matching value AND format
|
||||
// https://support.office.com/en-us/article/Filter-for-unique-values-or-remove-duplicate-values-ccf664b0-81d6-449b-bbe1-8daaec1e83c2
|
||||
return getMeaningfulValues(region, true, new ValueFunction() {
|
||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
|
||||
List<ValueAndFormat> values = allValues;
|
||||
Collections.sort(values);
|
||||
|
||||
final Set<ValueAndFormat> unique = new HashSet<ValueAndFormat>();
|
||||
|
||||
for (int i=0; i < values.size(); i++) {
|
||||
final ValueAndFormat v = values.get(i);
|
||||
// skip this if the current value matches the next one, or is the last one and matches the previous one
|
||||
if ( (i < values.size()-1 && v.equals(values.get(i+1)) ) || ( i > 0 && i == values.size()-1 && v.equals(values.get(i-1)) ) ) {
|
||||
// current value matches next value, skip both
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
unique.add(v);
|
||||
}
|
||||
|
||||
return unique;
|
||||
}
|
||||
}).contains(getCellValue(cell));
|
||||
case DUPLICATE_VALUES:
|
||||
// Per Excel help, "duplicate" means matching value AND format
|
||||
// https://support.office.com/en-us/article/Filter-for-unique-values-or-remove-duplicate-values-ccf664b0-81d6-449b-bbe1-8daaec1e83c2
|
||||
return getMeaningfulValues(region, true, new ValueFunction() {
|
||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
|
||||
List<ValueAndFormat> values = allValues;
|
||||
Collections.sort(values);
|
||||
|
||||
final Set<ValueAndFormat> dup = new HashSet<ValueAndFormat>();
|
||||
|
||||
for (int i=0; i < values.size(); i++) {
|
||||
final ValueAndFormat v = values.get(i);
|
||||
// skip this if the current value matches the next one, or is the last one and matches the previous one
|
||||
if ( (i < values.size()-1 && v.equals(values.get(i+1)) ) || ( i > 0 && i == values.size()-1 && v.equals(values.get(i-1)) ) ) {
|
||||
// current value matches next value, add one
|
||||
dup.add(v);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
return dup;
|
||||
}
|
||||
}).contains(getCellValue(cell));
|
||||
case ABOVE_AVERAGE:
|
||||
// from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range.
|
||||
// numbers stored as text are ignored, but numbers formatted as text are treated as numbers.
|
||||
|
||||
final ConditionFilterData conf = rule.getFilterConfiguration();
|
||||
|
||||
// actually ordered, so iteration order is predictable
|
||||
List<ValueAndFormat> values = new ArrayList<ValueAndFormat>(getMeaningfulValues(region, false, new ValueFunction() {
|
||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) {
|
||||
List<ValueAndFormat> values = allValues;
|
||||
double total = 0;
|
||||
ValueEval[] pop = new ValueEval[values.size()];
|
||||
for (int i=0; i < values.size(); i++) {
|
||||
ValueAndFormat v = values.get(i);
|
||||
total += v.value.doubleValue();
|
||||
pop[i] = new NumberEval(v.value.doubleValue());
|
||||
}
|
||||
|
||||
final Set<ValueAndFormat> avgSet = new LinkedHashSet<ValueAndFormat>(1);
|
||||
avgSet.add(new ValueAndFormat(new Double(values.size() == 0 ? 0 : total / values.size()), null));
|
||||
|
||||
final double stdDev = values.size() <= 1 ? 0 : ((NumberEval) AggregateFunction.STDEV.evaluate(pop, 0, 0)).getNumberValue();
|
||||
avgSet.add(new ValueAndFormat(new Double(stdDev), null));
|
||||
return avgSet;
|
||||
}
|
||||
}));
|
||||
|
||||
final ValueAndFormat cv = getCellValue(cell);
|
||||
Double val = cv.isNumber() ? cv.getValue() : null;
|
||||
if (val == null) return false;
|
||||
|
||||
double avg = values.get(0).value.doubleValue();
|
||||
double stdDev = values.get(1).value.doubleValue();
|
||||
|
||||
/*
|
||||
* use StdDev, aboveAverage, equalAverage to find:
|
||||
* comparison value
|
||||
* operator type
|
||||
*/
|
||||
|
||||
Double comp = new Double(conf.getStdDev() > 0 ? (avg + (conf.getAboveAverage() ? 1 : -1) * stdDev * conf.getStdDev()) : avg) ;
|
||||
|
||||
OperatorEnum op = null;
|
||||
if (conf.getAboveAverage()) {
|
||||
if (conf.getEqualAverage()) op = OperatorEnum.GREATER_OR_EQUAL;
|
||||
else op = OperatorEnum.GREATER_THAN;
|
||||
} else {
|
||||
if (conf.getEqualAverage()) op = OperatorEnum.LESS_OR_EQUAL;
|
||||
else op = OperatorEnum.LESS_THAN;
|
||||
}
|
||||
return op != null && op.isValid(val, comp, null);
|
||||
case CONTAINS_TEXT:
|
||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula.
|
||||
return checkFormula(cell, region);
|
||||
case NOT_CONTAINS_TEXT:
|
||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula.
|
||||
return checkFormula(cell, region);
|
||||
case BEGINS_WITH:
|
||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula.
|
||||
return checkFormula(cell, region);
|
||||
case ENDS_WITH:
|
||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula.
|
||||
return checkFormula(cell, region);
|
||||
case CONTAINS_BLANKS:
|
||||
try {
|
||||
String v = cell.getStringCellValue();
|
||||
// see TextFunction.TRIM for implementation
|
||||
return v == null || v.trim().length() == 0;
|
||||
} catch (Exception e) {
|
||||
// not a valid string value, and not a blank cell (that's checked earlier)
|
||||
return false;
|
||||
}
|
||||
case NOT_CONTAINS_BLANKS:
|
||||
try {
|
||||
String v = cell.getStringCellValue();
|
||||
// see TextFunction.TRIM for implementation
|
||||
return v != null && v.trim().length() > 0;
|
||||
} catch (Exception e) {
|
||||
// not a valid string value, but not blank
|
||||
return true;
|
||||
}
|
||||
case CONTAINS_ERRORS:
|
||||
return cell != null && DataValidationEvaluator.isType(cell, CellType.ERROR);
|
||||
case NOT_CONTAINS_ERRORS:
|
||||
return cell == null || ! DataValidationEvaluator.isType(cell, CellType.ERROR);
|
||||
case TIME_PERIOD:
|
||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula.
|
||||
return checkFormula(cell, region);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range.
|
||||
* numbers stored as text are ignored, but numbers formatted as text are treated as numbers.
|
||||
*
|
||||
* @param region
|
||||
* @return
|
||||
*/
|
||||
private Set<ValueAndFormat> getMeaningfulValues(CellRangeAddress region, boolean withText, ValueFunction func) {
|
||||
Set<ValueAndFormat> values = meaningfulRegionValues.get(region);
|
||||
if (values != null) return values;
|
||||
|
||||
List<ValueAndFormat> allValues = new ArrayList<ValueAndFormat>((region.getLastColumn() - region.getFirstColumn()+1) * (region.getLastRow() - region.getFirstRow() + 1));
|
||||
|
||||
for (int r=region.getFirstRow(); r <= region.getLastRow(); r++) {
|
||||
final Row row = sheet.getRow(r);
|
||||
if (row == null) continue;
|
||||
for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) {
|
||||
Cell cell = row.getCell(c);
|
||||
final ValueAndFormat cv = getCellValue(cell);
|
||||
if (cv != null && (withText || cv.isNumber()) ) allValues.add(cv);
|
||||
}
|
||||
}
|
||||
|
||||
values = func.evaluate(allValues);
|
||||
meaningfulRegionValues.put(region, values);
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
private ValueAndFormat getCellValue(Cell cell) {
|
||||
if (cell != null) {
|
||||
final CellType type = cell.getCellTypeEnum();
|
||||
if (type == CellType.NUMERIC || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.NUMERIC) ) {
|
||||
return new ValueAndFormat(new Double(cell.getNumericCellValue()), cell.getCellStyle().getDataFormatString());
|
||||
} else if (type == CellType.STRING || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.STRING) ) {
|
||||
return new ValueAndFormat(cell.getStringCellValue(), cell.getCellStyle().getDataFormatString());
|
||||
} else if (type == CellType.BOOLEAN || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.BOOLEAN) ) {
|
||||
return new ValueAndFormat(cell.getStringCellValue(), cell.getCellStyle().getDataFormatString());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* instances evaluate the values for a region and return the positive matches for the function type.
|
||||
* TODO: when we get to use Java 8, this is obviously a Lambda Function.
|
||||
*/
|
||||
protected interface ValueFunction {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param values
|
||||
* @return the desired values for the rules implemented by the current instance
|
||||
*/
|
||||
Set<ValueAndFormat> evaluate(List<ValueAndFormat> values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Not calling it OperatorType to avoid confusion for now with other classes.
|
||||
* Definition order matches OOXML type ID indexes.
|
||||
* Note that this has NO_COMPARISON as the first item, unlike the similar
|
||||
* DataValidation operator enum. Thanks, Microsoft.
|
||||
*/
|
||||
public static enum OperatorEnum {
|
||||
NO_COMPARISON {
|
||||
/** always false/invalid */
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
BETWEEN {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
return cellValue.compareTo(v1) >= 0 && cellValue.compareTo(v2) <= 0;
|
||||
}
|
||||
},
|
||||
NOT_BETWEEN {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
return cellValue.compareTo(v1) < 0 || cellValue.compareTo(v2) > 0;
|
||||
}
|
||||
},
|
||||
EQUAL {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
if (cellValue instanceof String) {
|
||||
return ((String) cellValue).compareToIgnoreCase((String) v1) == 0;
|
||||
}
|
||||
return cellValue.compareTo(v1) == 0;
|
||||
}
|
||||
},
|
||||
NOT_EQUAL {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
if (cellValue instanceof String) {
|
||||
return ((String) cellValue).compareToIgnoreCase((String) v1) != 0;
|
||||
}
|
||||
return cellValue.compareTo(v1) != 0;
|
||||
}
|
||||
},
|
||||
GREATER_THAN {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
return cellValue.compareTo(v1) > 0;
|
||||
}
|
||||
},
|
||||
LESS_THAN {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
return cellValue.compareTo(v1) < 0;
|
||||
}
|
||||
},
|
||||
GREATER_OR_EQUAL {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
return cellValue.compareTo(v1) >= 0;
|
||||
}
|
||||
},
|
||||
LESS_OR_EQUAL {
|
||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) {
|
||||
return cellValue.compareTo(v1) <= 0;
|
||||
}
|
||||
},
|
||||
;
|
||||
|
||||
/**
|
||||
* Evaluates comparison using operator instance rules
|
||||
* @param cellValue won't be null, assumption is previous checks handled that
|
||||
* @param v1 if null, value assumed invalid, anything passes, per Excel behavior
|
||||
* @param v2 null if not needed. If null when needed, assume anything passes, per Excel behavior
|
||||
* @return true if the comparison is valid
|
||||
*/
|
||||
public abstract <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this class has a natural ordering that is inconsistent with equals.
|
||||
*/
|
||||
protected class ValueAndFormat implements Comparable<ValueAndFormat> {
|
||||
|
||||
private final Double value;
|
||||
private final String string;
|
||||
private final String format;
|
||||
|
||||
public ValueAndFormat(Double value, String format) {
|
||||
this.value = value;
|
||||
this.format = format;
|
||||
string = null;
|
||||
}
|
||||
|
||||
public ValueAndFormat(String value, String format) {
|
||||
this.value = null;
|
||||
this.format = format;
|
||||
string = value;
|
||||
}
|
||||
|
||||
public boolean isNumber() {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
public Double getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public boolean equals(Object obj) {
|
||||
ValueAndFormat o = (ValueAndFormat) obj;
|
||||
return ( value == o.value || value.equals(o.value))
|
||||
&& ( format == o.format || format.equals(o.format))
|
||||
&& (string == o.string || string.equals(o.string));
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: this class has a natural ordering that is inconsistent with equals.
|
||||
* @param o
|
||||
* @return value comparison
|
||||
*/
|
||||
public int compareTo(ValueAndFormat o) {
|
||||
if (value == null && o.value != null) return 1;
|
||||
if (o.value == null && value != null) return -1;
|
||||
int cmp = value == null ? 0 : value.compareTo(o.value);
|
||||
if (cmp != 0) return cmp;
|
||||
|
||||
if (string == null && o.string != null) return 1;
|
||||
if (o.string == null && string != null) return -1;
|
||||
|
||||
return string == null ? 0 : string.compareTo(o.string);
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return (string == null ? 0 : string.hashCode()) * 37 * 37 + 37 * (value == null ? 0 : value.hashCode()) + (format == null ? 0 : format.hashCode());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -785,6 +785,7 @@ public final class FormulaParser {
|
|||
actualEndRow = _rowIndex;
|
||||
} else { // Really no special quantifiers
|
||||
actualStartRow++;
|
||||
if (tbl.isHasTotalsRow()) actualEndRow--;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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.
|
||||
* <p/>
|
||||
* Instance names match the constants from <code>STCfType</code> for convenience.
|
||||
*/
|
||||
public enum ConditionFilterType {
|
||||
/** This is the only value valid for HSSF rules */
|
||||
FILTER,
|
||||
TOP_10,
|
||||
UNIQUE_VALUES,
|
||||
DUPLICATE_VALUES,
|
||||
CONTAINS_TEXT,
|
||||
NOT_CONTAINS_TEXT,
|
||||
BEGINS_WITH,
|
||||
ENDS_WITH,
|
||||
CONTAINS_BLANKS,
|
||||
NOT_CONTAINS_BLANKS,
|
||||
CONTAINS_ERRORS,
|
||||
NOT_CONTAINS_ERRORS,
|
||||
TIME_PERIOD,
|
||||
ABOVE_AVERAGE,
|
||||
;
|
||||
}
|
|
@ -83,6 +83,32 @@ public interface ConditionalFormattingRule {
|
|||
* @return the type of condition
|
||||
*/
|
||||
ConditionType getConditionType();
|
||||
|
||||
/**
|
||||
* This is null if
|
||||
* <p/>
|
||||
* <code>{@link #getConditionType()} != {@link ConditionType#FILTER}</code>
|
||||
* <p/>
|
||||
* This is always {@link ConditionFilterType#FILTER} for HSSF rules of type {@link ConditionType#FILTER}.
|
||||
* <p/>
|
||||
* For XSSF filter rules, this will indicate the specific type of filter.
|
||||
*
|
||||
* @return filter type for filter rules, or null if not a filter rule.
|
||||
*/
|
||||
ConditionFilterType getConditionFilterType();
|
||||
|
||||
/**
|
||||
* This is null if
|
||||
* <p/>
|
||||
* <code>{@link #getConditionFilterType()} == null</code>
|
||||
* <p/>
|
||||
* This means it is always null for HSSF, which does not define the extended condition types.
|
||||
* <p/>
|
||||
* This object contains the additional configuration information for XSSF filter conditions.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ConditionFilterData getFilterConfiguration();
|
||||
|
||||
/**
|
||||
* The comparison function used when the type of conditional formatting is set to
|
||||
|
@ -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.
|
||||
* <p/>
|
||||
* If priority is 0, just use definition order, as that's how HSSF rules are evaluated.
|
||||
* <p/>
|
||||
* If a rule is created but not yet added to a sheet, this value may not be valid.
|
||||
|
||||
* @return rule priority
|
||||
*/
|
||||
int getPriority();
|
||||
|
||||
/**
|
||||
* Always true for HSSF rules, optional flag for XSSF rules.
|
||||
* See Excel help for more.
|
||||
*
|
||||
* @return true if conditional formatting rule processing stops when this one is true, false if not
|
||||
* @see <a href="https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-063cde21-516e-45ca-83f5-8e8126076249">Microsoft Excel help</a>
|
||||
*/
|
||||
boolean getStopIfTrue();
|
||||
}
|
||||
|
|
|
@ -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 <code>null</code>.
|
||||
*/
|
||||
BorderFormatting createBorderFormatting();
|
||||
|
||||
/**
|
||||
* @return - border formatting object if defined, <code>null</code> 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 <code>null</code>.
|
||||
*/
|
||||
FontFormatting createFontFormatting();
|
||||
|
||||
/**
|
||||
* @return - font formatting object if defined, <code>null</code> 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 <code>null</code>.
|
||||
*/
|
||||
PatternFormatting createPatternFormatting();
|
||||
|
||||
/**
|
||||
* @return - pattern formatting object if defined, <code>null</code> otherwise
|
||||
*/
|
||||
PatternFormatting getPatternFormatting();
|
||||
|
||||
/**
|
||||
* @return - databar / data-bar formatting object if defined, <code>null</code> otherwise
|
||||
*/
|
||||
DataBarFormatting getDataBarFormatting();
|
||||
|
||||
/**
|
||||
* @return - icon / multi-state formatting object if defined, <code>null</code> otherwise
|
||||
*/
|
||||
IconMultiStateFormatting getMultiStateFormatting();
|
||||
|
||||
/**
|
||||
* @return color scale / color grate formatting object if defined, <code>null</code> otherwise
|
||||
*/
|
||||
ColorScaleFormatting getColorScaleFormatting();
|
||||
|
||||
/**
|
||||
* Type of conditional formatting rule.
|
||||
*
|
||||
* @return the type of condition
|
||||
*/
|
||||
ConditionType getConditionType();
|
||||
|
||||
/**
|
||||
* This is null if
|
||||
* <p/>
|
||||
* <code>{@link #getConditionType()} != {@link ConditionType#FILTER}</code>
|
||||
* <p/>
|
||||
* This is always {@link ConditionFilterType#FILTER} for HSSF rules of type {@link ConditionType#FILTER}.
|
||||
* <p/>
|
||||
* For XSSF filter rules, this will indicate the specific type of filter.
|
||||
*
|
||||
* @return filter type for filter rules, or null if not a filter rule.
|
||||
*/
|
||||
ConditionFilterType getConditionFilterType();
|
||||
|
||||
/**
|
||||
* This is null if
|
||||
* <p/>
|
||||
* <code>{@link #getConditionFilterType()} == null</code>
|
||||
* <p/>
|
||||
* This means it is always null for HSSF, which does not define the extended condition types.
|
||||
* <p/>
|
||||
* This object contains the additional configuration information for XSSF filter conditions.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ConditionFilterData getFilterConfiguration();
|
||||
|
||||
/**
|
||||
* The comparison function used when the type of conditional formatting is set to
|
||||
* {@link ConditionType#CELL_VALUE_IS}
|
||||
* <p>
|
||||
* MUST be a constant from {@link ComparisonOperator}
|
||||
* </p>
|
||||
*
|
||||
* @return the conditional format operator
|
||||
*/
|
||||
byte getComparisonOperation();
|
||||
|
||||
/**
|
||||
* The formula used to evaluate the first operand for the conditional formatting rule.
|
||||
* <p>
|
||||
* 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.
|
||||
* </p>
|
||||
* <p>
|
||||
* If comparison type is {@link ConditionType#FORMULA} the formula MUST be a Boolean function
|
||||
* </p>
|
||||
*
|
||||
* @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.
|
||||
* <p/>
|
||||
* If priority is 0, just use definition order, as that's how HSSF rules are evaluated.
|
||||
* <p/>
|
||||
* If a rule is created but not yet added to a sheet, this value may not be valid.
|
||||
|
||||
* @return rule priority
|
||||
*/
|
||||
int getPriority();
|
||||
|
||||
/**
|
||||
* Always true for HSSF rules, optional flag for XSSF rules.
|
||||
* See Excel help for more.
|
||||
*
|
||||
* @return true if conditional formatting rule processing stops when this one is true, false if not
|
||||
* @see <a href="https://support.office.com/en-us/article/Manage-conditional-formatting-rule-precedence-063cde21-516e-45ca-83f5-8e8126076249">Microsoft Excel help</a>
|
||||
*/
|
||||
boolean getStopIfTrue();
|
||||
}
|
|
@ -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.
|
||||
* <p/>NOTE: It is up to the caller to ensure the reference is
|
||||
* for the correct sheet, since this instance doesn't have a sheet reference.
|
||||
*
|
||||
* @param ref the CellReference to check
|
||||
* @return True if the reference lies within the bounds, false otherwise.
|
||||
* @see #intersects(CellRangeAddressBase) for checking if two ranges overlap
|
||||
*/
|
||||
public boolean isInRange(CellReference ref) {
|
||||
return isInRange(ref.getRow(), ref.getCol());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given {@link Cell} lies within the bounds
|
||||
* of this range.
|
||||
* <p/>NOTE: It is up to the caller to ensure the reference is
|
||||
* for the correct sheet, since this instance doesn't have a sheet reference.
|
||||
*
|
||||
* @param cell the Cell to check
|
||||
* @return True if the cell lies within the bounds, false otherwise.
|
||||
* @see #intersects(CellRangeAddressBase) for checking if two ranges overlap
|
||||
*/
|
||||
public boolean isInRange(Cell cell) {
|
||||
return isInRange(cell.getRowIndex(), cell.getColumnIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the row is in the specified cell range
|
||||
*
|
||||
|
|
|
@ -351,6 +351,29 @@ public class SheetUtil {
|
|||
return cr.isInRange(rowIx, colIx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cell, without taking account of merged regions.
|
||||
* <p/>
|
||||
* Use {@link #getCellWithMerges(Sheet, int, int)} if you want the top left
|
||||
* cell from merged regions instead when the reference is a merged cell.
|
||||
* <p/>
|
||||
* Use this where you want to know if the given cell is explicitly defined
|
||||
* or not.
|
||||
*
|
||||
* @param sheet
|
||||
* @param rowIx
|
||||
* @param colIx
|
||||
* @return cell at the given location, or null if not defined
|
||||
* @throws NullPointerException if sheet is null
|
||||
*/
|
||||
public static Cell getCell(Sheet sheet, int rowIx, int colIx) {
|
||||
Row r = sheet.getRow(rowIx);
|
||||
if (r != null) {
|
||||
return r.getCell(colIx);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the cell, taking account of merged regions. Allows you to find the
|
||||
* cell who's contents are shown in a given position in the sheet.
|
||||
|
@ -361,22 +384,22 @@ public class SheetUtil {
|
|||
* then will return the cell itself.
|
||||
* <p>If there is no cell defined at the given co-ordinates, will return
|
||||
* null.
|
||||
*
|
||||
* @param sheet
|
||||
* @param rowIx
|
||||
* @param colIx
|
||||
* @return cell at the given location, its base merged cell, or null if not defined
|
||||
* @throws NullPointerException if sheet is null
|
||||
*/
|
||||
public static Cell getCellWithMerges(Sheet sheet, int rowIx, int colIx) {
|
||||
Row r = sheet.getRow(rowIx);
|
||||
if (r != null) {
|
||||
Cell c = r.getCell(colIx);
|
||||
if (c != null) {
|
||||
// Normal, non-merged cell
|
||||
return c;
|
||||
}
|
||||
}
|
||||
final Cell c = getCell(sheet, rowIx, colIx);
|
||||
if (c != null) return c;
|
||||
|
||||
for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) {
|
||||
if (mergedRegion.isInRange(rowIx, colIx)) {
|
||||
// The cell wanted is in this merged range
|
||||
// Return the primary (top-left) cell for the range
|
||||
r = sheet.getRow(mergedRegion.getFirstRow());
|
||||
Row r = sheet.getRow(mergedRegion.getFirstRow());
|
||||
if (r != null) {
|
||||
return r.getCell(mergedRegion.getFirstColumn());
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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<STCfType.Enum, ConditionType> typeLookup = new HashMap<STCfType.Enum, ConditionType>();
|
||||
private static Map<STCfType.Enum, ConditionFilterType> filterTypeLookup = new HashMap<STCfType.Enum, ConditionFilterType>();
|
||||
static {
|
||||
typeLookup.put(STCfType.CELL_IS, ConditionType.CELL_VALUE_IS);
|
||||
typeLookup.put(STCfType.EXPRESSION, ConditionType.FORMULA);
|
||||
|
@ -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}
|
||||
|
|
|
@ -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<XSSFCell> cr = getCellRange(range);
|
||||
for (XSSFCell c : cr) {
|
||||
|
|
|
@ -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<EvaluationConditionalFormatRule> rules;
|
||||
|
||||
@Before
|
||||
public void openWB() {
|
||||
wb = XSSFTestDataSamples.openSampleWorkbook("ConditionalFormattingSamples.xlsx");
|
||||
formulaEval = new XSSFFormulaEvaluator(wb);
|
||||
cfe = new ConditionalFormattingEvaluator(wb, formulaEval);
|
||||
}
|
||||
|
||||
@After
|
||||
public void closeWB() {
|
||||
formulaEval = null;
|
||||
cfe = null;
|
||||
ref = null;
|
||||
rules = null;
|
||||
try {
|
||||
if (wb != null) wb.close();
|
||||
} catch (IOException e) {
|
||||
// keep going, this shouldn't cancel things
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormattingEvaluation() {
|
||||
sheet = wb.getSheet("Products1");
|
||||
|
||||
getRulesFor(12, 1);
|
||||
assertEquals("wrong # of rules for " + ref, 1, rules.size());
|
||||
assertEquals("wrong bg color for " + ref, "FFFFEB9C", getColor(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()));
|
||||
assertFalse("should not be italic " + ref, rules.get(0).getRule().getFontFormatting().isItalic());
|
||||
|
||||
getRulesFor(16, 3);
|
||||
assertEquals("wrong # of rules for " + ref, 1, rules.size());
|
||||
assertEquals("wrong bg color for " + ref, 0.7999816888943144d, getTint(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()), 0.000000000000001);
|
||||
|
||||
getRulesFor(12, 3);
|
||||
assertEquals("wrong # of rules for " + ref, 0, rules.size());
|
||||
|
||||
sheet = wb.getSheet("Products2");
|
||||
|
||||
getRulesFor(15,1);
|
||||
assertEquals("wrong # of rules for " + ref, 1, rules.size());
|
||||
assertEquals("wrong bg color for " + ref, "FFFFEB9C", getColor(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()));
|
||||
|
||||
getRulesFor(20,3);
|
||||
assertEquals("wrong # of rules for " + ref, 0, rules.size());
|
||||
|
||||
// now change a cell value that's an input for the rules
|
||||
Cell cell = sheet.getRow(1).getCell(6);
|
||||
cell.setCellValue("Dairy");
|
||||
formulaEval.notifyUpdateCell(cell);
|
||||
cell = sheet.getRow(4).getCell(6);
|
||||
cell.setCellValue(500);
|
||||
formulaEval.notifyUpdateCell(cell);
|
||||
// need to throw away all evaluations, since we don't know how value changes may have affected format formulas
|
||||
cfe.clearAllCachedValues();
|
||||
|
||||
// test that the conditional validation evaluations changed
|
||||
getRulesFor(15,1);
|
||||
assertEquals("wrong # of rules for " + ref, 0, rules.size());
|
||||
|
||||
getRulesFor(20,3);
|
||||
assertEquals("wrong # of rules for " + ref, 1, rules.size());
|
||||
assertEquals("wrong bg color for " + ref, 0.7999816888943144d, getTint(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()), 0.000000000000001);
|
||||
|
||||
getRulesFor(20,1);
|
||||
assertEquals("wrong # of rules for " + ref, 1, rules.size());
|
||||
assertEquals("wrong bg color for " + ref, "FFFFEB9C", getColor(rules.get(0).getRule().getPatternFormatting().getFillBackgroundColorColor()));
|
||||
|
||||
sheet = wb.getSheet("Book tour");
|
||||
|
||||
getRulesFor(8,2);
|
||||
assertEquals("wrong # of rules for " + ref, 1, rules.size());
|
||||
|
||||
}
|
||||
|
||||
private List<EvaluationConditionalFormatRule> getRulesFor(int row, int col) {
|
||||
ref = new CellReference(sheet.getSheetName(), row, col, false, false);
|
||||
return rules = cfe.getConditionalFormattingForCell(ref);
|
||||
}
|
||||
|
||||
private String getColor(Color color) {
|
||||
final XSSFColor c = XSSFColor.toXSSFColor(color);
|
||||
return c.getARGBHex();
|
||||
}
|
||||
|
||||
private double getTint(Color color) {
|
||||
final XSSFColor c = XSSFColor.toXSSFColor(color);
|
||||
return c.getTint();
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in New Issue