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