]> source.dussan.org Git - poi.git/commitdiff
improved DataFormatter, see Bugzilla 50841
authorYegor Kozlov <yegor@apache.org>
Sat, 19 Mar 2011 12:42:01 +0000 (12:42 +0000)
committerYegor Kozlov <yegor@apache.org>
Sat, 19 Mar 2011 12:42:01 +0000 (12:42 +0000)
git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1083173 13f79535-47bb-0310-9956-ffa450edef68

src/documentation/content/xdocs/status.xml
src/examples/src/org/apache/poi/ss/examples/ToCSV.java
src/java/org/apache/poi/ss/usermodel/DataFormatter.java
src/java/org/apache/poi/ss/usermodel/DateUtil.java
src/java/org/apache/poi/ss/usermodel/ExcelStyleDateFormatter.java
src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDateUtil.java
src/testcases/org/apache/poi/ss/usermodel/TestDataFormatter.java

index 30170329c364374d8fc430b165df71c99a37e841..9cb0bccc2368511a6d681784d3f9f19f06febcfd 100644 (file)
@@ -34,6 +34,7 @@
 
     <changes>
         <release version="3.8-beta2" date="2011-??-??">
+           <action dev="poi-developers" type="fix">50841 - Improved SpreadSheet DataFormatter to handle scientific notation, invalid dates and format spacers</action>
            <action dev="poi-developers" type="fix">49381 - Correct createFreezePane in XSSF, so that the left row/column matches the documentation + HSSF</action>
            <action dev="poi-developers" type="fix">49253 - When setting repeating rows and columns for XSSF, don't break the print settings if they were already there</action>
            <action dev="poi-developers" type="fix">49219 - ExternalNameRecord support for DDE Link entries without an operation</action>
index eefe3b550cf87e936931063c13407f218e6ee58b..0343ab6d737ace85864fc90644cbb3490040828e 100644 (file)
@@ -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) {
index 38afc61d345611077f48a6b183924efe1f42b61f..6c9412383d4c64f932354fd319b115cfef5ad1e1 100644 (file)
@@ -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.
  *
+ * <p>
+ * 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.
+ * </p>
+ * <p>Example:</p>
+ * <p>
+ * Consider a numeric cell with a value <code>12.343</code> and format <code>"##.##_ "</code>.
+ *  The trailing underscore and space ("_ ") in the format adds a space to the end and Excel formats this cell as <code>"12.34 "</code>,
+ *  but <code>DataFormatter</code> trims the formatted value and returns <code>"12.34"</code>.
+ * </p>
+ * You can enable spaces by passing the <code>emulateCsv=true</code> flag in the <code>DateFormatter</code> 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:
+ * <ul>
+ *  <li>returned values are not trimmed</li>
+ *  <li>Invalid dates are formatted as  255 pound signs ("#")</li>
+ *  <li>simulate Excel's handling of a format string of all # when the value is 0.
+ *   Excel will output "", <code>DataFormatter</code> will output "0".
+ * </ul>
  * @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<String,Format> 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<Integer> ms = new ArrayList<Integer>();
+        boolean isElapsed = false;
         for(int j=0; j<chars.length; j++) {
             char c = chars[j];
+            if (c == '[' && !isElapsed) {
+                isElapsed = true;
+                mIsMonth = false;
+                sb.append(c);
+            }
+            else if (c == ']' && isElapsed) {
+                isElapsed = false;
+                sb.append(c);
+            }
+            else if (isElapsed) {
             if (c == 'h' || c == 'H') {
+                    sb.append('H');
+                }
+                else if (c == 'm' || c == 'M') {
+                    sb.append('m');
+                }
+                else if (c == 's' || c == 'S') {
+                    sb.append('s');
+                }
+                else {
+                    sb.append(c);
+                }
+            }
+            else if (c == 'h' || c == 'H') {
                 mIsMonth = false;
                 if (hasAmPm) {
                     sb.append('h');
@@ -373,32 +467,61 @@ public class DataFormatter {
 
     }
 
-    private Format createNumberFormat(String formatStr, double cellValue) {
+    private String cleanFormatForNumber(String formatStr) {
         StringBuffer sb = new StringBuffer(formatStr);
-        
-        // 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;
-               } 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);
+        }
+    }
 }
index 787d8d0ec3c09c49c254325135938ba3394a1b68..cf445f9fae2c091836f813662b61109247e1fc0a 100644 (file)
@@ -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("");
index 91ea083b560ebeb2e912c55c28f9745a64ff4e33..a1307ee061557fed1fe683002e025448d1c9a6db 100644 (file)
 ==================================================================== */
 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);
-   }
-
-   /**
-    * 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;
-   }
-
-   /**
-    * 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)
-         );
-      }
-
-      return new StringBuffer(s);
-   }
+    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';
+
+    private DecimalFormat format1digit = new DecimalFormat("0");
+    private DecimalFormat format2digits = new DecimalFormat("00");
+
+    private DecimalFormat format3digit = new DecimalFormat("0");
+    private DecimalFormat format4digits = new DecimalFormat("00");
+
+    {
+        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);
+    }
 }
index cab1467d8f259ed669de433e75138dace60e83bf..926572b1a8efef19b174819fd4859127a971fa0e 100644 (file)
@@ -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<formats.length; i++) {
             assertTrue(
@@ -286,6 +287,7 @@ public final class TestHSSFDateUtil extends TestCase {
                 "0.0", "0.000",
                 "0%", "0.0%",
                 "[]Foo", "[BLACK]0.00%",
+                "[ms]", "[Mh]",
                 "", null
         };
         for(int i=0; i<formats.length; i++) {
index 06abd6c5027e38cf75e8ab38fc13d433358abc86..41c220c5d32aef1bc4fd3708a49248dc9c12ff53 100644 (file)
@@ -27,7 +27,7 @@ import org.apache.poi.hssf.usermodel.TestHSSFDataFormatter;
 /**
  * Tests of {@link DataFormatter}
  *
- * See {@link TestHSSFDataFormatter} too for 
+ * See {@link TestHSSFDataFormatter} too for
  *  more tests.
  */
 public class TestDataFormatter extends TestCase {
@@ -179,6 +179,38 @@ public class TestDataFormatter extends TestCase {
        assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "*-##.##"));
     }
     
+    /**
+     * DataFormatter is the CSV mode preserves spaces
+     */
+    public void testPaddingSpacesCSV() {
+       DataFormatter dfUS = new DataFormatter(Locale.US, true);
+       assertEquals("12.34 ", dfUS.formatRawCellContents(12.343, -1, "##.##_ "));
+       assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.343, -1, "##.##_ "));
+       assertEquals(". ", dfUS.formatRawCellContents(0.0, -1, "##.##_ "));
+       assertEquals("12.34 ", dfUS.formatRawCellContents(12.343, -1, "##.##_1"));
+       assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.343, -1, "##.##_1"));
+       assertEquals(". ", dfUS.formatRawCellContents(0.0, -1, "##.##_1"));
+       assertEquals("12.34 ", dfUS.formatRawCellContents(12.343, -1, "##.##_)"));
+       assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.343, -1, "##.##_)"));
+       assertEquals(". ", dfUS.formatRawCellContents(0.0, -1, "##.##_)"));
+       assertEquals(" 12.34", dfUS.formatRawCellContents(12.343, -1, "_-##.##"));
+       assertEquals("- 12.34", dfUS.formatRawCellContents(-12.343, -1, "_-##.##"));
+       assertEquals(" .", dfUS.formatRawCellContents(0.0, -1, "_-##.##"));
+
+       assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "##.##* "));
+       assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "##.##* "));
+       assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "##.##* "));
+       assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "##.##*1"));
+       assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "##.##*1"));
+       assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "##.##*1"));
+       assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "##.##*)"));
+       assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "##.##*)"));
+       assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "##.##*)"));
+       assertEquals("12.34", dfUS.formatRawCellContents(12.343, -1, "*-##.##"));
+       assertEquals("-12.34", dfUS.formatRawCellContents(-12.343, -1, "*-##.##"));
+       assertEquals(".", dfUS.formatRawCellContents(0.0, -1, "*-##.##"));
+    }
+
     /**
      * Test that the special Excel month format MMMMM
      *  gets turned into the first letter of the month
@@ -196,14 +228,14 @@ public class TestDataFormatter extends TestCase {
     }
     
     /**
-     * Test that we can handle elapsed time, 
+     * Test that we can handle elapsed time,
      *  eg formatting 1 day 4 hours as 28 hours
      */
     public void testElapsedTime() {
        DataFormatter dfUS = new DataFormatter(Locale.US);
-       
+
        double hour = 1.0/24.0;
-       
+
        assertEquals("01:00", dfUS.formatRawCellContents(1*hour, -1, "hh:mm"));
        assertEquals("05:00", dfUS.formatRawCellContents(5*hour, -1, "hh:mm"));
        assertEquals("20:00", dfUS.formatRawCellContents(20*hour, -1, "hh:mm"));
@@ -212,7 +244,7 @@ public class TestDataFormatter extends TestCase {
        assertEquals("02:00", dfUS.formatRawCellContents(26*hour, -1, "hh:mm"));
        assertEquals("20:00", dfUS.formatRawCellContents(44*hour, -1, "hh:mm"));
        assertEquals("02:00", dfUS.formatRawCellContents(50*hour, -1, "hh:mm"));
-       
+
        assertEquals("01:00", dfUS.formatRawCellContents(1*hour, -1, "[hh]:mm"));
        assertEquals("05:00", dfUS.formatRawCellContents(5*hour, -1, "[hh]:mm"));
        assertEquals("20:00", dfUS.formatRawCellContents(20*hour, -1, "[hh]:mm"));
@@ -221,12 +253,73 @@ public class TestDataFormatter extends TestCase {
        assertEquals("26:00", dfUS.formatRawCellContents(26*hour, -1, "[hh]:mm"));
        assertEquals("44:00", dfUS.formatRawCellContents(44*hour, -1, "[hh]:mm"));
        assertEquals("50:00", dfUS.formatRawCellContents(50*hour, -1, "[hh]:mm"));
-       
-       assertEquals("30:00", dfUS.formatRawCellContents(0.5*hour, -1, "[mm]:ss"));
-       assertEquals("60:00", dfUS.formatRawCellContents(1*hour, -1, "[mm]:ss"));
-       assertEquals("120:00", dfUS.formatRawCellContents(2*hour, -1, "[mm]:ss"));
+
+       assertEquals("01", dfUS.formatRawCellContents(1*hour, -1, "[hh]"));
+       assertEquals("05", dfUS.formatRawCellContents(5*hour, -1, "[hh]"));
+       assertEquals("20", dfUS.formatRawCellContents(20*hour, -1, "[hh]"));
+       assertEquals("23", dfUS.formatRawCellContents(23*hour, -1, "[hh]"));
+       assertEquals("24", dfUS.formatRawCellContents(24*hour, -1, "[hh]"));
+       assertEquals("26", dfUS.formatRawCellContents(26*hour, -1, "[hh]"));
+       assertEquals("44", dfUS.formatRawCellContents(44*hour, -1, "[hh]"));
+       assertEquals("50", dfUS.formatRawCellContents(50*hour, -1, "[hh]"));
+
+       double minute = 1.0/24.0/60.0;
+       assertEquals("01:00", dfUS.formatRawCellContents(1*minute, -1, "[mm]:ss"));
+       assertEquals("05:00", dfUS.formatRawCellContents(5*minute, -1, "[mm]:ss"));
+       assertEquals("20:00", dfUS.formatRawCellContents(20*minute, -1, "[mm]:ss"));
+       assertEquals("23:00", dfUS.formatRawCellContents(23*minute, -1, "[mm]:ss"));
+       assertEquals("24:00", dfUS.formatRawCellContents(24*minute, -1, "[mm]:ss"));
+       assertEquals("26:00", dfUS.formatRawCellContents(26*minute, -1, "[mm]:ss"));
+       assertEquals("44:00", dfUS.formatRawCellContents(44*minute, -1, "[mm]:ss"));
+       assertEquals("50:00", dfUS.formatRawCellContents(50*minute, -1, "[mm]:ss"));
+       assertEquals("59:00", dfUS.formatRawCellContents(59*minute, -1, "[mm]:ss"));
+       assertEquals("60:00", dfUS.formatRawCellContents(60*minute, -1, "[mm]:ss"));
+       assertEquals("61:00", dfUS.formatRawCellContents(61*minute, -1, "[mm]:ss"));
+       assertEquals("119:00", dfUS.formatRawCellContents(119*minute, -1, "[mm]:ss"));
+       assertEquals("120:00", dfUS.formatRawCellContents(120*minute, -1, "[mm]:ss"));
+       assertEquals("121:00", dfUS.formatRawCellContents(121*minute, -1, "[mm]:ss"));
+
+       assertEquals("01", dfUS.formatRawCellContents(1*minute, -1, "[mm]"));
+       assertEquals("05", dfUS.formatRawCellContents(5*minute, -1, "[mm]"));
+       assertEquals("20", dfUS.formatRawCellContents(20*minute, -1, "[mm]"));
+       assertEquals("23", dfUS.formatRawCellContents(23*minute, -1, "[mm]"));
+       assertEquals("24", dfUS.formatRawCellContents(24*minute, -1, "[mm]"));
+       assertEquals("26", dfUS.formatRawCellContents(26*minute, -1, "[mm]"));
+       assertEquals("44", dfUS.formatRawCellContents(44*minute, -1, "[mm]"));
+       assertEquals("50", dfUS.formatRawCellContents(50*minute, -1, "[mm]"));
+       assertEquals("59", dfUS.formatRawCellContents(59*minute, -1, "[mm]"));
+       assertEquals("60", dfUS.formatRawCellContents(60*minute, -1, "[mm]"));
+       assertEquals("61", dfUS.formatRawCellContents(61*minute, -1, "[mm]"));
+       assertEquals("119", dfUS.formatRawCellContents(119*minute, -1, "[mm]"));
+       assertEquals("120", dfUS.formatRawCellContents(120*minute, -1, "[mm]"));
+       assertEquals("121", dfUS.formatRawCellContents(121*minute, -1, "[mm]"));
+
+       double second = 1.0/24.0/60.0/60.0;
+       assertEquals("86400", dfUS.formatRawCellContents(86400*second, -1, "[ss]"));
+       assertEquals("01", dfUS.formatRawCellContents(1*second, -1, "[ss]"));
+       assertEquals("05", dfUS.formatRawCellContents(5*second, -1, "[ss]"));
+       assertEquals("20", dfUS.formatRawCellContents(20*second, -1, "[ss]"));
+       assertEquals("23", dfUS.formatRawCellContents(23*second, -1, "[ss]"));
+       assertEquals("24", dfUS.formatRawCellContents(24*second, -1, "[ss]"));
+       assertEquals("26", dfUS.formatRawCellContents(26*second, -1, "[ss]"));
+       assertEquals("44", dfUS.formatRawCellContents(44*second, -1, "[ss]"));
+       assertEquals("50", dfUS.formatRawCellContents(50*second, -1, "[ss]"));
+       assertEquals("59", dfUS.formatRawCellContents(59*second, -1, "[ss]"));
+       assertEquals("60", dfUS.formatRawCellContents(60*second, -1, "[ss]"));
+       assertEquals("61", dfUS.formatRawCellContents(61*second, -1, "[ss]"));
+       assertEquals("119", dfUS.formatRawCellContents(119*second, -1, "[ss]"));
+       assertEquals("120", dfUS.formatRawCellContents(120*second, -1, "[ss]"));
+       assertEquals("121", dfUS.formatRawCellContents(121*second, -1, "[ss]"));
+
+       assertEquals("28:48:00",dfUS.formatRawCellContents(1.2, -1, "[h]:mm:ss"));
+
+       assertEquals("57:07.2", dfUS.formatRawCellContents(.123, -1, "mm:ss.0;@"));
+       assertEquals("57:41.8", dfUS.formatRawCellContents(.1234, -1, "mm:ss.0;@"));
+       assertEquals("57:41.76", dfUS.formatRawCellContents(.1234, -1, "mm:ss.00;@"));
+       assertEquals("57:41.760", dfUS.formatRawCellContents(.1234, -1, "mm:ss.000;@"));
+       assertEquals("24:00.0", dfUS.formatRawCellContents(123456.6, -1, "mm:ss.0"));
     }
-    
+
     public void testDateWindowing() {
        DataFormatter dfUS = new DataFormatter(Locale.US);
        
@@ -234,4 +327,31 @@ public class TestDataFormatter extends TestCase {
        assertEquals("1899-12-31 00:00:00", dfUS.formatRawCellContents(0.0, -1, "yyyy-mm-dd hh:mm:ss", false));
        assertEquals("1904-01-01 00:00:00", dfUS.formatRawCellContents(0.0, -1, "yyyy-mm-dd hh:mm:ss", true));
     }
+
+    public void testScientificNotation() {
+        DataFormatter dfUS = new DataFormatter(Locale.US);
+
+        assertEquals("1.23E+01", dfUS.formatRawCellContents(12.343, -1, "0.00E+00"));
+        assertEquals("-1.23E+01", dfUS.formatRawCellContents(-12.343, -1, "0.00E+00"));
+        assertEquals("0.00E+00", dfUS.formatRawCellContents(0.0, -1, "0.00E+00"));
+     }
+
+    public void testInvalidDate() {
+        DataFormatter df1 = new DataFormatter(Locale.US);
+        assertEquals("-1.0", df1.formatRawCellContents(-1, -1, "mm/dd/yyyy"));
+
+        DataFormatter df2 = new DataFormatter(Locale.US, true);
+        assertEquals("###############################################################################################################################################################################################################################################################",
+                df2.formatRawCellContents(-1, -1, "mm/dd/yyyy"));
+    }
+
+
+    public void testOther() {
+        DataFormatter dfUS = new DataFormatter(Locale.US, true);
+
+        assertEquals(" 12.34 ", dfUS.formatRawCellContents(12.34, -1, "_-* #,##0.00_-;-* #,##0.00_-;_-* \"-\"??_-;_-@_-"));
+        assertEquals("-12.34 ", dfUS.formatRawCellContents(-12.34, -1, "_-* #,##0.00_-;-* #,##0.00_-;_-* \"-\"??_-;_-@_-"));
+        assertEquals(" -   ", dfUS.formatRawCellContents(0.0, -1, "_-* #,##0.00_-;-* #,##0.00_-;_-* \"-\"??_-;_-@_-"));
+        assertEquals(" $-   ", dfUS.formatRawCellContents(0.0, -1, "_-$* #,##0.00_-;-$* #,##0.00_-;_-$* \"-\"??_-;_-@_-"));
+    }
 }