git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1782894 13f79535-47bb-0310-9956-ffa450edef68tags/REL_3_16_FINAL
import org.apache.poi.hssf.record.cf.IconMultiStateFormatting; | import org.apache.poi.hssf.record.cf.IconMultiStateFormatting; | ||||
import org.apache.poi.hssf.record.cf.PatternFormatting; | import org.apache.poi.hssf.record.cf.PatternFormatting; | ||||
import org.apache.poi.ss.formula.ptg.Ptg; | 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.ConditionType; | ||||
import org.apache.poi.ss.usermodel.ConditionalFormattingRule; | import org.apache.poi.ss.usermodel.ConditionalFormattingRule; | ||||
cfRuleRecord = pRuleRecord; | 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() { | CFRuleBase getCfRuleRecord() { | ||||
return cfRuleRecord; | return cfRuleRecord; | ||||
} | } | ||||
return ConditionType.forId(code); | 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 | * @return - the comparisionoperatation for the cfrule | ||||
*/ | */ |
/* ==================================================================== | |||||
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); | |||||
} | |||||
} |
/* ==================================================================== | |||||
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(); | |||||
} | |||||
} | |||||
} |
/* ==================================================================== | |||||
Licensed to the Apache Software Foundation (ASF) under one or more | |||||
contributor license agreements. See the NOTICE file distributed with | |||||
this work for additional information regarding copyright ownership. | |||||
The ASF licenses this file to You under the Apache License, Version 2.0 | |||||
(the "License"); you may not use this file except in compliance with | |||||
the License. You may obtain a copy of the License at | |||||
http://www.apache.org/licenses/LICENSE-2.0 | |||||
Unless required by applicable law or agreed to in writing, software | |||||
distributed under the License is distributed on an "AS IS" BASIS, | |||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
See the License for the specific language governing permissions and | |||||
limitations under the License. | |||||
==================================================================== */ | |||||
package org.apache.poi.ss.formula; | |||||
import java.util.ArrayList; | |||||
import java.util.Collections; | |||||
import java.util.HashMap; | |||||
import java.util.HashSet; | |||||
import java.util.LinkedHashSet; | |||||
import java.util.List; | |||||
import java.util.Map; | |||||
import java.util.Set; | |||||
import org.apache.poi.ss.formula.eval.BlankEval; | |||||
import org.apache.poi.ss.formula.eval.BoolEval; | |||||
import org.apache.poi.ss.formula.eval.ErrorEval; | |||||
import org.apache.poi.ss.formula.eval.NumberEval; | |||||
import org.apache.poi.ss.formula.eval.RefEval; | |||||
import org.apache.poi.ss.formula.eval.StringEval; | |||||
import org.apache.poi.ss.formula.eval.ValueEval; | |||||
import org.apache.poi.ss.formula.functions.AggregateFunction; | |||||
import org.apache.poi.ss.usermodel.Cell; | |||||
import org.apache.poi.ss.usermodel.CellType; | |||||
import org.apache.poi.ss.usermodel.ConditionFilterData; | |||||
import org.apache.poi.ss.usermodel.ConditionFilterType; | |||||
import org.apache.poi.ss.usermodel.ConditionType; | |||||
import org.apache.poi.ss.usermodel.ConditionalFormatting; | |||||
import org.apache.poi.ss.usermodel.ConditionalFormattingRule; | |||||
import org.apache.poi.ss.usermodel.Row; | |||||
import org.apache.poi.ss.usermodel.Sheet; | |||||
import org.apache.poi.ss.util.CellRangeAddress; | |||||
/** | |||||
* Abstracted and cached version of a Conditional Format rule for use with a | |||||
* {@link ConditionalFormattingEvaluator}. This references a rule, its owning | |||||
* {@link ConditionalFormatting}, its priority order (lower index = higher priority in Excel), | |||||
* and the information needed to evaluate the rule for a given cell. | |||||
* <p/> | |||||
* Having this all combined and cached avoids repeated access calls to the | |||||
* underlying structural objects, XSSF CT* objects and HSSF raw byte structures. | |||||
* Those objects can be referenced from here. This object will be out of sync if | |||||
* anything modifies the referenced structures' evaluation properties. | |||||
* <p/> | |||||
* The assumption is that consuming applications will read the display properties once and | |||||
* create whatever style objects they need, caching those at the application level. | |||||
* Thus this class only caches values needed for evaluation, not display. | |||||
*/ | |||||
public class EvaluationConditionalFormatRule implements Comparable<EvaluationConditionalFormatRule> { | |||||
private final WorkbookEvaluator workbookEvaluator; | |||||
private final Sheet sheet; | |||||
private final ConditionalFormatting formatting; | |||||
private final ConditionalFormattingRule rule; | |||||
/* cached values */ | |||||
private final CellRangeAddress[] regions; | |||||
/** | |||||
* Depending on the rule type, it may want to know about certain values in the region when evaluating {@link #matches(Cell)}, | |||||
* such as top 10, unique, duplicate, average, etc. This collection stores those if needed so they are not repeatedly calculated | |||||
*/ | |||||
private final Map<CellRangeAddress, Set<ValueAndFormat>> meaningfulRegionValues = new HashMap<CellRangeAddress, Set<ValueAndFormat>>(); | |||||
private final int priority; | |||||
private final int formattingIndex; | |||||
private final int ruleIndex; | |||||
private final String formula1; | |||||
private final String formula2; | |||||
private final OperatorEnum operator; | |||||
private final ConditionType type; | |||||
/** | |||||
* | |||||
* @param workbookEvaluator | |||||
* @param sheet | |||||
* @param formatting | |||||
* @param formattingIndex for priority, zero based | |||||
* @param rule | |||||
* @param ruleIndex for priority, zero based, if this is an HSSF rule. Unused for XSSF rules | |||||
* @param regions could be read from formatting, but every call creates new objects in a new array. | |||||
* this allows calling it once per formatting instance, and re-using the array. | |||||
*/ | |||||
public EvaluationConditionalFormatRule(WorkbookEvaluator workbookEvaluator, Sheet sheet, ConditionalFormatting formatting, int formattingIndex, ConditionalFormattingRule rule, int ruleIndex, CellRangeAddress[] regions) { | |||||
super(); | |||||
this.workbookEvaluator = workbookEvaluator; | |||||
this.sheet = sheet; | |||||
this.formatting = formatting; | |||||
this.rule = rule; | |||||
this.formattingIndex = formattingIndex; | |||||
this.ruleIndex = ruleIndex; | |||||
this.priority = rule.getPriority(); | |||||
this.regions = regions; | |||||
formula1 = rule.getFormula1(); | |||||
formula2 = rule.getFormula2(); | |||||
operator = OperatorEnum.values()[rule.getComparisonOperation()]; | |||||
type = rule.getConditionType(); | |||||
} | |||||
public Sheet getSheet() { | |||||
return sheet; | |||||
} | |||||
/** | |||||
* @return the formatting | |||||
*/ | |||||
public ConditionalFormatting getFormatting() { | |||||
return formatting; | |||||
} | |||||
public int getFormattingIndex() { | |||||
return formattingIndex; | |||||
} | |||||
/** | |||||
* @return the rule | |||||
*/ | |||||
public ConditionalFormattingRule getRule() { | |||||
return rule; | |||||
} | |||||
public int getRuleIndex() { | |||||
return ruleIndex; | |||||
} | |||||
/** | |||||
* @return the regions | |||||
*/ | |||||
public CellRangeAddress[] getRegions() { | |||||
return regions; | |||||
} | |||||
/** | |||||
* @return the priority | |||||
*/ | |||||
public int getPriority() { | |||||
return priority; | |||||
} | |||||
/** | |||||
* @return the formula1 | |||||
*/ | |||||
public String getFormula1() { | |||||
return formula1; | |||||
} | |||||
/** | |||||
* @return the formula2 | |||||
*/ | |||||
public String getFormula2() { | |||||
return formula2; | |||||
} | |||||
/** | |||||
* @return the operator | |||||
*/ | |||||
public OperatorEnum getOperator() { | |||||
return operator; | |||||
} | |||||
/** | |||||
* @return the type | |||||
*/ | |||||
public ConditionType getType() { | |||||
return type; | |||||
} | |||||
/** | |||||
* Defined as equal sheet name and formatting and rule indexes | |||||
* @see java.lang.Object#equals(java.lang.Object) | |||||
*/ | |||||
public boolean equals(Object obj) { | |||||
if (obj == null) return false; | |||||
if (! obj.getClass().equals(this.getClass())) return false; | |||||
final EvaluationConditionalFormatRule r = (EvaluationConditionalFormatRule) obj; | |||||
return getSheet().getSheetName().equalsIgnoreCase(r.getSheet().getSheetName()) | |||||
&& getFormattingIndex() == r.getFormattingIndex() | |||||
&& getRuleIndex() == r.getRuleIndex(); | |||||
} | |||||
/** | |||||
* Per Excel Help, XSSF rule priority is sheet-wide, not just within the owning ConditionalFormatting object. | |||||
* This can be seen by creating 4 rules applying to two different ranges and examining the XML. | |||||
* <p/> | |||||
* HSSF priority is based on definition/persistence order. | |||||
* | |||||
* @param o | |||||
* @return comparison based on sheet name, formatting index, and rule priority | |||||
*/ | |||||
public int compareTo(EvaluationConditionalFormatRule o) { | |||||
int cmp = getSheet().getSheetName().compareToIgnoreCase(o.getSheet().getSheetName()); | |||||
if (cmp != 0) return cmp; | |||||
final int x = getPriority(); | |||||
final int y = o.getPriority(); | |||||
// logic from Integer.compare() | |||||
cmp = (x < y) ? -1 : ((x == y) ? 0 : 1); | |||||
if (cmp != 0) return cmp; | |||||
cmp = Integer.compare(getFormattingIndex(), o.getFormattingIndex()); | |||||
if (cmp != 0) return cmp; | |||||
return Integer.compare(getRuleIndex(), o.getRuleIndex()); | |||||
} | |||||
public int hashCode() { | |||||
int hash = sheet.getSheetName().hashCode(); | |||||
hash = 31 * hash + formattingIndex; | |||||
hash = 31 * hash + ruleIndex; | |||||
return hash; | |||||
} | |||||
/** | |||||
* @param cell | |||||
* @return true if this rule evaluates to true for the given cell | |||||
*/ | |||||
/* package */ boolean matches(Cell cell) { | |||||
// first check that it is in one of the regions defined for this format | |||||
CellRangeAddress region = null; | |||||
for (CellRangeAddress r : regions) { | |||||
if (r.isInRange(cell)) { | |||||
region = r; | |||||
break; | |||||
} | |||||
} | |||||
if (region == null) return false; // cell not in range of this rule | |||||
final ConditionType ruleType = getRule().getConditionType(); | |||||
// these rules apply to all cells in a region. Specific condition criteria | |||||
// may specify no special formatting for that value partition, but that's display logic | |||||
if (ruleType.equals(ConditionType.COLOR_SCALE) | |||||
|| ruleType.equals(ConditionType.DATA_BAR) | |||||
|| ruleType.equals(ConditionType.ICON_SET)) { | |||||
return true; | |||||
} | |||||
if (ruleType.equals(ConditionType.CELL_VALUE_IS)) { | |||||
return checkValue(cell, region); | |||||
} | |||||
if (ruleType.equals(ConditionType.FORMULA)) { | |||||
return checkFormula(cell, region); | |||||
} | |||||
if (ruleType.equals(ConditionType.FILTER)) { | |||||
return checkFilter(cell, region); | |||||
} | |||||
// TODO: anything else, we don't handle yet, such as top 10 | |||||
return false; | |||||
} | |||||
/** | |||||
* @param cell | |||||
* @param region for adjusting relative formulas | |||||
* @return | |||||
*/ | |||||
private boolean checkValue(Cell cell, CellRangeAddress region) { | |||||
if (cell == null || DataValidationEvaluator.isType(cell, CellType.BLANK) | |||||
|| DataValidationEvaluator.isType(cell,CellType.ERROR) | |||||
|| (DataValidationEvaluator.isType(cell,CellType.STRING) | |||||
&& (cell.getStringCellValue() == null || cell.getStringCellValue().isEmpty()) | |||||
) | |||||
) return false; | |||||
ValueEval eval = unwrapEval(workbookEvaluator.evaluate(rule.getFormula1(), ConditionalFormattingEvaluator.getRef(cell), region)); | |||||
String f2 = rule.getFormula2(); | |||||
ValueEval eval2 = null; | |||||
if (f2 != null && f2.length() > 0) { | |||||
eval2 = unwrapEval(workbookEvaluator.evaluate(f2, ConditionalFormattingEvaluator.getRef(cell), region)); | |||||
} | |||||
// we assume the cell has been evaluated, and the current formula value stored | |||||
if (DataValidationEvaluator.isType(cell, CellType.BOOLEAN)) { | |||||
if (eval instanceof BoolEval && (eval2 == null || eval2 instanceof BoolEval) ) { | |||||
return operator.isValid(cell.getBooleanCellValue(), ((BoolEval) eval).getBooleanValue(), eval2 == null ? null : ((BoolEval) eval2).getBooleanValue()); | |||||
} | |||||
return false; // wrong types | |||||
} | |||||
if (DataValidationEvaluator.isType(cell, CellType.NUMERIC)) { | |||||
if (eval instanceof NumberEval && (eval2 == null || eval2 instanceof NumberEval) ) { | |||||
return operator.isValid(cell.getNumericCellValue(), ((NumberEval) eval).getNumberValue(), eval2 == null ? null : ((NumberEval) eval2).getNumberValue()); | |||||
} | |||||
return false; // wrong types | |||||
} | |||||
if (DataValidationEvaluator.isType(cell, CellType.STRING)) { | |||||
if (eval instanceof StringEval && (eval2 == null || eval2 instanceof StringEval) ) { | |||||
return operator.isValid(cell.getStringCellValue(), ((StringEval) eval).getStringValue(), eval2 == null ? null : ((StringEval) eval2).getStringValue()); | |||||
} | |||||
return false; // wrong types | |||||
} | |||||
// should not get here, but in case... | |||||
return false; | |||||
} | |||||
private ValueEval unwrapEval(ValueEval eval) { | |||||
ValueEval comp = eval; | |||||
while (comp instanceof RefEval) { | |||||
RefEval ref = (RefEval) comp; | |||||
comp = ref.getInnerValueEval(ref.getFirstSheetIndex()); | |||||
} | |||||
return comp; | |||||
} | |||||
/** | |||||
* @param cell needed for offsets from region anchor | |||||
* @param region for adjusting relative formulas | |||||
* @return true/false using the same rules as Data Validation evaluations | |||||
*/ | |||||
private boolean checkFormula(Cell cell, CellRangeAddress region) { | |||||
ValueEval comp = unwrapEval(workbookEvaluator.evaluate(rule.getFormula1(), ConditionalFormattingEvaluator.getRef(cell), region)); | |||||
// Copied for now from DataValidationEvaluator.ValidationEnum.FORMULA#isValidValue() | |||||
if (comp instanceof BlankEval) return true; | |||||
if (comp instanceof ErrorEval) return false; | |||||
if (comp instanceof BoolEval) { | |||||
return ((BoolEval) comp).getBooleanValue(); | |||||
} | |||||
// empirically tested in Excel - 0=false, any other number = true/valid | |||||
// see test file DataValidationEvaluations.xlsx | |||||
if (comp instanceof NumberEval) { | |||||
return ((NumberEval) comp).getNumberValue() != 0; | |||||
} | |||||
return false; // anything else is false, such as text | |||||
} | |||||
private boolean checkFilter(Cell cell, CellRangeAddress region) { | |||||
final ConditionFilterType filterType = rule.getConditionFilterType(); | |||||
if (filterType == null) return false; | |||||
// TODO: this could/should be delegated to the Enum type, but that's in the usermodel package, | |||||
// we may not want evaluation code there. Of course, maybe the enum should go here in formula, | |||||
// and not be returned by the SS model, but then we need the XSSF rule to expose the raw OOXML | |||||
// type value, which isn't ideal either. | |||||
switch (filterType) { | |||||
case FILTER: | |||||
return false; // we don't evaluate HSSF filters yet | |||||
case TOP_10: | |||||
// from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range. | |||||
// numbers stored as text are ignored, but numbers formatted as text are treated as numbers. | |||||
final ValueAndFormat cv10 = getCellValue(cell); | |||||
if (! cv10.isNumber()) return false; | |||||
return getMeaningfulValues(region, false, new ValueFunction() { | |||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) { | |||||
List<ValueAndFormat> values = allValues; | |||||
final ConditionFilterData conf = rule.getFilterConfiguration(); | |||||
if (! conf.getBottom()) Collections.sort(values, Collections.reverseOrder()); | |||||
else Collections.sort(values); | |||||
int limit = (int) conf.getRank(); | |||||
if (conf.getPercent()) limit = allValues.size() * limit / 100; | |||||
if (allValues.size() <= limit) return new HashSet<ValueAndFormat>(allValues); | |||||
return new HashSet<ValueAndFormat>(allValues.subList(0, limit)); | |||||
} | |||||
}).contains(cv10); | |||||
case UNIQUE_VALUES: | |||||
// Per Excel help, "duplicate" means matching value AND format | |||||
// https://support.office.com/en-us/article/Filter-for-unique-values-or-remove-duplicate-values-ccf664b0-81d6-449b-bbe1-8daaec1e83c2 | |||||
return getMeaningfulValues(region, true, new ValueFunction() { | |||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) { | |||||
List<ValueAndFormat> values = allValues; | |||||
Collections.sort(values); | |||||
final Set<ValueAndFormat> unique = new HashSet<ValueAndFormat>(); | |||||
for (int i=0; i < values.size(); i++) { | |||||
final ValueAndFormat v = values.get(i); | |||||
// skip this if the current value matches the next one, or is the last one and matches the previous one | |||||
if ( (i < values.size()-1 && v.equals(values.get(i+1)) ) || ( i > 0 && i == values.size()-1 && v.equals(values.get(i-1)) ) ) { | |||||
// current value matches next value, skip both | |||||
i++; | |||||
continue; | |||||
} | |||||
unique.add(v); | |||||
} | |||||
return unique; | |||||
} | |||||
}).contains(getCellValue(cell)); | |||||
case DUPLICATE_VALUES: | |||||
// Per Excel help, "duplicate" means matching value AND format | |||||
// https://support.office.com/en-us/article/Filter-for-unique-values-or-remove-duplicate-values-ccf664b0-81d6-449b-bbe1-8daaec1e83c2 | |||||
return getMeaningfulValues(region, true, new ValueFunction() { | |||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) { | |||||
List<ValueAndFormat> values = allValues; | |||||
Collections.sort(values); | |||||
final Set<ValueAndFormat> dup = new HashSet<ValueAndFormat>(); | |||||
for (int i=0; i < values.size(); i++) { | |||||
final ValueAndFormat v = values.get(i); | |||||
// skip this if the current value matches the next one, or is the last one and matches the previous one | |||||
if ( (i < values.size()-1 && v.equals(values.get(i+1)) ) || ( i > 0 && i == values.size()-1 && v.equals(values.get(i-1)) ) ) { | |||||
// current value matches next value, add one | |||||
dup.add(v); | |||||
i++; | |||||
} | |||||
} | |||||
return dup; | |||||
} | |||||
}).contains(getCellValue(cell)); | |||||
case ABOVE_AVERAGE: | |||||
// from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range. | |||||
// numbers stored as text are ignored, but numbers formatted as text are treated as numbers. | |||||
final ConditionFilterData conf = rule.getFilterConfiguration(); | |||||
// actually ordered, so iteration order is predictable | |||||
List<ValueAndFormat> values = new ArrayList<ValueAndFormat>(getMeaningfulValues(region, false, new ValueFunction() { | |||||
public Set<ValueAndFormat> evaluate(List<ValueAndFormat> allValues) { | |||||
List<ValueAndFormat> values = allValues; | |||||
double total = 0; | |||||
ValueEval[] pop = new ValueEval[values.size()]; | |||||
for (int i=0; i < values.size(); i++) { | |||||
ValueAndFormat v = values.get(i); | |||||
total += v.value.doubleValue(); | |||||
pop[i] = new NumberEval(v.value.doubleValue()); | |||||
} | |||||
final Set<ValueAndFormat> avgSet = new LinkedHashSet<ValueAndFormat>(1); | |||||
avgSet.add(new ValueAndFormat(new Double(values.size() == 0 ? 0 : total / values.size()), null)); | |||||
final double stdDev = values.size() <= 1 ? 0 : ((NumberEval) AggregateFunction.STDEV.evaluate(pop, 0, 0)).getNumberValue(); | |||||
avgSet.add(new ValueAndFormat(new Double(stdDev), null)); | |||||
return avgSet; | |||||
} | |||||
})); | |||||
final ValueAndFormat cv = getCellValue(cell); | |||||
Double val = cv.isNumber() ? cv.getValue() : null; | |||||
if (val == null) return false; | |||||
double avg = values.get(0).value.doubleValue(); | |||||
double stdDev = values.get(1).value.doubleValue(); | |||||
/* | |||||
* use StdDev, aboveAverage, equalAverage to find: | |||||
* comparison value | |||||
* operator type | |||||
*/ | |||||
Double comp = new Double(conf.getStdDev() > 0 ? (avg + (conf.getAboveAverage() ? 1 : -1) * stdDev * conf.getStdDev()) : avg) ; | |||||
OperatorEnum op = null; | |||||
if (conf.getAboveAverage()) { | |||||
if (conf.getEqualAverage()) op = OperatorEnum.GREATER_OR_EQUAL; | |||||
else op = OperatorEnum.GREATER_THAN; | |||||
} else { | |||||
if (conf.getEqualAverage()) op = OperatorEnum.LESS_OR_EQUAL; | |||||
else op = OperatorEnum.LESS_THAN; | |||||
} | |||||
return op != null && op.isValid(val, comp, null); | |||||
case CONTAINS_TEXT: | |||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula. | |||||
return checkFormula(cell, region); | |||||
case NOT_CONTAINS_TEXT: | |||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula. | |||||
return checkFormula(cell, region); | |||||
case BEGINS_WITH: | |||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula. | |||||
return checkFormula(cell, region); | |||||
case ENDS_WITH: | |||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula. | |||||
return checkFormula(cell, region); | |||||
case CONTAINS_BLANKS: | |||||
try { | |||||
String v = cell.getStringCellValue(); | |||||
// see TextFunction.TRIM for implementation | |||||
return v == null || v.trim().length() == 0; | |||||
} catch (Exception e) { | |||||
// not a valid string value, and not a blank cell (that's checked earlier) | |||||
return false; | |||||
} | |||||
case NOT_CONTAINS_BLANKS: | |||||
try { | |||||
String v = cell.getStringCellValue(); | |||||
// see TextFunction.TRIM for implementation | |||||
return v != null && v.trim().length() > 0; | |||||
} catch (Exception e) { | |||||
// not a valid string value, but not blank | |||||
return true; | |||||
} | |||||
case CONTAINS_ERRORS: | |||||
return cell != null && DataValidationEvaluator.isType(cell, CellType.ERROR); | |||||
case NOT_CONTAINS_ERRORS: | |||||
return cell == null || ! DataValidationEvaluator.isType(cell, CellType.ERROR); | |||||
case TIME_PERIOD: | |||||
// implemented both by a cfRule "text" attribute and a formula. Use the formula. | |||||
return checkFormula(cell, region); | |||||
default: | |||||
return false; | |||||
} | |||||
} | |||||
/** | |||||
* from testing, Excel only operates on numbers and dates (which are stored as numbers) in the range. | |||||
* numbers stored as text are ignored, but numbers formatted as text are treated as numbers. | |||||
* | |||||
* @param region | |||||
* @return | |||||
*/ | |||||
private Set<ValueAndFormat> getMeaningfulValues(CellRangeAddress region, boolean withText, ValueFunction func) { | |||||
Set<ValueAndFormat> values = meaningfulRegionValues.get(region); | |||||
if (values != null) return values; | |||||
List<ValueAndFormat> allValues = new ArrayList<ValueAndFormat>((region.getLastColumn() - region.getFirstColumn()+1) * (region.getLastRow() - region.getFirstRow() + 1)); | |||||
for (int r=region.getFirstRow(); r <= region.getLastRow(); r++) { | |||||
final Row row = sheet.getRow(r); | |||||
if (row == null) continue; | |||||
for (int c = region.getFirstColumn(); c <= region.getLastColumn(); c++) { | |||||
Cell cell = row.getCell(c); | |||||
final ValueAndFormat cv = getCellValue(cell); | |||||
if (cv != null && (withText || cv.isNumber()) ) allValues.add(cv); | |||||
} | |||||
} | |||||
values = func.evaluate(allValues); | |||||
meaningfulRegionValues.put(region, values); | |||||
return values; | |||||
} | |||||
private ValueAndFormat getCellValue(Cell cell) { | |||||
if (cell != null) { | |||||
final CellType type = cell.getCellTypeEnum(); | |||||
if (type == CellType.NUMERIC || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.NUMERIC) ) { | |||||
return new ValueAndFormat(new Double(cell.getNumericCellValue()), cell.getCellStyle().getDataFormatString()); | |||||
} else if (type == CellType.STRING || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.STRING) ) { | |||||
return new ValueAndFormat(cell.getStringCellValue(), cell.getCellStyle().getDataFormatString()); | |||||
} else if (type == CellType.BOOLEAN || (type == CellType.FORMULA && cell.getCachedFormulaResultTypeEnum() == CellType.BOOLEAN) ) { | |||||
return new ValueAndFormat(cell.getStringCellValue(), cell.getCellStyle().getDataFormatString()); | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* instances evaluate the values for a region and return the positive matches for the function type. | |||||
* TODO: when we get to use Java 8, this is obviously a Lambda Function. | |||||
*/ | |||||
protected interface ValueFunction { | |||||
/** | |||||
* | |||||
* @param values | |||||
* @return the desired values for the rules implemented by the current instance | |||||
*/ | |||||
Set<ValueAndFormat> evaluate(List<ValueAndFormat> values); | |||||
} | |||||
/** | |||||
* Not calling it OperatorType to avoid confusion for now with other classes. | |||||
* Definition order matches OOXML type ID indexes. | |||||
* Note that this has NO_COMPARISON as the first item, unlike the similar | |||||
* DataValidation operator enum. Thanks, Microsoft. | |||||
*/ | |||||
public static enum OperatorEnum { | |||||
NO_COMPARISON { | |||||
/** always false/invalid */ | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
return false; | |||||
} | |||||
}, | |||||
BETWEEN { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
return cellValue.compareTo(v1) >= 0 && cellValue.compareTo(v2) <= 0; | |||||
} | |||||
}, | |||||
NOT_BETWEEN { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
return cellValue.compareTo(v1) < 0 || cellValue.compareTo(v2) > 0; | |||||
} | |||||
}, | |||||
EQUAL { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
if (cellValue instanceof String) { | |||||
return ((String) cellValue).compareToIgnoreCase((String) v1) == 0; | |||||
} | |||||
return cellValue.compareTo(v1) == 0; | |||||
} | |||||
}, | |||||
NOT_EQUAL { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
if (cellValue instanceof String) { | |||||
return ((String) cellValue).compareToIgnoreCase((String) v1) != 0; | |||||
} | |||||
return cellValue.compareTo(v1) != 0; | |||||
} | |||||
}, | |||||
GREATER_THAN { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
return cellValue.compareTo(v1) > 0; | |||||
} | |||||
}, | |||||
LESS_THAN { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
return cellValue.compareTo(v1) < 0; | |||||
} | |||||
}, | |||||
GREATER_OR_EQUAL { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
return cellValue.compareTo(v1) >= 0; | |||||
} | |||||
}, | |||||
LESS_OR_EQUAL { | |||||
public <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2) { | |||||
return cellValue.compareTo(v1) <= 0; | |||||
} | |||||
}, | |||||
; | |||||
/** | |||||
* Evaluates comparison using operator instance rules | |||||
* @param cellValue won't be null, assumption is previous checks handled that | |||||
* @param v1 if null, value assumed invalid, anything passes, per Excel behavior | |||||
* @param v2 null if not needed. If null when needed, assume anything passes, per Excel behavior | |||||
* @return true if the comparison is valid | |||||
*/ | |||||
public abstract <C extends Comparable<C>> boolean isValid(C cellValue, C v1, C v2); | |||||
} | |||||
/** | |||||
* Note: this class has a natural ordering that is inconsistent with equals. | |||||
*/ | |||||
protected class ValueAndFormat implements Comparable<ValueAndFormat> { | |||||
private final Double value; | |||||
private final String string; | |||||
private final String format; | |||||
public ValueAndFormat(Double value, String format) { | |||||
this.value = value; | |||||
this.format = format; | |||||
string = null; | |||||
} | |||||
public ValueAndFormat(String value, String format) { | |||||
this.value = null; | |||||
this.format = format; | |||||
string = value; | |||||
} | |||||
public boolean isNumber() { | |||||
return value != null; | |||||
} | |||||
public Double getValue() { | |||||
return value; | |||||
} | |||||
public boolean equals(Object obj) { | |||||
ValueAndFormat o = (ValueAndFormat) obj; | |||||
return ( value == o.value || value.equals(o.value)) | |||||
&& ( format == o.format || format.equals(o.format)) | |||||
&& (string == o.string || string.equals(o.string)); | |||||
} | |||||
/** | |||||
* Note: this class has a natural ordering that is inconsistent with equals. | |||||
* @param o | |||||
* @return value comparison | |||||
*/ | |||||
public int compareTo(ValueAndFormat o) { | |||||
if (value == null && o.value != null) return 1; | |||||
if (o.value == null && value != null) return -1; | |||||
int cmp = value == null ? 0 : value.compareTo(o.value); | |||||
if (cmp != 0) return cmp; | |||||
if (string == null && o.string != null) return 1; | |||||
if (o.string == null && string != null) return -1; | |||||
return string == null ? 0 : string.compareTo(o.string); | |||||
} | |||||
public int hashCode() { | |||||
return (string == null ? 0 : string.hashCode()) * 37 * 37 + 37 * (value == null ? 0 : value.hashCode()) + (format == null ? 0 : format.hashCode()); | |||||
} | |||||
} | |||||
} |
package org.apache.poi.ss.formula; | 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.NamePtg; | ||||
import org.apache.poi.ss.formula.ptg.NameXPtg; | import org.apache.poi.ss.formula.ptg.NameXPtg; | ||||
import org.apache.poi.ss.formula.ptg.Ptg; | import org.apache.poi.ss.formula.ptg.Ptg; | ||||
String resolveNameXText(NameXPtg ptg); | String resolveNameXText(NameXPtg ptg); | ||||
Ptg[] getFormulaTokens(EvaluationCell cell); | Ptg[] getFormulaTokens(EvaluationCell cell); | ||||
UDFFinder getUDFFinder(); | UDFFinder getUDFFinder(); | ||||
SpreadsheetVersion getSpreadsheetVersion(); | |||||
/** | /** | ||||
* Propagated from {@link WorkbookEvaluator#clearAllCachedResultValues()} to clear locally cached data. | * Propagated from {@link WorkbookEvaluator#clearAllCachedResultValues()} to clear locally cached data. |
actualEndRow = _rowIndex; | actualEndRow = _rowIndex; | ||||
} else { // Really no special quantifiers | } else { // Really no special quantifiers | ||||
actualStartRow++; | actualStartRow++; | ||||
if (tbl.isHasTotalsRow()) actualEndRow--; | |||||
} | } | ||||
} | } | ||||
import java.util.Stack; | import java.util.Stack; | ||||
import java.util.TreeSet; | import java.util.TreeSet; | ||||
import org.apache.poi.ss.SpreadsheetVersion; | |||||
import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment.WorkbookNotFoundException; | import org.apache.poi.ss.formula.CollaboratingWorkbooksEnvironment.WorkbookNotFoundException; | ||||
import org.apache.poi.ss.formula.atp.AnalysisToolPak; | import org.apache.poi.ss.formula.atp.AnalysisToolPak; | ||||
import org.apache.poi.ss.formula.eval.BlankEval; | import org.apache.poi.ss.formula.eval.BlankEval; | ||||
import org.apache.poi.ss.formula.functions.FreeRefFunction; | import org.apache.poi.ss.formula.functions.FreeRefFunction; | ||||
import org.apache.poi.ss.formula.functions.Function; | import org.apache.poi.ss.formula.functions.Function; | ||||
import org.apache.poi.ss.formula.functions.IfFunc; | 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.AggregatingUDFFinder; | ||||
import org.apache.poi.ss.formula.udf.UDFFinder; | import org.apache.poi.ss.formula.udf.UDFFinder; | ||||
import org.apache.poi.ss.usermodel.CellType; | 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.ss.util.CellReference; | ||||
import org.apache.poi.util.Internal; | import org.apache.poi.util.Internal; | ||||
import org.apache.poi.util.POILogFactory; | import org.apache.poi.util.POILogFactory; | ||||
return _udfFinder.findFunction(functionName); | 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 | * Whether to ignore missing references to external workbooks and | ||||
* use cached formula results in the main workbook instead. | * use cached formula results in the main workbook instead. |
import java.util.HashMap; | import java.util.HashMap; | ||||
import java.util.Map; | import java.util.Map; | ||||
import org.apache.poi.ss.SpreadsheetVersion; | |||||
import org.apache.poi.ss.formula.EvaluationCell; | import org.apache.poi.ss.formula.EvaluationCell; | ||||
import org.apache.poi.ss.formula.EvaluationName; | import org.apache.poi.ss.formula.EvaluationName; | ||||
import org.apache.poi.ss.formula.EvaluationSheet; | import org.apache.poi.ss.formula.EvaluationSheet; | ||||
return _masterBook.getUDFFinder(); | return _masterBook.getUDFFinder(); | ||||
} | } | ||||
public SpreadsheetVersion getSpreadsheetVersion() { | |||||
return _masterBook.getSpreadsheetVersion(); | |||||
} | |||||
/* (non-Javadoc) | /* (non-Javadoc) | ||||
* leave the map alone, if it needs resetting, reusing this class is probably a bad idea. | * leave the map alone, if it needs resetting, reusing this class is probably a bad idea. | ||||
* @see org.apache.poi.ss.formula.EvaluationSheet#clearAllCachedResultValues() | * @see org.apache.poi.ss.formula.EvaluationSheet#clearAllCachedResultValues() |
/* | |||||
* ==================================================================== | |||||
* 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(); | |||||
} |
/* | |||||
* ==================================================================== | |||||
* Licensed to the Apache Software Foundation (ASF) under one or more | |||||
* contributor license agreements. See the NOTICE file distributed with | |||||
* this work for additional information regarding copyright ownership. | |||||
* The ASF licenses this file to You under the Apache License, Version 2.0 | |||||
* (the "License"); you may not use this file except in compliance with | |||||
* the License. You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ==================================================================== | |||||
*/ | |||||
package org.apache.poi.ss.usermodel; | |||||
/** | |||||
* Used primarily for XSSF conditions, which defines a multitude of additional "filter" types | |||||
* for conditional formatting. HSSF rules will always be null (not a filter type) or #FILTER. | |||||
* XSSF conditions will be null (not a filter type) or any value other than #FILTER. | |||||
* <p/> | |||||
* Instance names match the constants from <code>STCfType</code> for convenience. | |||||
*/ | |||||
public enum ConditionFilterType { | |||||
/** This is the only value valid for HSSF rules */ | |||||
FILTER, | |||||
TOP_10, | |||||
UNIQUE_VALUES, | |||||
DUPLICATE_VALUES, | |||||
CONTAINS_TEXT, | |||||
NOT_CONTAINS_TEXT, | |||||
BEGINS_WITH, | |||||
ENDS_WITH, | |||||
CONTAINS_BLANKS, | |||||
NOT_CONTAINS_BLANKS, | |||||
CONTAINS_ERRORS, | |||||
NOT_CONTAINS_ERRORS, | |||||
TIME_PERIOD, | |||||
ABOVE_AVERAGE, | |||||
; | |||||
} |
* @return the type of condition | * @return the type of condition | ||||
*/ | */ | ||||
ConditionType getConditionType(); | 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 | * The comparison function used when the type of conditional formatting is set to | ||||
* @return the second formula | * @return the second formula | ||||
*/ | */ | ||||
String getFormula2(); | 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(); | |||||
} | } |
/* | |||||
* ==================================================================== | |||||
* 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(); | |||||
} |
package org.apache.poi.ss.util; | package org.apache.poi.ss.util; | ||||
import org.apache.poi.ss.SpreadsheetVersion; | import org.apache.poi.ss.SpreadsheetVersion; | ||||
import org.apache.poi.ss.usermodel.Cell; | |||||
/** | /** | ||||
_firstCol <= colInd && colInd <= _lastCol; //containsColumn | _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 | * Check if the row is in the specified cell range | ||||
* | * |
return cr.isInRange(rowIx, colIx); | 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 | * 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. | * cell who's contents are shown in a given position in the sheet. | ||||
* then will return the cell itself. | * then will return the cell itself. | ||||
* <p>If there is no cell defined at the given co-ordinates, will return | * <p>If there is no cell defined at the given co-ordinates, will return | ||||
* null. | * 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) { | 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()) { | for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) { | ||||
if (mergedRegion.isInRange(rowIx, colIx)) { | if (mergedRegion.isInRange(rowIx, colIx)) { | ||||
// The cell wanted is in this merged range | // The cell wanted is in this merged range | ||||
// Return the primary (top-left) cell for the range | // Return the primary (top-left) cell for the range | ||||
r = sheet.getRow(mergedRegion.getFirstRow()); | |||||
Row r = sheet.getRow(mergedRegion.getFirstRow()); | |||||
if (r != null) { | if (r != null) { | ||||
return r.getCell(mergedRegion.getFirstColumn()); | return r.getCell(mergedRegion.getFirstColumn()); | ||||
} | } |
/* | |||||
* ==================================================================== | |||||
* Licensed to the Apache Software Foundation (ASF) under one or more | |||||
* contributor license agreements. See the NOTICE file distributed with | |||||
* this work for additional information regarding copyright ownership. | |||||
* The ASF licenses this file to You under the Apache License, Version 2.0 | |||||
* (the "License"); you may not use this file except in compliance with | |||||
* the License. You may obtain a copy of the License at | |||||
* | |||||
* http://www.apache.org/licenses/LICENSE-2.0 | |||||
* | |||||
* Unless required by applicable law or agreed to in writing, software | |||||
* distributed under the License is distributed on an "AS IS" BASIS, | |||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||||
* See the License for the specific language governing permissions and | |||||
* limitations under the License. | |||||
* ==================================================================== | |||||
*/ | |||||
package org.apache.poi.xssf.usermodel; | |||||
import org.apache.poi.ss.usermodel.ConditionFilterData; | |||||
import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCfRule; | |||||
public class XSSFConditionFilterData implements ConditionFilterData { | |||||
private final CTCfRule _cfRule; | |||||
/*package*/ XSSFConditionFilterData(CTCfRule cfRule) { | |||||
_cfRule = cfRule; | |||||
} | |||||
public boolean getAboveAverage() { | |||||
return _cfRule.getAboveAverage(); | |||||
} | |||||
public boolean getBottom() { | |||||
return _cfRule.getBottom(); | |||||
} | |||||
public boolean getEqualAverage() { | |||||
return _cfRule.getEqualAverage(); | |||||
} | |||||
public boolean getPercent() { | |||||
return _cfRule.getPercent(); | |||||
} | |||||
public long getRank() { | |||||
return _cfRule.getRank(); | |||||
} | |||||
public int getStdDev() { | |||||
return _cfRule.getStdDev(); | |||||
} | |||||
} |
import org.openxmlformats.schemas.spreadsheetml.x2006.main.*; | import org.openxmlformats.schemas.spreadsheetml.x2006.main.*; | ||||
/** | /** | ||||
* XSSF suport for Conditional Formatting rules | |||||
* XSSF support for Conditional Formatting rules | |||||
*/ | */ | ||||
public class XSSFConditionalFormattingRule implements ConditionalFormattingRule { | public class XSSFConditionalFormattingRule implements ConditionalFormattingRule { | ||||
private final CTCfRule _cfRule; | private final CTCfRule _cfRule; | ||||
private XSSFSheet _sh; | private XSSFSheet _sh; | ||||
private static Map<STCfType.Enum, ConditionType> typeLookup = new HashMap<STCfType.Enum, ConditionType>(); | 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 { | static { | ||||
typeLookup.put(STCfType.CELL_IS, ConditionType.CELL_VALUE_IS); | typeLookup.put(STCfType.CELL_IS, ConditionType.CELL_VALUE_IS); | ||||
typeLookup.put(STCfType.EXPRESSION, ConditionType.FORMULA); | typeLookup.put(STCfType.EXPRESSION, ConditionType.FORMULA); | ||||
typeLookup.put(STCfType.NOT_CONTAINS_ERRORS, ConditionType.FILTER); | typeLookup.put(STCfType.NOT_CONTAINS_ERRORS, ConditionType.FILTER); | ||||
typeLookup.put(STCfType.TIME_PERIOD, ConditionType.FILTER); | typeLookup.put(STCfType.TIME_PERIOD, ConditionType.FILTER); | ||||
typeLookup.put(STCfType.ABOVE_AVERAGE, 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){ | /*package*/ XSSFConditionalFormattingRule(XSSFSheet sh){ | ||||
_cfRule = CTCfRule.Factory.newInstance(); | _cfRule = CTCfRule.Factory.newInstance(); | ||||
_sh = sh; | _sh = sh; | ||||
return dxf; | 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, | * Create a new border formatting structure if it does not exist, | ||||
* otherwise just return existing object. | * otherwise just return existing object. | ||||
return typeLookup.get(_cfRule.getType()); | 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 | * The comparison function used when the type of conditional formatting is set to | ||||
* {@link ConditionType#CELL_VALUE_IS} | * {@link ConditionType#CELL_VALUE_IS} |
throw new IllegalArgumentException("Specified cell does not belong to this sheet."); | throw new IllegalArgumentException("Specified cell does not belong to this sheet."); | ||||
} | } | ||||
for (CellRangeAddress range : arrayFormulas) { | for (CellRangeAddress range : arrayFormulas) { | ||||
if (range.isInRange(cell.getRowIndex(), cell.getColumnIndex())) { | |||||
if (range.isInRange(cell)) { | |||||
arrayFormulas.remove(range); | arrayFormulas.remove(range); | ||||
CellRange<XSSFCell> cr = getCellRange(range); | CellRange<XSSFCell> cr = getCellRange(range); | ||||
for (XSSFCell c : cr) { | for (XSSFCell c : cr) { |
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(); | |||||
} | |||||
} |