From d4655fe1bbb2783f3d51b8dc419681de54a2be1e Mon Sep 17 00:00:00 2001 From: Nick Burch Date: Sun, 20 Jul 2008 17:18:07 +0000 Subject: [PATCH] Apply, with some tweaks, the patch from bug #45404 - New class, hssf.usermodel.HSSFDataFormatter, for formatting numbers and dates in the same way that Excel does git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@678287 13f79535-47bb-0310-9956-ffa450edef68 --- src/documentation/content/xdocs/changes.xml | 1 + src/documentation/content/xdocs/status.xml | 1 + .../FormatTrackingHSSFListener.java | 31 +- .../apache/poi/hssf/usermodel/HSSFCell.java | 12 +- .../poi/hssf/usermodel/HSSFDataFormatter.java | 703 ++++++++++++++++++ .../hssf/usermodel/TestHSSFDataFormatter.java | 282 +++++++ 6 files changed, 1001 insertions(+), 29 deletions(-) create mode 100644 src/java/org/apache/poi/hssf/usermodel/HSSFDataFormatter.java create mode 100644 src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java diff --git a/src/documentation/content/xdocs/changes.xml b/src/documentation/content/xdocs/changes.xml index 9933efbf1..021e946e7 100644 --- a/src/documentation/content/xdocs/changes.xml +++ b/src/documentation/content/xdocs/changes.xml @@ -37,6 +37,7 @@ + 45404 - New class, hssf.usermodel.HSSFDataFormatter, for formatting numbers and dates in the same way that Excel does 45414 - Don't add too many UncalcedRecords to sheets with charts in them 45398 - Support detecting date formats containing "am/pm" as date times 45410 - Removed dependency from contrib on commons beanutils,collections and lang diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 09949e470..038458006 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 45404 - New class, hssf.usermodel.HSSFDataFormatter, for formatting numbers and dates in the same way that Excel does 45414 - Don't add too many UncalcedRecords to sheets with charts in them 45398 - Support detecting date formats containing "am/pm" as date times 45410 - Removed dependency from contrib on commons beanutils,collections and lang diff --git a/src/java/org/apache/poi/hssf/eventusermodel/FormatTrackingHSSFListener.java b/src/java/org/apache/poi/hssf/eventusermodel/FormatTrackingHSSFListener.java index 5a84f4564..355a9b71f 100644 --- a/src/java/org/apache/poi/hssf/eventusermodel/FormatTrackingHSSFListener.java +++ b/src/java/org/apache/poi/hssf/eventusermodel/FormatTrackingHSSFListener.java @@ -32,6 +32,7 @@ import org.apache.poi.hssf.record.FormulaRecord; import org.apache.poi.hssf.record.NumberRecord; import org.apache.poi.hssf.record.Record; import org.apache.poi.hssf.usermodel.HSSFDataFormat; +import org.apache.poi.hssf.usermodel.HSSFDataFormatter; import org.apache.poi.hssf.usermodel.HSSFDateUtil; /** @@ -41,6 +42,7 @@ import org.apache.poi.hssf.usermodel.HSSFDateUtil; */ public class FormatTrackingHSSFListener implements HSSFListener { private HSSFListener childListener; + private HSSFDataFormatter formatter = new HSSFDataFormatter(); private Map customFormatRecords = new Hashtable(); private List xfRecords = new ArrayList(); @@ -102,32 +104,9 @@ public class FormatTrackingHSSFListener implements HSSFListener { if(formatString == null) { return Double.toString(value); } else { - // Is it a date? - if(HSSFDateUtil.isADateFormat(formatIndex,formatString) && - HSSFDateUtil.isValidExcelDate(value)) { - // Java wants M not m for month - formatString = formatString.replace('m','M'); - // Change \- into -, if it's there - formatString = formatString.replaceAll("\\\\-","-"); - - // Format as a date - Date d = HSSFDateUtil.getJavaDate(value, false); - DateFormat df = new SimpleDateFormat(formatString); - return df.format(d); - } else { - if(formatString == "General") { - // Some sort of wierd default - return Double.toString(value); - } - if(formatString == "0.00E+00") { - // This seems to mean output as a normal double - return Double.toString(value); - } - - // Format as a number - DecimalFormat df = new DecimalFormat(formatString); - return df.format(value); - } + // Format, using the nice new + // HSSFDataFormatter to do the work for us + return formatter.formatRawCellContents(value, formatIndex, formatString); } } diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java index 21335a8dd..3e3dc0542 100644 --- a/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFCell.java @@ -681,10 +681,13 @@ public class HSSFCell /** - * get the value of the cell as a number. For strings we throw an exception. + * Get the value of the cell as a number. + * For strings we throw an exception. * For blank cells we return a 0. + * See {@link HSSFDataFormatter} for turning this + * number into a string similar to that which + * Excel would render this number as. */ - public double getNumericCellValue() { if (cellType == CELL_TYPE_BLANK) @@ -718,8 +721,11 @@ public class HSSFCell } /** - * get the value of the cell as a date. For strings we throw an exception. + * Get the value of the cell as a date. + * For strings we throw an exception. * For blank cells we return a null. + * See {@link HSSFDataFormatter} for formatting + * this date into a string similar to how excel does. */ public Date getDateCellValue() { diff --git a/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormatter.java b/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormatter.java new file mode 100644 index 000000000..c1701e22b --- /dev/null +++ b/src/java/org/apache/poi/hssf/usermodel/HSSFDataFormatter.java @@ -0,0 +1,703 @@ +/* ==================================================================== + 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.hssf.usermodel; + +import java.text.DecimalFormat; +import java.text.FieldPosition; +import java.text.Format; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * HSSFDataFormatter contains methods for formatting the value stored in an + * HSSFCell. This can be useful for reports and GUI presentations when you + * need to display data exactly as it appears in Excel. Supported formats + * include currency, SSN, percentages, decimals, dates, phone numbers, zip + * codes, etc. + *

+ * Internally, formats will be implemented using subclasses of {@link Format} + * such as {@link DecimalFormat} and {@link SimpleDateFormat}. Therefore the + * formats used by this class must obey the same pattern rules as these Format + * subclasses. This means that only legal number pattern characters ("0", "#", + * ".", "," etc.) may appear in number formats. Other characters can be + * inserted before or after the number pattern to form a + * prefix or suffix. + *

+ *

+ * For example the Excel pattern "$#,##0.00 "USD"_);($#,##0.00 "USD")" + * will be correctly formatted as "$1,000.00 USD" or "($1,000.00 USD)". + * However the pattern "00-00-00" is incorrectly formatted by + * DecimalFormat as "000000--". For Excel formats that are not compatible with + * DecimalFormat, you can provide your own custom {@link Format} implementation + * via HSSFDataFormatter.addFormat(String,Format). The following + * custom formats are already provided by this class: + *

+ *
+ * 
  • SSN "000-00-0000"
  • + *
  • Phone Number "(###) ###-####"
  • + *
  • Zip plus 4 "00000-0000"
  • + *
+ *
+ *

+ * If the Excel format pattern cannot be parsed successfully, then a default + * format will be used. The default number format will mimic the Excel General + * format: "#" for whole numbers and "#.##########" for decimal numbers. You + * can override the default format pattern with + * HSSFDataFormatter.setDefaultNumberFormat(Format). Note: the + * default format will only be used when a Format cannot be created from the + * cell's data format string. + * + * @author James May (james dot may at fmr dot com) + * + */ +public class HSSFDataFormatter { + + /** Pattern to find a number format: "0" or "#" */ + protected Pattern numPattern; + + /** Pattern to find days of week as text "ddd...." */ + protected Pattern daysAsText; + + /** Pattern to find "AM/PM" marker */ + protected Pattern amPmPattern; + + /** A regex to find patterns like [$$-1009] and [$�-452]. */ + protected Pattern specialPatternGroup; + + /** General format for whole numbers. */ + protected Format generalWholeNumFormat; + + /** General format for decimal numbers. */ + protected Format generalDecimalNumFormat; + + /** A default format to use when a number pattern cannot be parsed. */ + protected Format defaultNumFormat; + + /** + * A map to cache formats. + * Map formats + */ + protected Map formats; + + + /** + * Constructor + */ + public HSSFDataFormatter() { + numPattern = Pattern.compile("[0#]+"); + daysAsText = Pattern.compile("([d]{3,})", Pattern.CASE_INSENSITIVE); + amPmPattern = Pattern.compile("((A|P)[M/P]*)", Pattern.CASE_INSENSITIVE); + specialPatternGroup = Pattern.compile("(\\[\\$[^-\\]]*-[0-9A-Z]+\\])"); + generalWholeNumFormat = new DecimalFormat("#"); + generalDecimalNumFormat = new DecimalFormat("#.##########"); + formats = new HashMap(); + + // init built-in formats + init(); + } + + /** + * Initialize the formatter. Called after construction. + */ + protected void init() { + + ZipPlusFourFormat zipFormat = new ZipPlusFourFormat(); + addFormat("00000\\-0000", zipFormat); + addFormat("00000-0000", zipFormat); + + PhoneFormat phoneFormat = new PhoneFormat(); + // allow for format string variations + addFormat("[<=9999999]###\\-####;\\(###\\)\\ ###\\-####", phoneFormat); + addFormat("[<=9999999]###-####;(###) ###-####", phoneFormat); + addFormat("###\\-####;\\(###\\)\\ ###\\-####", phoneFormat); + addFormat("###-####;(###) ###-####", phoneFormat); + + SSNFormat ssnFormat = new SSNFormat(); + addFormat("000\\-00\\-0000", ssnFormat); + addFormat("000-00-0000", ssnFormat); + } + + /** + * Return a Format for the given cell if one exists, otherwise try to + * create one. This method will return null if the any of the + * following is true: + *

    + *
  • the cell's style is null
  • + *
  • the style's data format string is null or empty
  • + *
  • the format string cannot be recognized as either a number or date
  • + *
+ * + * @param cell The cell to retrieve a Format for + * @return A Format for the format String + */ + protected Format getFormat(HSSFCell cell) { + if ( cell.getCellStyle() == null) { + return null; + } + + int formatIndex = cell.getCellStyle().getDataFormat(); + String formatStr = cell.getCellStyle().getDataFormatString(); + if(formatStr == null || formatStr.trim().length() == 0) { + return null; + } + return getFormat(cell.getNumericCellValue(), formatIndex, formatStr); + } + + private Format getFormat(double cellValue, int formatIndex, String formatStr) { + Format format = (Format)formats.get(formatStr); + if (format != null) { + return format; + } else if (formatStr.equals("General")) { + if (HSSFDataFormatter.isWholeNumber(cellValue)) { + return generalWholeNumFormat; + } else { + return generalDecimalNumFormat; + } + } else { + format = createFormat(cellValue, formatIndex, formatStr); + formats.put(formatStr, format); + return format; + } + } + + /** + * Create and return a Format based on the format string from a cell's + * style. If the pattern cannot be parsed, return a default pattern. + * + * @param cell The Excel cell + * @return A Format representing the excel format. May return null. + */ + protected Format createFormat(HSSFCell cell) { + String sFormat = cell.getCellStyle().getDataFormatString(); + + int formatIndex = cell.getCellStyle().getDataFormat(); + String formatStr = cell.getCellStyle().getDataFormatString(); + return createFormat(cell.getNumericCellValue(), formatIndex, formatStr); + } + + private Format createFormat(double cellValue, int formatIndex, String sFormat) { + // remove color formatting if present + String formatStr = sFormat.replaceAll("\\[[a-zA-Z]*\\]", ""); + + // try to extract special characters like currency + Matcher m = specialPatternGroup.matcher(formatStr); + try { + while(m.find()) { + String match = m.group(); + String symbol = match.substring(match.indexOf('$') + 1, match.indexOf('-')); + if (symbol.indexOf('$') > -1) { + StringBuffer sb = new StringBuffer(); + sb.append(symbol.substring(0, symbol.indexOf('$'))); + sb.append('\\'); + sb.append(symbol.substring(symbol.indexOf('$'), symbol.length())); + symbol = sb.toString(); + } + formatStr = m.replaceAll(symbol); + } + } catch (Exception e) { + return getDefaultFormat(cellValue); + } + + if(formatStr == null || formatStr.trim().length() == 0) { + return getDefaultFormat(cellValue); + } + + Format returnVal = null; + StringBuffer sb = null; + + if(HSSFDateUtil.isADateFormat(formatIndex,formatStr) && + HSSFDateUtil.isValidExcelDate(cellValue)) { + formatStr = formatStr.replaceAll("\\\\-","-"); + formatStr = formatStr.replaceAll("\\\\,",","); + formatStr = formatStr.replaceAll("\\\\ "," "); + formatStr = formatStr.replaceAll(";@", ""); + boolean hasAmPm = false; + Matcher amPmMatcher = amPmPattern.matcher(formatStr); + while (amPmMatcher.find()) { + formatStr = amPmMatcher.replaceAll("a"); + hasAmPm = true; + } + + Matcher dateMatcher = daysAsText.matcher(formatStr); + if (dateMatcher.find()) { + String match = dateMatcher.group(0); + formatStr = dateMatcher.replaceAll(match.toUpperCase().replaceAll("D", "E")); + } + + // Convert excel date format to SimpleDateFormat. + // Excel uses lower case 'm' for both minutes and months. + // From Excel help: + /* + The "m" or "mm" code must appear immediately after the "h" or"hh" + code or immediately before the "ss" code; otherwise, Microsoft + Excel displays the month instead of minutes." + */ + + sb = new StringBuffer(); + char[] chars = formatStr.toCharArray(); + boolean mIsMonth = true; + List ms = new ArrayList(); + for(int j=0; j -1 && sb.charAt(idx -1) == '_') { + sb.deleteCharAt(idx); + sb.deleteCharAt(idx - 1); + sb.deleteCharAt(i); + i--; + } + } else if (c == ')' && i > 0 && sb.charAt(i - 1) == '_') { + sb.deleteCharAt(i); + sb.deleteCharAt(i - 1); + i--; + // remove quotes and back slashes + } else if (c == '\\' || c == '"') { + sb.deleteCharAt(i); + i--; + + // for scientific/engineering notation + } else if (c == '+' && i > 0 && sb.charAt(i - 1) == 'E') { + sb.deleteCharAt(i); + i--; + } + } + formatStr = sb.toString(); + try { + returnVal = new DecimalFormat(formatStr); + } catch(IllegalArgumentException iae) { + + // the pattern could not be parsed correctly, + // so fall back to the default number format + return getDefaultFormat(cellValue); + } + } + return returnVal; + } + + /** + * Return true if the double value represents a whole number + * @param d the double value to check + * @return true if d is a whole number + */ + private static boolean isWholeNumber(double d) { + return d == Math.floor(d); + } + + /** + * Returns a default format for a cell. + * @param cell The cell + * @return a default format + */ + protected Format getDefaultFormat(HSSFCell cell) { + return getDefaultFormat(cell.getNumericCellValue()); + } + private Format getDefaultFormat(double cellValue) { + // for numeric cells try user supplied default + if (defaultNumFormat != null) { + return defaultNumFormat; + + // otherwise use general format + } else if (isWholeNumber(cellValue)){ + return generalWholeNumFormat; + } else { + return generalDecimalNumFormat; + } + } + + /** + * Returns the formatted value of an Excel date as a String based + * on the cell's DataFormat. i.e. "Thursday, January 02, 2003" + * , "01/02/2003" , "02-Jan" , etc. + * + * @param cell The cell + * @return a formatted date string + */ + protected String getFormattedDateString(HSSFCell cell) { + Format dateFormat = getFormat(cell); + Date d = cell.getDateCellValue(); + if (dateFormat != null) { + return dateFormat.format(d); + } else { + return d.toString(); + } + } + + /** + * Returns the formatted value of an Excel number as a String + * based on the cell's DataFormat. Supported formats include + * currency, percents, decimals, phone number, SSN, etc.: + * "61.54%", "$100.00", "(800) 555-1234". + * + * @param cell The cell + * @return a formatted number string + */ + protected String getFormattedNumberString(HSSFCell cell) { + + Format numberFormat = getFormat(cell); + double d = cell.getNumericCellValue(); + if (numberFormat != null) { + return numberFormat.format(new Double(d)); + } else { + return String.valueOf(d); + } + } + + /** + * Formats the given raw cell value, based on the supplied + * format index and string, according to excel style rules. + * @see #formatCellValue(HSSFCell) + */ + public String formatRawCellContents(double value, int formatIndex, String formatString) { + // Is it a date? + if(HSSFDateUtil.isADateFormat(formatIndex,formatString) && + HSSFDateUtil.isValidExcelDate(value)) { + + Format dateFormat = getFormat(value, formatIndex, formatString); + Date d = HSSFDateUtil.getJavaDate(value); + if (dateFormat != null) { + return dateFormat.format(d); + } else { + return d.toString(); + } + } else { + // Number + Format numberFormat = getFormat(value, formatIndex, formatString); + if (numberFormat != null) { + return numberFormat.format(new Double(value)); + } else { + return String.valueOf(value); + } + } + } + + /** + *

+ * 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 + * cell value will be formatted using a default format. + *

+ *

When passed a null or blank cell, this method will return an empty + * String (""). Formulas in formula type cells will not be evaluated. + *

+ * + * @param cell The cell + * @return the formatted cell value as a String + */ + public String formatCellValue(HSSFCell cell) { + return formatCellValue(cell, null); + } + + /** + *

+ * 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 + * 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 HSSFFormulaEvaluator} 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, otherwise an + * IllegalArgumentException may be thrown. + *

+ * + * @param cell The cell + * @param evaluator The HSSFFormulaEvaluator (can be null) + * @return a string value of the cell + * @throws IllegalArgumentException if cell type is + * HSSFCell.CELL_TYPE_FORMULA and evaluator is not null + * and the evlaluator's currentRow has not been set. + */ + public String formatCellValue(HSSFCell cell, + HSSFFormulaEvaluator evaluator) throws IllegalArgumentException { + + String value = ""; + if (cell == null) { + return value; + } + + int cellType = cell.getCellType(); + if (evaluator != null && cellType == HSSFCell.CELL_TYPE_FORMULA) { + try { + cellType = evaluator.evaluateFormulaCell(cell); + } catch (Throwable t) { + throw new IllegalArgumentException("Did you forget to set the current" + + " row on the HSSFFormulaEvaluator?", t); + } + } + switch (cellType) + { + case HSSFCell.CELL_TYPE_FORMULA : + // should only occur if evaluator is null + value = cell.getCellFormula(); + break; + + case HSSFCell.CELL_TYPE_NUMERIC : + + if (HSSFDateUtil.isCellDateFormatted(cell)) { + value = getFormattedDateString(cell); + } else { + value = getFormattedNumberString(cell); + } + break; + + case HSSFCell.CELL_TYPE_STRING : + value = cell.getRichStringCellValue().getString(); + break; + + case HSSFCell.CELL_TYPE_BOOLEAN : + value = String.valueOf(cell.getBooleanCellValue()); + } + return value; + } + + + /** + *

+ * Sets a default number format to be used when the Excel format cannot be + * parsed successfully. Note: This is a fall back for when an error + * occurs while parsing an Excel number format pattern. This will not + * affect cells with the General format. + *

+ *

+ * The value that will be passed to the Format's format method (specified + * by java.text.Format#format) will be a double value from a + * numeric cell. Therefore the code in the format method should expect a + * Number value. + *

+ * + * @param format A Format instance to be used as a default + * @see java.text.Format#format + */ + public void setDefaultNumberFormat(Format format) { + Iterator itr = formats.entrySet().iterator(); + while(itr.hasNext()) { + Map.Entry entry = (Map.Entry)itr.next(); + if (entry.getValue() == generalDecimalNumFormat + || entry.getValue() == generalWholeNumFormat) { + entry.setValue(format); + } + } + defaultNumFormat = format; + } + + /** + * Adds a new format to the available formats. + *

+ * The value that will be passed to the Format's format method (specified + * by java.text.Format#format) will be a double value from a + * numeric cell. Therefore the code in the format method should expect a + * Number value. + *

+ * @param excelFormatStr The data format string + * @param format A Format instance + */ + public void addFormat(String excelFormatStr, Format format) { + formats.put(excelFormatStr, format); + } + + // Some custom formats + + /** + * Format class for Excel's SSN format. This class mimics Excel's built-in + * SSN formatting. + * + * @author James May + */ + static class SSNFormat extends Format { + private DecimalFormat df; + + /** Constructor */ + public SSNFormat() { + df = new DecimalFormat("000000000"); + df.setParseIntegerOnly(true); + } + + /** Format a number as an SSN */ + public String format(Number num) { + String result = df.format(num); + StringBuffer sb = new StringBuffer(); + sb.append(result.substring(0, 3)).append('-'); + sb.append(result.substring(3, 5)).append('-'); + sb.append(result.substring(5, 9)); + return sb.toString(); + } + + public StringBuffer format(Object obj, StringBuffer toAppendTo, + FieldPosition pos) { + return toAppendTo.append(format((Number)obj)); + } + + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Format class for Excel Zip + 4 format. This class mimics Excel's + * built-in formatting for Zip + 4. + * @author James May + */ + static class ZipPlusFourFormat extends Format { + private DecimalFormat df; + + /** Constructor */ + public ZipPlusFourFormat() { + df = new DecimalFormat("000000000"); + df.setParseIntegerOnly(true); + } + + /** Format a number as Zip + 4 */ + public String format(Number num) { + String result = df.format(num); + StringBuffer sb = new StringBuffer(); + sb.append(result.substring(0, 5)).append('-'); + sb.append(result.substring(5, 9)); + return sb.toString(); + } + + public StringBuffer format(Object obj, StringBuffer toAppendTo, + FieldPosition pos) { + return toAppendTo.append(format((Number)obj)); + } + + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } + + /** + * Format class for Excel phone number format. This class mimics Excel's + * built-in phone number formatting. + * @author James May + */ + static class PhoneFormat extends Format { + private DecimalFormat df; + + /** Constructor */ + public PhoneFormat() { + df = new DecimalFormat("##########"); + df.setParseIntegerOnly(true); + } + + /** Format a number as a phone number */ + public String format(Number num) { + String result = df.format(num); + StringBuffer sb = new StringBuffer(); + String seg1, seg2, seg3; + int len = result.length(); + if (len <= 4) { + return result; + } + + seg3 = result.substring(len - 4, len); + seg2 = result.substring(Math.max(0, len - 7), len - 4); + seg1 = result.substring(Math.max(0, len - 10), Math.max(0, len - 7)); + + if(seg1 != null && seg1.trim().length() > 0) { + sb.append('(').append(seg1).append(") "); + } + if(seg2 != null && seg2.trim().length() > 0) { + sb.append(seg2).append('-'); + } + sb.append(seg3); + return sb.toString(); + } + + public StringBuffer format(Object obj, StringBuffer toAppendTo, + FieldPosition pos) { + return toAppendTo.append(format((Number)obj)); + } + + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } +} diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java new file mode 100644 index 000000000..39baedd88 --- /dev/null +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java @@ -0,0 +1,282 @@ +/* ==================================================================== + Licensed to the Apache Software Foundation (ASF) under one or more + contributor license agreements. See the NOTICE file distributed with + this work for additional information regarding copyright ownership. + The ASF licenses this file to You under the Apache License, Version 2.0 + (the "License"); you may not use this file except in compliance with + the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +==================================================================== */ + +package org.apache.poi.hssf.usermodel; + +import java.text.DecimalFormat; +import java.text.Format; +import java.util.Iterator; + +import junit.framework.TestCase; + +/** + * Unit tests for HSSFDataFormatter.java + * + * @author James May (james dot may at fmr dot com) + * + */ +public class TestHSSFDataFormatter extends TestCase { + + HSSFDataFormatter formatter; + HSSFWorkbook wb; + + public TestHSSFDataFormatter() { + // create the formatter to test + formatter = new HSSFDataFormatter(); + + // create a workbook to test with + wb = new HSSFWorkbook(); + HSSFSheet sheet = wb.createSheet(); + HSSFDataFormat format = wb.createDataFormat(); + + // create a row and put some cells in it + HSSFRow row = sheet.createRow((short)0); + + // date value for July 8 1901 1:19 PM + double dateNum = 555.555; + + //valid date formats -- all should have "Jul" in output + String[] goodDatePatterns = new String[] { + "[$-F800]dddd\\,\\ mmmm\\ dd\\,\\ yyyy", + "mmm/d/yy\\ h:mm PM;@", + "mmmm/d/yy\\ h:mm;@", + "mmmm/d;@", + "mmmm/d/yy;@", + "mmm/dd/yy;@", + "[$-409]d\\-mmm;@", + "[$-409]d\\-mmm\\-yy;@", + "[$-409]dd\\-mmm\\-yy;@", + "[$-409]mmm\\-yy;@", + "[$-409]mmmm\\-yy;@", + "[$-409]mmmm\\ d\\,\\ yyyy;@", + "[$-409]mmm/d/yy\\ h:mm:ss;@", + "[$-409]mmmm/d/yy\\ h:mm:ss am;@", + "[$-409]mmmmm;@", + "[$-409]mmmmm\\-yy;@", + "mmmm/d/yyyy;@", + "[$-409]d\\-mmm\\-yyyy;@" + }; + + // valid number formats + String[] goodNumPatterns = new String[] { + "#,##0.0000", + "#,##0;[Red]#,##0", + "(#,##0.00_);(#,##0.00)", + "($#,##0.00_);[Red]($#,##0.00)", + "$#,##0.00", + "[$�-809]#,##0.00", + "[$�-2] #,##0.00", + "0000.00000%", + "0.000E+00", + "0.00E+00", + }; + + // invalid date formats -- will throw exception in DecimalFormat ctor + String[] badNumPatterns = new String[] { + "#,#$'#0.0000", + "'#','#ABC#0;##,##0", + "000 '123 4'5'6 000", + "#''0#0'1#10L16EE" + }; + + // create cells with good date patterns + for (int i = 0; i < goodDatePatterns.length; i++) { + HSSFCell cell = row.createCell((short) i); + cell.setCellValue(dateNum); + HSSFCellStyle cellStyle = wb.createCellStyle(); + cellStyle.setDataFormat(format.getFormat(goodDatePatterns[i])); + cell.setCellStyle(cellStyle); + } + row = sheet.createRow(1); + + // create cells with num patterns + for (int i = 0; i < goodNumPatterns.length; i++) { + HSSFCell cell = row.createCell((short) i); + cell.setCellValue(-1234567890.12345); + HSSFCellStyle cellStyle = wb.createCellStyle(); + cellStyle.setDataFormat(format.getFormat(goodNumPatterns[i])); + cell.setCellStyle(cellStyle); + } + row = sheet.createRow(2); + + // create cells with bad num patterns + for (int i = 0; i < badNumPatterns.length; i++) { + HSSFCell cell = row.createCell((short) i); + cell.setCellValue(1234567890.12345); + HSSFCellStyle cellStyle = wb.createCellStyle(); + cellStyle.setDataFormat(format.getFormat(badNumPatterns[i])); + cell.setCellStyle(cellStyle); + } + + // Built in formats + + { // Zip + 4 format + row = sheet.createRow(3); + HSSFCell cell = row.createCell((short) 0); + cell.setCellValue(123456789); + HSSFCellStyle cellStyle = wb.createCellStyle(); + cellStyle.setDataFormat(format.getFormat("00000-0000")); + cell.setCellStyle(cellStyle); + } + + { // Phone number format + row = sheet.createRow(4); + HSSFCell cell = row.createCell((short) 0); + cell.setCellValue(5551234567D); + HSSFCellStyle cellStyle = wb.createCellStyle(); + cellStyle.setDataFormat(format.getFormat("[<=9999999]###-####;(###) ###-####")); + cell.setCellStyle(cellStyle); + } + + { // SSN format + row = sheet.createRow(5); + HSSFCell cell = row.createCell((short) 0); + cell.setCellValue(444551234); + HSSFCellStyle cellStyle = wb.createCellStyle(); + cellStyle.setDataFormat(format.getFormat("000-00-0000")); + cell.setCellStyle(cellStyle); + } + + { // formula cell + row = sheet.createRow(6); + HSSFCell cell = row.createCell((short) 0); + cell.setCellType(HSSFCell.CELL_TYPE_FORMULA); + cell.setCellFormula("SUM(12.25,12.25)/100"); + HSSFCellStyle cellStyle = wb.createCellStyle(); + cellStyle.setDataFormat(format.getFormat("##.00%;")); + cell.setCellStyle(cellStyle); + } + } + + /** + * Test getting formatted values from numeric and date cells. + */ + public void testGetFormattedCellValueHSSFCell() { + // Valid date formats -- cell values should be date formatted & not "555.555" + HSSFRow row = wb.getSheetAt(0).getRow(0); + Iterator it = row.cellIterator(); + System.out.println("==== VALID DATE FORMATS ===="); + while (it.hasNext()) { + HSSFCell cell = (HSSFCell) it.next(); + System.out.println(formatter.formatCellValue(cell)); + + // should not be equal to "555.555" + assertTrue( ! "555.555".equals(formatter.formatCellValue(cell))); + + // should contain "Jul" in the String + assertTrue( formatter.formatCellValue(cell).indexOf("Jul") > -1); + } + + // test number formats + row = wb.getSheetAt(0).getRow(1); + it = row.cellIterator(); + System.out.println("\n==== VALID NUMBER FORMATS ===="); + while (it.hasNext()) { + HSSFCell cell = (HSSFCell) it.next(); + System.out.println(formatter.formatCellValue(cell)); + + // should not be equal to "1234567890.12345" + assertTrue( ! "1234567890.12345".equals(formatter.formatCellValue(cell))); + } + + // test bad number formats + row = wb.getSheetAt(0).getRow(2); + it = row.cellIterator(); + System.out.println("\n==== INVALID NUMBER FORMATS ===="); + while (it.hasNext()) { + HSSFCell cell = (HSSFCell) it.next(); + System.out.println(formatter.formatCellValue(cell)); + // should be equal to "1234567890.12345" + assertEquals("1234567890.12345", formatter.formatCellValue(cell)); + } + + // test Zip+4 format + row = wb.getSheetAt(0).getRow(3); + HSSFCell cell = row.getCell(0); + System.out.println("\n==== ZIP FORMAT ===="); + System.out.println(formatter.formatCellValue(cell)); + assertEquals("12345-6789", formatter.formatCellValue(cell)); + + // test phone number format + row = wb.getSheetAt(0).getRow(4); + cell = row.getCell(0); + System.out.println("\n==== PHONE FORMAT ===="); + System.out.println(formatter.formatCellValue(cell)); + assertEquals("(555) 123-4567", formatter.formatCellValue(cell)); + + // test SSN format + row = wb.getSheetAt(0).getRow(5); + cell = row.getCell(0); + System.out.println("\n==== SSN FORMAT ===="); + System.out.println(formatter.formatCellValue(cell)); + assertEquals("444-55-1234", formatter.formatCellValue(cell)); + + // null test-- null cell should result in empty String + assertEquals(formatter.formatCellValue(null), ""); + + // null test-- null cell should result in empty String + assertEquals(formatter.formatCellValue(null), ""); + + } + + public void testGetFormattedCellValueHSSFCellHSSFFormulaEvaluator() { + // test formula format + HSSFRow row = wb.getSheetAt(0).getRow(6); + HSSFCell cell = row.getCell(0); + System.out.println("\n==== FORMULA CELL ===="); + + // first without a formula evaluator + System.out.println(formatter.formatCellValue(cell) + "\t (without evaluator)"); + assertEquals("SUM(12.25,12.25)/100", formatter.formatCellValue(cell)); + + // now with a formula evaluator + HSSFFormulaEvaluator evaluator = new HSSFFormulaEvaluator(wb.getSheetAt(0), wb); + //! must set current row ! + evaluator.setCurrentRow(row); + System.out.println(formatter.formatCellValue(cell, evaluator) + "\t\t\t (with evaluator)"); + assertEquals("24.50%", formatter.formatCellValue(cell,evaluator)); + } + + + + /** + * Test using a default number format. The format should be used when a + * format pattern cannot be parsed by DecimalFormat. + */ + public void testSetDefaultNumberFormat() { + HSSFRow row = wb.getSheetAt(0).getRow(2); + Iterator it = row.cellIterator(); + Format defaultFormat = new DecimalFormat("Balance $#,#00.00 USD;Balance -$#,#00.00 USD"); + formatter.setDefaultNumberFormat(defaultFormat); + double value = 10d; + System.out.println("\n==== DEFAULT NUMBER FORMAT ===="); + while (it.hasNext()) { + HSSFCell cell = (HSSFCell) it.next(); + cell.setCellValue(cell.getNumericCellValue() * Math.random() / 1000000 - 1000); + System.out.println(formatter.formatCellValue(cell)); + assertTrue(formatter.formatCellValue(cell).startsWith("Balance ")); + assertTrue(formatter.formatCellValue(cell).endsWith(" USD")); + } + } + + public static void main(String [] args) { + System.out + .println("Testing org.apache.poi.hssf.usermodel.TestHSSFDataFormatter"); + junit.textui.TestRunner.run(TestHSSFDataFormatter.class); + } + +}