diff --git a/src/documentation/content/xdocs/status.xml b/src/documentation/content/xdocs/status.xml index 30170329c..9cb0bccc2 100644 --- a/src/documentation/content/xdocs/status.xml +++ b/src/documentation/content/xdocs/status.xml @@ -34,6 +34,7 @@ + 50841 - Improved SpreadSheet DataFormatter to handle scientific notation, invalid dates and format spacers 49381 - Correct createFreezePane in XSSF, so that the left row/column matches the documentation + HSSF 49253 - When setting repeating rows and columns for XSSF, don't break the print settings if they were already there 49219 - ExternalNameRecord support for DDE Link entries without an operation diff --git a/src/examples/src/org/apache/poi/ss/examples/ToCSV.java b/src/examples/src/org/apache/poi/ss/examples/ToCSV.java index eefe3b550..0343ab6d7 100644 --- a/src/examples/src/org/apache/poi/ss/examples/ToCSV.java +++ b/src/examples/src/org/apache/poi/ss/examples/ToCSV.java @@ -389,7 +389,7 @@ public class ToCSV { // formatted String encapsulating the cells contents. this.workbook = WorkbookFactory.create(fis); this.evaluator = this.workbook.getCreationHelper().createFormulaEvaluator(); - this.formatter = new DataFormatter(); + this.formatter = new DataFormatter(true); } finally { if(fis != null) { diff --git a/src/java/org/apache/poi/ss/usermodel/DataFormatter.java b/src/java/org/apache/poi/ss/usermodel/DataFormatter.java index 38afc61d3..6c9412383 100644 --- a/src/java/org/apache/poi/ss/usermodel/DataFormatter.java +++ b/src/java/org/apache/poi/ss/usermodel/DataFormatter.java @@ -63,8 +63,27 @@ import java.text.*; * default format will only be used when a Format cannot be created from the * cell's data format string. * + *

+ * Note that by default formatted numeric values are trimmed. + * Excel formats can contain spacers and padding and the default behavior is to strip them off. + *

+ *

Example:

+ *

+ * Consider a numeric cell with a value 12.343 and format "##.##_ ". + * The trailing underscore and space ("_ ") in the format adds a space to the end and Excel formats this cell as "12.34 ", + * but DataFormatter trims the formatted value and returns "12.34". + *

+ * You can enable spaces by passing the emulateCsv=true flag in the DateFormatter cosntructor. + * If set to true, then the output tries to conform to what you get when you take an xls or xlsx in Excel and Save As CSV file: + * * @author James May (james dot may at fmr dot com) - * + * @author Robert Kish + * */ public class DataFormatter { @@ -80,7 +99,7 @@ public class DataFormatter { /** A regex to find patterns like [$$-1009] and [$?-452]. */ private static final Pattern specialPatternGroup = Pattern.compile("(\\[\\$[^-\\]]*-[0-9A-Z]+\\])"); - /** + /** * A regex to match the colour formattings rules. * Allowed colours are: Black, Blue, Cyan, Green, * Magenta, Red, White, Yellow, "Color n" (1<=n<=56) @@ -89,7 +108,18 @@ public class DataFormatter { Pattern.compile("(\\[BLACK\\])|(\\[BLUE\\])|(\\[CYAN\\])|(\\[GREEN\\])|" + "(\\[MAGENTA\\])|(\\[RED\\])|(\\[WHITE\\])|(\\[YELLOW\\])|" + "(\\[COLOR\\s*\\d\\])|(\\[COLOR\\s*[0-5]\\d\\])", Pattern.CASE_INSENSITIVE); - + + /** + * Cells formatted with a date or time format and which contain invalid date or time values + * show 255 pound signs ("#"). + */ + private static final String invalidDateTimeString; + static { + StringBuilder buf = new StringBuilder(); + for(int i = 0; i < 255; i++) buf.append('#'); + invalidDateTimeString = buf.toString(); + } + /** * The decimal symbols of the locale used for formatting values. */ @@ -115,11 +145,33 @@ public class DataFormatter { */ private final Map formats; + private boolean emulateCsv = false; + /** * Creates a formatter using the {@link Locale#getDefault() default locale}. */ public DataFormatter() { + this(false); + } + + /** + * Creates a formatter using the {@link Locale#getDefault() default locale}. + * + * @param emulateCsv whether to emulate CSV output. + */ + public DataFormatter(boolean emulateCsv) { this(Locale.getDefault()); + this.emulateCsv = emulateCsv; + } + + /** + * Creates a formatter using the given locale. + * + * @param emulateCsv whether to emulate CSV output. + */ + public DataFormatter(Locale locale, boolean emulateCsv) { + this(locale); + this.emulateCsv = emulateCsv; } /** @@ -177,27 +229,42 @@ public class DataFormatter { return getFormat(cell.getNumericCellValue(), formatIndex, formatStr); } - private Format getFormat(double cellValue, int formatIndex, String formatStr) { - // Excel supports positive/negative/zero, but java - // doesn't, so we need to do it specially - if(formatStr.indexOf(';') != formatStr.lastIndexOf(';')) { - int lastAt = formatStr.lastIndexOf(';'); - String zeroFormat = formatStr.substring(lastAt+1); - String normalFormat = formatStr.substring(0,lastAt); - if(cellValue == 0.0) { - formatStr = zeroFormat; - } else { - formatStr = normalFormat; - } + private Format getFormat(double cellValue, int formatIndex, String formatStrIn) { + String formatStr = formatStrIn; + // Excel supports positive/negative/zero, but java + // doesn't, so we need to do it specially + final int firstAt = formatStr.indexOf(';'); + final int lastAt = formatStr.lastIndexOf(';'); + // p and p;n are ok by default. p;n;z and p;n;z;s need to be fixed. + if (firstAt != -1 && firstAt != lastAt) { + final int secondAt = formatStr.indexOf(';', firstAt + 1); + if (secondAt == lastAt) { // p;n;z + if (cellValue == 0.0) { + formatStr = formatStr.substring(lastAt + 1); + } else { + formatStr = formatStr.substring(0, lastAt); + } + } else { + if (cellValue == 0.0) { // p;n;z;s + formatStr = formatStr.substring(secondAt + 1, lastAt); + } else { + formatStr = formatStr.substring(0, secondAt); + } + } + } + + // Excel's # with value 0 will output empty where Java will output 0. This hack removes the # from the format. + if (emulateCsv && cellValue == 0.0 && formatStr.contains("#") && !formatStr.contains("0")) { + formatStr = formatStr.replaceAll("#", ""); } - - // See if we already have it cached + + // See if we already have it cached Format format = formats.get(formatStr); if (format != null) { return format; } if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) { - if (DataFormatter.isWholeNumber(cellValue)) { + if (isWholeNumber(cellValue)) { return generalWholeNumFormat; } return generalDecimalNumFormat; @@ -261,7 +328,6 @@ public class DataFormatter { return getDefaultFormat(cellValue); } - if(DateUtil.isADateFormat(formatIndex,formatStr) && DateUtil.isValidExcelDate(cellValue)) { return createDateFormat(formatStr, cellValue); @@ -269,6 +335,10 @@ public class DataFormatter { if (numPattern.matcher(formatStr).find()) { return createNumberFormat(formatStr, cellValue); } + + if (emulateCsv) { + return new ConstantStringFormat(cleanFormatForNumber(formatStr)); + } // TODO - when does this occur? return null; } @@ -311,9 +381,33 @@ public class DataFormatter { char[] chars = formatStr.toCharArray(); boolean mIsMonth = true; List ms = new ArrayList(); + boolean isElapsed = false; for(int j=0; j 0 && sb.charAt((i-1)) == '\\') { - // It's escaped, don't worry - continue; - } else { - if(i < sb.length()-1) { - // Remove the character we're supposed - // to match the space of / pad to the - // column width with - sb.deleteCharAt(i+1); - } - // Remove the _ too - sb.deleteCharAt(i); - } + + if (emulateCsv) { + // Requested spacers with "_" are replaced by a single space. + // Full-column-width padding "*" are removed. + // Not processing fractions at this time. Replace ? with space. + // This matches CSV output. + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (c == '_' || c == '*' || c == '?') { + if (i > 0 && sb.charAt((i - 1)) == '\\') { + // It's escaped, don't worry + continue; + } + if (c == '?') { + sb.setCharAt(i, ' '); + } else if (i < sb.length() - 1) { + // Remove the character we're supposed + // to match the space of / pad to the + // column width with + if (c == '_') { + sb.setCharAt(i + 1, ' '); + } else { + sb.deleteCharAt(i + 1); + } + // Remove the character too + sb.deleteCharAt(i); + } + } + } + } else { + // If they requested spacers, with "_", + // remove those as we don't do spacing + // If they requested full-column-width + // padding, with "*", remove those too + for (int i = 0; i < sb.length(); i++) { + char c = sb.charAt(i); + if (c == '_' || c == '*') { + if (i > 0 && sb.charAt((i - 1)) == '\\') { + // It's escaped, don't worry + continue; + } + if (i < sb.length() - 1) { + // Remove the character we're supposed + // to match the space of / pad to the + // column width with + sb.deleteCharAt(i + 1); + } + // Remove the _ too + sb.deleteCharAt(i); + } } } - + // Now, handle the other aspects like // quoting and scientific notation for(int i = 0; i < sb.length(); i++) { @@ -415,8 +538,14 @@ public class DataFormatter { } } + return sb.toString(); + } + + private Format createNumberFormat(String formatStr, double cellValue) { + final String format = cleanFormatForNumber(formatStr); + try { - DecimalFormat df = new DecimalFormat(sb.toString(), decimalSymbols); + DecimalFormat df = new DecimalFormat(format, decimalSymbols); setExcelStyleRoundingMode(df); return df; } catch(IllegalArgumentException iae) { @@ -522,22 +651,32 @@ public class DataFormatter { */ public String formatRawCellContents(double value, int formatIndex, String formatString, boolean use1904Windowing) { // Is it a date? - if(DateUtil.isADateFormat(formatIndex,formatString) && - DateUtil.isValidExcelDate(value)) { - Format dateFormat = getFormat(value, formatIndex, formatString); - if(dateFormat instanceof ExcelStyleDateFormatter) { - // Hint about the raw excel value - ((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted(value); + if(DateUtil.isADateFormat(formatIndex,formatString)) { + if(DateUtil.isValidExcelDate(value)) { + Format dateFormat = getFormat(value, formatIndex, formatString); + if(dateFormat instanceof ExcelStyleDateFormatter) { + // Hint about the raw excel value + ((ExcelStyleDateFormatter)dateFormat).setDateToBeFormatted(value); + } + Date d = DateUtil.getJavaDate(value, use1904Windowing); + return performDateFormatting(d, dateFormat); } - Date d = DateUtil.getJavaDate(value, use1904Windowing); - return performDateFormatting(d, dateFormat); + // RK: Invalid dates are 255 #s. + if (emulateCsv) { + return invalidDateTimeString; + } } // else Number - Format numberFormat = getFormat(value, formatIndex, formatString); - if (numberFormat == null) { - return String.valueOf(value); - } - return numberFormat.format(new Double(value)); + Format numberFormat = getFormat(value, formatIndex, formatString); + if (numberFormat == null) { + return String.valueOf(value); + } + // RK: This hack handles scientific notation by adding the missing + back. + String result = numberFormat.format(new Double(value)); + if (result.contains("E") && !result.contains("E-")) { + result = result.replaceFirst("E", "E+"); + } + return result; } /** @@ -669,9 +808,21 @@ public class DataFormatter { * on Java 1.5. */ public static void setExcelStyleRoundingMode(DecimalFormat format) { + setExcelStyleRoundingMode(format, RoundingMode.HALF_UP); + } + + /** + * Enables custom rounding mode + * on the Decimal Format if possible. + * This will work for Java 1.6, but isn't possible + * on Java 1.5. + * @param format DecimalFormat + * @param roundingMode RoundingMode + */ + public static void setExcelStyleRoundingMode(DecimalFormat format, RoundingMode roundingMode) { try { Method srm = format.getClass().getMethod("setRoundingMode", RoundingMode.class); - srm.invoke(format, RoundingMode.HALF_UP); + srm.invoke(format, roundingMode); } catch(NoSuchMethodException e) { // Java 1.5 } catch(IllegalAccessException iae) { @@ -684,7 +835,7 @@ public class DataFormatter { // Not much we can do here } } - + /** * Format class for Excel's SSN format. This class mimics Excel's built-in * SSN formatting. @@ -794,4 +945,31 @@ public class DataFormatter { return df.parseObject(source, pos); } } + + /** + * Format class that does nothing and always returns a constant string. + * + * This format is used to simulate Excel's handling of a format string + * of all # when the value is 0. Excel will output "", Java will output "0". + * + * @see DataFormatter#createFormat(double, int, String) + */ + @SuppressWarnings("serial") + private static final class ConstantStringFormat extends Format { + private static final DecimalFormat df = createIntegerOnlyFormat("##########"); + private final String str; + public ConstantStringFormat(String s) { + str = s; + } + + @Override + public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { + return toAppendTo.append(str); + } + + @Override + public Object parseObject(String source, ParsePosition pos) { + return df.parseObject(source, pos); + } + } } diff --git a/src/java/org/apache/poi/ss/usermodel/DateUtil.java b/src/java/org/apache/poi/ss/usermodel/DateUtil.java index 787d8d0ec..cf445f9fa 100644 --- a/src/java/org/apache/poi/ss/usermodel/DateUtil.java +++ b/src/java/org/apache/poi/ss/usermodel/DateUtil.java @@ -52,7 +52,9 @@ public class DateUtil { */ private static final Pattern date_ptrn1 = Pattern.compile("^\\[\\$\\-.*?\\]"); private static final Pattern date_ptrn2 = Pattern.compile("^\\[[a-zA-Z]+\\]"); - private static final Pattern date_ptrn3 = Pattern.compile("^[yYmMdDhHsS\\-/,. :\"\\\\]+0?[ampAMP/]*$"); + private static final Pattern date_ptrn3 = Pattern.compile("^[\\[\\]yYmMdDhHsS\\-/,. :\"\\\\]+0*[ampAMP/]*$"); + // elapsed time patterns: [h],[m] and [s] + private static final Pattern date_ptrn4 = Pattern.compile("^\\[([hH]+|[mM]+|[sS]+)\\]"); /** * Given a Date, converts it into a double representing its internal Excel representation, @@ -258,7 +260,12 @@ public class DateUtil { sb.append(c); } fs = sb.toString(); - + + // short-circuit if it indicates elapsed time: [h], [m] or [s] + if(date_ptrn4.matcher(fs).matches()){ + return true; + } + // If it starts with [$-...], then could be a date, but // who knows what that starting bit is all about fs = date_ptrn1.matcher(fs).replaceAll(""); diff --git a/src/java/org/apache/poi/ss/usermodel/ExcelStyleDateFormatter.java b/src/java/org/apache/poi/ss/usermodel/ExcelStyleDateFormatter.java index 91ea083b5..a1307ee06 100644 --- a/src/java/org/apache/poi/ss/usermodel/ExcelStyleDateFormatter.java +++ b/src/java/org/apache/poi/ss/usermodel/ExcelStyleDateFormatter.java @@ -16,130 +16,157 @@ ==================================================================== */ package org.apache.poi.ss.usermodel; -import java.util.regex.Pattern; -import java.util.regex.Matcher; import java.util.*; import java.math.RoundingMode; import java.text.*; /** * A wrapper around a {@link SimpleDateFormat} instance, - * which handles a few Excel-style extensions that - * are not supported by {@link SimpleDateFormat}. + * which handles a few Excel-style extensions that + * are not supported by {@link SimpleDateFormat}. * Currently, the extensions are around the handling - * of elapsed time, eg rendering 1 day 2 hours - * as 26 hours. + * of elapsed time, eg rendering 1 day 2 hours + * as 26 hours. */ public class ExcelStyleDateFormatter extends SimpleDateFormat { - public static final char MMMMM_START_SYMBOL = '\ue001'; - public static final char MMMMM_TRUNCATE_SYMBOL = '\ue002'; - public static final char H_BRACKET_SYMBOL = '\ue010'; - public static final char HH_BRACKET_SYMBOL = '\ue011'; - public static final char M_BRACKET_SYMBOL = '\ue012'; - public static final char MM_BRACKET_SYMBOL = '\ue013'; - public static final char S_BRACKET_SYMBOL = '\ue014'; - public static final char SS_BRACKET_SYMBOL = '\ue015'; - - private DecimalFormat format1digit = new DecimalFormat("0"); - private DecimalFormat format2digits = new DecimalFormat("00"); - { - DataFormatter.setExcelStyleRoundingMode(format1digit); - DataFormatter.setExcelStyleRoundingMode(format2digits); - } - - private double dateToBeFormatted = 0.0; - - public ExcelStyleDateFormatter() { - super(); - } - public ExcelStyleDateFormatter(String pattern) { - super(processFormatPattern(pattern)); - } - public ExcelStyleDateFormatter(String pattern, - DateFormatSymbols formatSymbols) { - super(processFormatPattern(pattern), formatSymbols); - } - public ExcelStyleDateFormatter(String pattern, Locale locale) { - super(processFormatPattern(pattern), locale); - } + public static final char MMMMM_START_SYMBOL = '\ue001'; + public static final char MMMMM_TRUNCATE_SYMBOL = '\ue002'; + public static final char H_BRACKET_SYMBOL = '\ue010'; + public static final char HH_BRACKET_SYMBOL = '\ue011'; + public static final char M_BRACKET_SYMBOL = '\ue012'; + public static final char MM_BRACKET_SYMBOL = '\ue013'; + public static final char S_BRACKET_SYMBOL = '\ue014'; + public static final char SS_BRACKET_SYMBOL = '\ue015'; + public static final char L_BRACKET_SYMBOL = '\ue016'; + public static final char LL_BRACKET_SYMBOL = '\ue017'; - /** - * Takes a format String, and replaces Excel specific bits - * with our detection sequences - */ - private static String processFormatPattern(String f) { - f = f.replaceAll("MMMMM", MMMMM_START_SYMBOL + "MMM" + MMMMM_TRUNCATE_SYMBOL); - f = f.replaceAll("\\[H\\]", String.valueOf(H_BRACKET_SYMBOL)); - f = f.replaceAll("\\[HH\\]", String.valueOf(HH_BRACKET_SYMBOL)); - f = f.replaceAll("\\[m\\]", String.valueOf(M_BRACKET_SYMBOL)); - f = f.replaceAll("\\[mm\\]", String.valueOf(MM_BRACKET_SYMBOL)); - f = f.replaceAll("\\[s\\]", String.valueOf(S_BRACKET_SYMBOL)); - f = f.replaceAll("\\[ss\\]", String.valueOf(SS_BRACKET_SYMBOL)); - return f; - } + private DecimalFormat format1digit = new DecimalFormat("0"); + private DecimalFormat format2digits = new DecimalFormat("00"); - /** - * Used to let us know what the date being - * formatted is, in Excel terms, which we - * may wish to use when handling elapsed - * times. - */ - public void setDateToBeFormatted(double date) { - this.dateToBeFormatted = date; - } - - @Override - public StringBuffer format(Date date, StringBuffer paramStringBuffer, - FieldPosition paramFieldPosition) { - // Do the normal format - String s = super.format(date, paramStringBuffer, paramFieldPosition).toString(); - - // Now handle our special cases - if(s.indexOf(MMMMM_START_SYMBOL) != -1) { - s = s.replaceAll( - MMMMM_START_SYMBOL + "(\\w)\\w+" + MMMMM_TRUNCATE_SYMBOL, - "$1" - ); - } - - if(s.indexOf(H_BRACKET_SYMBOL) != -1 || - s.indexOf(HH_BRACKET_SYMBOL) != -1) { - double hours = dateToBeFormatted * 24; - s = s.replaceAll( - String.valueOf(H_BRACKET_SYMBOL), - format1digit.format(hours) - ); - s = s.replaceAll( - String.valueOf(HH_BRACKET_SYMBOL), - format2digits.format(hours) - ); - } - - if(s.indexOf(M_BRACKET_SYMBOL) != -1 || - s.indexOf(MM_BRACKET_SYMBOL) != -1) { - double minutes = dateToBeFormatted * 24 * 60; - s = s.replaceAll( - String.valueOf(M_BRACKET_SYMBOL), - format1digit.format(minutes) - ); - s = s.replaceAll( - String.valueOf(MM_BRACKET_SYMBOL), - format2digits.format(minutes) - ); - } - if(s.indexOf(S_BRACKET_SYMBOL) != -1 || - s.indexOf(SS_BRACKET_SYMBOL) != -1) { - double seconds = dateToBeFormatted * 24 * 60 * 60; - s = s.replaceAll( - String.valueOf(S_BRACKET_SYMBOL), - format1digit.format(seconds) - ); - s = s.replaceAll( - String.valueOf(SS_BRACKET_SYMBOL), - format2digits.format(seconds) - ); - } + private DecimalFormat format3digit = new DecimalFormat("0"); + private DecimalFormat format4digits = new DecimalFormat("00"); - return new StringBuffer(s); - } + { + DataFormatter.setExcelStyleRoundingMode(format1digit, RoundingMode.DOWN); + DataFormatter.setExcelStyleRoundingMode(format2digits, RoundingMode.DOWN); + DataFormatter.setExcelStyleRoundingMode(format3digit); + DataFormatter.setExcelStyleRoundingMode(format4digits); + } + + private double dateToBeFormatted = 0.0; + + public ExcelStyleDateFormatter() { + super(); + } + + public ExcelStyleDateFormatter(String pattern) { + super(processFormatPattern(pattern)); + } + + public ExcelStyleDateFormatter(String pattern, + DateFormatSymbols formatSymbols) { + super(processFormatPattern(pattern), formatSymbols); + } + + public ExcelStyleDateFormatter(String pattern, Locale locale) { + super(processFormatPattern(pattern), locale); + } + + /** + * Takes a format String, and replaces Excel specific bits + * with our detection sequences + */ + private static String processFormatPattern(String f) { + String t = f.replaceAll("MMMMM", MMMMM_START_SYMBOL + "MMM" + MMMMM_TRUNCATE_SYMBOL); + t = t.replaceAll("\\[H\\]", String.valueOf(H_BRACKET_SYMBOL)); + t = t.replaceAll("\\[HH\\]", String.valueOf(HH_BRACKET_SYMBOL)); + t = t.replaceAll("\\[m\\]", String.valueOf(M_BRACKET_SYMBOL)); + t = t.replaceAll("\\[mm\\]", String.valueOf(MM_BRACKET_SYMBOL)); + t = t.replaceAll("\\[s\\]", String.valueOf(S_BRACKET_SYMBOL)); + t = t.replaceAll("\\[ss\\]", String.valueOf(SS_BRACKET_SYMBOL)); + t = t.replaceAll("s.000", "s.S"); + t = t.replaceAll("s.00", "s." + LL_BRACKET_SYMBOL); + t = t.replaceAll("s.0", "s." + L_BRACKET_SYMBOL); + return t; + } + + /** + * Used to let us know what the date being + * formatted is, in Excel terms, which we + * may wish to use when handling elapsed + * times. + */ + public void setDateToBeFormatted(double date) { + this.dateToBeFormatted = date; + } + + @Override + public StringBuffer format(Date date, StringBuffer paramStringBuffer, + FieldPosition paramFieldPosition) { + // Do the normal format + String s = super.format(date, paramStringBuffer, paramFieldPosition).toString(); + + // Now handle our special cases + if (s.indexOf(MMMMM_START_SYMBOL) != -1) { + s = s.replaceAll( + MMMMM_START_SYMBOL + "(\\w)\\w+" + MMMMM_TRUNCATE_SYMBOL, + "$1" + ); + } + + if (s.indexOf(H_BRACKET_SYMBOL) != -1 || + s.indexOf(HH_BRACKET_SYMBOL) != -1) { + float hours = (float) dateToBeFormatted * 24; + + s = s.replaceAll( + String.valueOf(H_BRACKET_SYMBOL), + format1digit.format(hours) + ); + s = s.replaceAll( + String.valueOf(HH_BRACKET_SYMBOL), + format2digits.format(hours) + ); + } + + if (s.indexOf(M_BRACKET_SYMBOL) != -1 || + s.indexOf(MM_BRACKET_SYMBOL) != -1) { + float minutes = (float) dateToBeFormatted * 24 * 60; + s = s.replaceAll( + String.valueOf(M_BRACKET_SYMBOL), + format1digit.format(minutes) + ); + s = s.replaceAll( + String.valueOf(MM_BRACKET_SYMBOL), + format2digits.format(minutes) + ); + } + if (s.indexOf(S_BRACKET_SYMBOL) != -1 || + s.indexOf(SS_BRACKET_SYMBOL) != -1) { + float seconds = (float) (dateToBeFormatted * 24.0 * 60.0 * 60.0); + s = s.replaceAll( + String.valueOf(S_BRACKET_SYMBOL), + format1digit.format(seconds) + ); + s = s.replaceAll( + String.valueOf(SS_BRACKET_SYMBOL), + format2digits.format(seconds) + ); + } + + if (s.indexOf(L_BRACKET_SYMBOL) != -1 || + s.indexOf(LL_BRACKET_SYMBOL) != -1) { + float millisTemp = (float) ((dateToBeFormatted - Math.floor(dateToBeFormatted)) * 24.0 * 60.0 * 60.0); + float millis = (millisTemp - (int) millisTemp); + s = s.replaceAll( + String.valueOf(L_BRACKET_SYMBOL), + format3digit.format(millis * 10) + ); + s = s.replaceAll( + String.valueOf(LL_BRACKET_SYMBOL), + format4digits.format(millis * 100) + ); + } + + return new StringBuffer(s); + } } diff --git a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java index cab1467d8..926572b1a 100644 --- a/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java +++ b/src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java @@ -271,7 +271,8 @@ public final class TestHSSFDateUtil extends TestCase { "mm/dd HH:MM PM", "mm/dd HH:MM pm", "m/d/yy h:mm AM/PM", "hh:mm:ss", "hh:mm:ss.0", "mm:ss.0", - + //support elapsed time [h],[m],[s] + "[hh]", "[mm]", "[ss]", "[SS]", "[red][hh]" }; for(int i=0; i