From 530dd0e083589cb4d212011eedeeee4afe988489 Mon Sep 17 00:00:00 2001 From: Greg Woolsey Date: Fri, 5 May 2017 17:44:58 +0000 Subject: [PATCH] 61060 - teach DataFormatter about conditional formatting rules with number formats Currently only implemented for XSSF, as there is no API available for HSSF conditional formatting rule number formats (if it is even in the files). git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1794084 13f79535-47bb-0310-9956-ffa450edef68 --- .../HSSFConditionalFormattingRule.java | 9 ++ .../EvaluationConditionalFormatRule.java | 23 ++++++ .../usermodel/ConditionalFormattingRule.java | 6 ++ .../poi/ss/usermodel/DataFormatter.java | 77 +++++++++++++---- .../org/apache/poi/ss/usermodel/DateUtil.java | 58 +++++++++++-- .../poi/ss/usermodel/ExcelNumberFormat.java | 78 ++++++++++++++++++ .../XSSFConditionalFormattingRule.java | 12 +++ .../xssf/usermodel/TestXSSFDataFormat.java | 41 +++++++++ .../61060-conditional-number-formatting.xlsx | Bin 0 -> 8224 bytes 9 files changed, 282 insertions(+), 22 deletions(-) create mode 100644 src/java/org/apache/poi/ss/usermodel/ExcelNumberFormat.java create mode 100644 test-data/spreadsheet/61060-conditional-number-formatting.xlsx diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java b/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java index aa46315091..71920a9ff8 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFConditionalFormattingRule.java @@ -33,6 +33,7 @@ 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; +import org.apache.poi.ss.usermodel.ExcelNumberFormat; /** * @@ -91,6 +92,14 @@ public final class HSSFConditionalFormattingRule implements ConditionalFormattin } return (CFRule12Record)cfRuleRecord; } + + /** + * Always null for HSSF records, until someone figures out where to find it + * @see org.apache.poi.ss.usermodel.ConditionalFormattingRule#getNumberFormat() + */ + public ExcelNumberFormat getNumberFormat() { + return null; + } private HSSFFontFormatting getFontFormatting(boolean create) { FontFormatting fontFormatting = cfRuleRecord.getFontFormatting(); diff --git a/src/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java b/src/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java index 1bab3e9cae..477aaaf64a 100644 --- a/src/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java +++ b/src/java/org/apache/poi/ss/formula/EvaluationConditionalFormatRule.java @@ -41,6 +41,7 @@ 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.ExcelNumberFormat; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.util.CellRangeAddress; @@ -60,6 +61,9 @@ import org.apache.poi.ss.util.CellRangeAddress; * create whatever style objects they need, caching those at the application level. * Thus this class only caches values needed for evaluation, not display. */ +/** + * + */ public class EvaluationConditionalFormatRule implements Comparable { private final WorkbookEvaluator workbookEvaluator; @@ -82,6 +86,8 @@ public class EvaluationConditionalFormatRule implements ComparableString based * on the cell's DataFormat. i.e. "Thursday, January 02, 2003" * , "01/02/2003" , "02-Jan" , etc. + *

+ * If any conditional format rules apply, the highest priority with a number format is used. + * If no rules contain a number format, or no rules apply, the cell's style format is used. + * If the style does not have a format, the default date format is applied. * - * @param cell The cell - * @return a formatted date string + * @param cell + * @param cfEvaluator ConditionalFormattingEvaluator (if available) + * @return */ - private String getFormattedDateString(Cell cell) { - Format dateFormat = getFormat(cell); + private String getFormattedDateString(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { + Format dateFormat = getFormat(cell, cfEvaluator); if(dateFormat instanceof ExcelStyleDateFormatter) { // Hint about the raw excel value ((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted( @@ -769,13 +783,17 @@ public class DataFormatter implements Observer { * based on the cell's DataFormat. Supported formats include * currency, percents, decimals, phone number, SSN, etc.: * "61.54%", "$100.00", "(800) 555-1234". - * + *

+ * Format comes from either the highest priority conditional format rule with a + * specified format, or from the cell style. + * * @param cell The cell + * @param cfEvaluator if available, or null * @return a formatted number string */ - private String getFormattedNumberString(Cell cell) { + private String getFormattedNumberString(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { - Format numberFormat = getFormat(cell); + Format numberFormat = getFormat(cell, cfEvaluator); double d = cell.getNumericCellValue(); if (numberFormat == null) { return String.valueOf(d); @@ -863,7 +881,7 @@ public class DataFormatter implements Observer { /** *

* Returns the formatted value of a cell as a String regardless - * of the cell type. If the Excel format pattern cannot be parsed then the + * of the cell type. If the Excel number format pattern cannot be parsed then the * cell value will be formatted using a default format. *

*

When passed a null or blank cell, this method will return an empty @@ -878,6 +896,37 @@ public class DataFormatter implements Observer { * @return a string value of the cell */ public String formatCellValue(Cell cell, FormulaEvaluator evaluator) { + return formatCellValue(cell, evaluator, null); + } + + /** + *

+ * Returns the formatted value of a cell as a String regardless + * of the cell type. If the Excel number format pattern cannot be parsed then the + * cell value will be formatted using a default format. + *

+ *

When passed a null or blank cell, this method will return an empty + * String (""). Formula cells will be evaluated using the given + * {@link FormulaEvaluator} if the evaluator is non-null. If the + * evaluator is null, then the formula String will be returned. The caller + * is responsible for setting the currentRow on the evaluator + *

+ *

+ * When a ConditionalFormattingEvaluator is present, it is checked first to see + * if there is a number format to apply. If multiple rules apply, the last one is used. + * If no ConditionalFormattingEvaluator is present, no rules apply, or the applied + * rules do not define a format, the cell's style format is used. + *

+ *

+ * The two evaluators should be from the same context, to avoid inconsistencies in cached values. + *

+ * + * @param cell The cell (can be null) + * @param evaluator The FormulaEvaluator (can be null) + * @param cfEvaluator ConditionalFormattingEvaluator (can be null) + * @return a string value of the cell + */ + public String formatCellValue(Cell cell, FormulaEvaluator evaluator, ConditionalFormattingEvaluator cfEvaluator) { localeChangedObservable.checkForLocaleChange(); if (cell == null) { @@ -894,10 +943,10 @@ public class DataFormatter implements Observer { switch (cellType) { case NUMERIC : - if (DateUtil.isCellDateFormatted(cell)) { - return getFormattedDateString(cell); + if (DateUtil.isCellDateFormatted(cell, cfEvaluator)) { + return getFormattedDateString(cell, cfEvaluator); } - return getFormattedNumberString(cell); + return getFormattedNumberString(cell, cfEvaluator); case STRING : return cell.getRichStringCellValue().getString(); diff --git a/src/java/org/apache/poi/ss/usermodel/DateUtil.java b/src/java/org/apache/poi/ss/usermodel/DateUtil.java index 79d94c2f8d..1ba0e133ff 100644 --- a/src/java/org/apache/poi/ss/usermodel/DateUtil.java +++ b/src/java/org/apache/poi/ss/usermodel/DateUtil.java @@ -19,10 +19,14 @@ package org.apache.poi.ss.usermodel; import java.util.Calendar; +import java.util.Collections; import java.util.Date; +import java.util.List; import java.util.TimeZone; import java.util.regex.Pattern; +import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; +import org.apache.poi.ss.formula.EvaluationConditionalFormatRule; import org.apache.poi.util.LocaleUtil; /** @@ -356,12 +360,33 @@ public class DateUtil { * date formatting characters (ymd-/), which covers most * non US date formats. * - * @param formatIndex The index of the format, eg from ExtendedFormatRecord.getFormatIndex - * @param formatString The format string, eg from FormatRecord.getFormatString + * @param numFmt The number format index and string expression, or null if not specified + * @return true if it is a valid date format, false if not or null + * @see #isInternalDateFormat(int) + */ + public static boolean isADateFormat(ExcelNumberFormat numFmt) { + + if (numFmt == null) return false; + + return isADateFormat(numFmt.getIdx(), numFmt.getFormat()); + } + + /** + * Given a format ID and its format String, will check to see if the + * format represents a date format or not. + * Firstly, it will check to see if the format ID corresponds to an + * internal excel date format (eg most US date formats) + * If not, it will check to see if the format string only contains + * date formatting characters (ymd-/), which covers most + * non US date formats. + * + * @param formatIndex The index of the format, eg from ExtendedFormatRecord.getFormatIndex + * @param formatString The format string, eg from FormatRecord.getFormatString + * @return true if it is a valid date format, false if not or null * @see #isInternalDateFormat(int) */ - public static boolean isADateFormat(int formatIndex, String formatString) { + // First up, is this an internal date format? if(isInternalDateFormat(formatIndex)) { cache(formatString, formatIndex, true); @@ -492,23 +517,40 @@ public class DateUtil { * Check if a cell contains a date * Since dates are stored internally in Excel as double values * we infer it is a date if it is formatted as such. + * @param cell + * @return true if it looks like a date * @see #isADateFormat(int, String) * @see #isInternalDateFormat(int) */ public static boolean isCellDateFormatted(Cell cell) { + return isCellDateFormatted(cell, null); + } + + /** + * Check if a cell contains a date + * Since dates are stored internally in Excel as double values + * we infer it is a date if it is formatted as such. + * Format is determined from applicable conditional formatting, if + * any, or cell style. + * @param cell + * @param cfEvaluator if available, or null + * @return true if it looks like a date + * @see #isADateFormat(int, String) + * @see #isInternalDateFormat(int) + */ + public static boolean isCellDateFormatted(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { if (cell == null) return false; boolean bDate = false; double d = cell.getNumericCellValue(); if ( DateUtil.isValidExcelDate(d) ) { - CellStyle style = cell.getCellStyle(); - if(style==null) return false; - int i = style.getDataFormat(); - String f = style.getDataFormatString(); - bDate = isADateFormat(i, f); + ExcelNumberFormat nf = ExcelNumberFormat.from(cell, cfEvaluator); + if(nf==null) return false; + bDate = isADateFormat(nf); } return bDate; } + /** * Check if a cell contains a date, checking only for internal * excel date formats. diff --git a/src/java/org/apache/poi/ss/usermodel/ExcelNumberFormat.java b/src/java/org/apache/poi/ss/usermodel/ExcelNumberFormat.java new file mode 100644 index 0000000000..88f93915bb --- /dev/null +++ b/src/java/org/apache/poi/ss/usermodel/ExcelNumberFormat.java @@ -0,0 +1,78 @@ +package org.apache.poi.ss.usermodel; + +import java.util.List; + +import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; +import org.apache.poi.ss.formula.EvaluationConditionalFormatRule; + +/** + * Object to hold a number format index and string, for various formatting evaluations + */ +public class ExcelNumberFormat { + + private final int idx; + private final String format; + + /** + * @param style + * @return null if the style is null, instance from style data format values otherwise + */ + public static ExcelNumberFormat from(CellStyle style) { + if (style == null) return null; + return new ExcelNumberFormat(style.getDataFormat(), style.getDataFormatString()); + } + + /** + * @param cell cell to extract format from + * @param cfEvaluator ConditionalFormattingEvaluator to use, or null if none in this context + * @return number format from highest-priority rule with a number format, or the cell style, or null if none of the above apply/are defined + */ + public static ExcelNumberFormat from(Cell cell, ConditionalFormattingEvaluator cfEvaluator) { + if (cell == null) return null; + + ExcelNumberFormat nf = null; + + if (cfEvaluator != null) { + // first one wins (priority order, per Excel help) + List rules = cfEvaluator.getConditionalFormattingForCell(cell); + for (EvaluationConditionalFormatRule rule : rules) { + nf = rule.getNumberFormat(); + if (nf != null) break; + } + } + if (nf == null) { + CellStyle style = cell.getCellStyle(); + nf = ExcelNumberFormat.from(style); + } + return nf; + } + + /** + * Use this carefully, prefer factory methods to ensure id/format relationships are not broken or confused. + * Left public so {@link ConditionalFormattingRule#getNumberFormat()} implementations can use it. + * @param idx Excel number format index, either a built-in or a higher custom # mapped in the workbook style table + * @param format Excel number format string for the index + */ + public ExcelNumberFormat(int idx, String format) { + this.idx = idx; + this.format = format; + } + + + + /** + * + * @return Excel number format index, either a built-in or a higher custom # mapped in the workbook style table + */ + public int getIdx() { + return idx; + } + + /** + * + * @return Excel number format string for the index + */ + public String getFormat() { + return format; + } +} diff --git a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java index 69816b7fac..5ab6c299ac 100644 --- a/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java +++ b/src/ooxml/java/org/apache/poi/xssf/usermodel/XSSFConditionalFormattingRule.java @@ -325,6 +325,18 @@ public class XSSFConditionalFormattingRule implements ConditionalFormattingRule } } + /** + * Return the number format from the dxf style record if present, null if not + * @see org.apache.poi.ss.usermodel.ConditionalFormattingRule#getNumberFormat() + */ + public ExcelNumberFormat getNumberFormat() { + CTDxf dxf = getDxf(false); + if(dxf == null || !dxf.isSetNumFmt()) return null; + + CTNumFmt numFmt = dxf.getNumFmt(); + return new ExcelNumberFormat((int) numFmt.getNumFmtId(), numFmt.getFormatCode()); + } + /** * Type of conditional formatting rule. */ diff --git a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java index c7179531d8..b4c12f8cbe 100644 --- a/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java +++ b/src/ooxml/testcases/org/apache/poi/xssf/usermodel/TestXSSFDataFormat.java @@ -21,11 +21,19 @@ import static org.junit.Assert.*; import java.io.IOException; +import org.apache.poi.hssf.HSSFTestDataSamples; +import org.apache.poi.ss.formula.ConditionalFormattingEvaluator; +import org.apache.poi.ss.formula.WorkbookEvaluatorProvider; import org.apache.poi.ss.usermodel.BaseTestDataFormat; import org.apache.poi.ss.usermodel.BuiltinFormats; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.DataFormat; +import org.apache.poi.ss.usermodel.DataFormatter; +import org.apache.poi.ss.usermodel.FormulaEvaluator; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellReference; import org.apache.poi.xssf.XSSFITestDataProvider; import org.apache.poi.xssf.XSSFTestDataSamples; import org.junit.Test; @@ -108,4 +116,37 @@ public final class TestXSSFDataFormat extends BaseTestDataFormat { wb2.close(); wb1.close(); } + + @Test + public void testConditionalFormattingEvaluation() throws IOException { + final Workbook wb = XSSFTestDataSamples.openSampleWorkbook("61060-conditional-number-formatting.xlsx"); + + final DataFormatter formatter = new DataFormatter(); + final FormulaEvaluator evaluator = wb.getCreationHelper().createFormulaEvaluator(); + final ConditionalFormattingEvaluator cfEvaluator = new ConditionalFormattingEvaluator(wb, (WorkbookEvaluatorProvider) evaluator); + + CellReference ref = new CellReference("A1"); + Cell cell = wb.getSheetAt(0).getRow(ref.getRow()).getCell(ref.getCol()); + assertEquals("0.10", formatter.formatCellValue(cell, evaluator, cfEvaluator)); + // verify cell format without the conditional rule applied + assertEquals("0.1", formatter.formatCellValue(cell, evaluator)); + + ref = new CellReference("A3"); + cell = wb.getSheetAt(0).getRow(ref.getRow()).getCell(ref.getCol()); + assertEquals("-2.00E+03", formatter.formatCellValue(cell, evaluator, cfEvaluator)); + // verify cell format without the conditional rule applied + assertEquals("-2000", formatter.formatCellValue(cell, evaluator)); + + ref = new CellReference("A4"); + cell = wb.getSheetAt(0).getRow(ref.getRow()).getCell(ref.getCol()); + assertEquals("100", formatter.formatCellValue(cell, evaluator, cfEvaluator)); + + ref = new CellReference("A5"); + cell = wb.getSheetAt(0).getRow(ref.getRow()).getCell(ref.getCol()); + assertEquals("$1,000", formatter.formatCellValue(cell, evaluator, cfEvaluator)); + // verify cell format without the conditional rule applied + assertEquals("1000", formatter.formatCellValue(cell, evaluator)); + + wb.close(); + } } diff --git a/test-data/spreadsheet/61060-conditional-number-formatting.xlsx b/test-data/spreadsheet/61060-conditional-number-formatting.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..95072c2464ac7ba4d80f991f4fb65249672f864d GIT binary patch literal 8224 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 Uo4o-5aBf~wH;UU&`{UjJ0mIQQQ2+n{ literal 0 HcmV?d00001