]> source.dussan.org Git - poi.git/commitdiff
Format numbers more like Excel does
authorDavid North <dnorth@apache.org>
Tue, 6 Oct 2015 09:56:26 +0000 (09:56 +0000)
committerDavid North <dnorth@apache.org>
Tue, 6 Oct 2015 09:56:26 +0000 (09:56 +0000)
Thanks to Chris Boyle for the patch

https://bz.apache.org/bugzilla/show_bug.cgi?id=58471

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1706971 13f79535-47bb-0310-9956-ffa450edef68

src/java/org/apache/poi/ss/usermodel/DataFormatter.java
src/java/org/apache/poi/ss/usermodel/ExcelGeneralNumberFormat.java [new file with mode: 0644]
src/testcases/org/apache/poi/hssf/usermodel/TestHSSFDataFormatter.java
src/testcases/org/apache/poi/ss/usermodel/TestDataFormatter.java

index b5bf2a8079b83750a18ac724146d99defc7edf7a..2f414a1d9eb0f2d9e10a063e87d5b0a119df813c 100644 (file)
@@ -162,11 +162,8 @@ public class DataFormatter implements Observer {
      */
     private DateFormatSymbols dateSymbols;
 
-    /** <em>General</em> format for whole numbers. */
-    private Format generalWholeNumFormat;
-
-    /** <em>General</em> format for decimal numbers. */
-    private Format generalDecimalNumFormat;
+    /** <em>General</em> format for numbers. */
+    private Format generalNumberFormat;
 
     /** A default format to use when a number pattern cannot be parsed. */
     private Format defaultNumFormat;
@@ -308,10 +305,7 @@ public class DataFormatter implements Observer {
         
         // Is it one of the special built in types, General or @?
         if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) {
-            if (isWholeNumber(cellValue)) {
-                return generalWholeNumFormat;
-            }
-            return generalDecimalNumFormat;
+            return generalNumberFormat;
         }
         
         // Build a formatter, and cache it
@@ -378,10 +372,7 @@ public class DataFormatter implements Observer {
         }
         
         if ("General".equalsIgnoreCase(formatStr) || "@".equals(formatStr)) {
-           if (isWholeNumber(cellValue)) {
-               return generalWholeNumFormat;
-           }
-           return generalDecimalNumFormat;
+           return generalNumberFormat;
         }
 
         if(DateUtil.isADateFormat(formatIndex,formatStr) &&
@@ -656,15 +647,6 @@ public class DataFormatter implements Observer {
         }
     }
 
-    /**
-     * Return true if the double value represents a whole number
-     * @param d the double value to check
-     * @return <code>true</code> 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
@@ -682,10 +664,7 @@ public class DataFormatter implements Observer {
 
           // otherwise use general format
         }
-        if (isWholeNumber(cellValue)){
-            return generalWholeNumFormat;
-        }
-        return generalDecimalNumFormat;
+        return generalNumberFormat;
     }
     
     /**
@@ -735,7 +714,8 @@ public class DataFormatter implements Observer {
         if (numberFormat == null) {
             return String.valueOf(d);
         }
-        return numberFormat.format(new Double(d));
+        String formatted = numberFormat.format(new Double(d));
+        return formatted.replaceFirst("E(\\d)", "E+$1"); // to match Excel's E-notation
     }
 
     /**
@@ -888,8 +868,7 @@ public class DataFormatter implements Observer {
         Iterator<Map.Entry<String,Format>> itr = formats.entrySet().iterator();
         while(itr.hasNext()) {
             Map.Entry<String,Format> entry = itr.next();
-            if (entry.getValue() == generalDecimalNumFormat
-                    || entry.getValue() == generalWholeNumFormat) {
+            if (entry.getValue() == generalNumberFormat) {
                 entry.setValue(format);
             }
         }
@@ -969,8 +948,7 @@ public class DataFormatter implements Observer {
         
         dateSymbols = DateFormatSymbols.getInstance(locale);
         decimalSymbols = DecimalFormatSymbols.getInstance(locale);
-        generalWholeNumFormat = new DecimalFormat("#", decimalSymbols);
-        generalDecimalNumFormat = new DecimalFormat("#.##########", decimalSymbols);
+        generalNumberFormat = new ExcelGeneralNumberFormat(locale);
 
         // init built-in formats
 
diff --git a/src/java/org/apache/poi/ss/usermodel/ExcelGeneralNumberFormat.java b/src/java/org/apache/poi/ss/usermodel/ExcelGeneralNumberFormat.java
new file mode 100644 (file)
index 0000000..2f5ec79
--- /dev/null
@@ -0,0 +1,87 @@
+/* ====================================================================
+   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.
+
+   2012 - Alfresco Software, Ltd.
+   Alfresco Software has modified source of this file
+   The details of changes as svn diff can be found in svn at location root/projects/3rd-party/src 
+==================================================================== */
+package org.apache.poi.ss.usermodel;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+/** A format that formats a double as Excel would, ignoring FieldPosition. All other operations are unsupported. */
+public class ExcelGeneralNumberFormat extends Format {
+
+    private static final long serialVersionUID = 1L;
+
+    private static final DecimalFormat SCIENTIFIC_FORMAT = new DecimalFormat("0.#####E0");
+    static {
+        SCIENTIFIC_FORMAT.setRoundingMode(RoundingMode.HALF_UP);
+    }
+    private static final MathContext TO_10_SF = new MathContext(10, RoundingMode.HALF_UP);
+
+    private final DecimalFormatSymbols decimalSymbols;
+    private final DecimalFormat wholeNumFormat;
+    private final DecimalFormat decimalNumFormat;
+
+    public ExcelGeneralNumberFormat(final Locale locale) {
+        decimalSymbols = new DecimalFormatSymbols(locale);
+        wholeNumFormat = new DecimalFormat("#", decimalSymbols);
+        DataFormatter.setExcelStyleRoundingMode(wholeNumFormat);
+        decimalNumFormat = new DecimalFormat("#.##########", decimalSymbols);
+        DataFormatter.setExcelStyleRoundingMode(decimalNumFormat);
+    }
+
+    public StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) {
+        final double value;
+        if (number instanceof Number) {
+            value = ((Number)number).doubleValue();
+            if (Double.isInfinite(value) || Double.isNaN(value)) {
+                return wholeNumFormat.format(number, toAppendTo, pos);
+            }
+        } else {
+            // testBug54786 gets here with a date, so retain previous behaviour
+            return wholeNumFormat.format(number, toAppendTo, pos);
+        }
+
+        final double abs = Math.abs(value);
+        if (abs >= 1E11 || (abs <= 1E-10 && abs > 0)) {
+            return SCIENTIFIC_FORMAT.format(number, toAppendTo, pos);
+        } else if (Math.floor(value) == value || abs >= 1E10) {
+            // integer, or integer portion uses all 11 allowed digits
+            return wholeNumFormat.format(number, toAppendTo, pos);
+        }
+        // Non-integers of non-scientific magnitude are formatted as "up to 11
+        // numeric characters, with the decimal point counting as a numeric
+        // character". We know there is a decimal point, so limit to 10 digits.
+        // https://support.microsoft.com/en-us/kb/65903
+        final double rounded = new BigDecimal(value).round(TO_10_SF).doubleValue();
+        return decimalNumFormat.format(rounded, toAppendTo, pos);
+    }
+
+    public Object parseObject(String source, ParsePosition pos) {
+        throw new UnsupportedOperationException();
+    }
+
+}
index 3b2588f075d0ea6095a188877950bbd919e3501a..dbb99c0a86f80746d7a8b97289b6906ed573547a 100644 (file)
@@ -171,7 +171,8 @@ public final class TestHSSFDataFormatter {
                // create cells with bad num patterns
                for (int i = 0; i < badNumPatterns.length; i++) {
                        HSSFCell cell = row.createCell(i);
-                       cell.setCellValue(1234567890.12345);
+                       // If the '.' is any later, ExcelGeneralNumberFormat will render an integer, as Excel does.
+                       cell.setCellValue(12345678.9012345);
                        HSSFCellStyle cellStyle = wb.createCellStyle();
                        cellStyle.setDataFormat(format.getFormat(badNumPatterns[i]));
                        cell.setCellStyle(cellStyle);
@@ -276,10 +277,11 @@ public final class TestHSSFDataFormatter {
                log("\n==== VALID NUMBER FORMATS ====");
                while (it.hasNext()) {
                        HSSFCell cell = (HSSFCell) it.next();
-                       log(formatter.formatCellValue(cell));
+                       final String formatted = formatter.formatCellValue(cell);
+                       log(formatted);
 
-                       // should not be equal to "1234567890.12345"
-                       assertTrue( ! "1234567890.12345".equals(formatter.formatCellValue(cell)));
+                       // should not include "12345678" - note that the input value was negative
+                       assertTrue(formatted != null && ! formatted.contains("12345678"));
                }
 
                // test bad number formats
@@ -289,10 +291,9 @@ public final class TestHSSFDataFormatter {
                while (it.hasNext()) {
                        HSSFCell cell = (HSSFCell) it.next();
                        log(formatter.formatCellValue(cell));
-                       // should be equal to "1234567890.12345" 
                        // in some locales the the decimal delimiter is a comma, not a dot
                        char decimalSeparator = new DecimalFormatSymbols(LocaleUtil.getUserLocale()).getDecimalSeparator();
-                       assertEquals("1234567890" + decimalSeparator + "12345", formatter.formatCellValue(cell));
+                       assertEquals("12345678" + decimalSeparator + "9", formatter.formatCellValue(cell));
                }
 
                // test Zip+4 format
index 1251f1e8b3fabf7f4bb7e698417addfbba861e9d..cacb02e9f1efc40f984c0442df38319043b27ae4 100644 (file)
@@ -660,4 +660,59 @@ public class TestDataFormatter {
         assertTrue(DateUtil.isADateFormat(-1, "dd/mm/yy;[red]dd/mm/yy"));
         assertTrue(DateUtil.isADateFormat(-1, "[h]"));
        }
+
+
+    @Test
+    public void testLargeNumbersAndENotation() throws IOException{
+      assertFormatsTo("1E+86", 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999d);
+      assertFormatsTo("1E-84", 0.000000000000000000000000000000000000000000000000000000000000000000000000000000000001d);
+      // Smallest double
+      assertFormatsTo("1E-323", 0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001d);
+
+      // "up to 11 numeric characters, with the decimal point counting as a numeric character"
+      // https://support.microsoft.com/en-us/kb/65903
+      assertFormatsTo( "12345678911",   12345678911d);
+      assertFormatsTo( "1.23457E+11",   123456789112d);  // 12th digit of integer -> scientific
+      assertFormatsTo( "-12345678911", -12345678911d);
+      assertFormatsTo( "-1.23457E+11", -123456789112d);
+      assertFormatsTo( "0.1",           0.1);
+      assertFormatsTo( "0.000000001",   0.000000001);
+      assertFormatsTo( "1E-10",         0.0000000001);  // 12th digit
+      assertFormatsTo( "-0.000000001", -0.000000001);
+      assertFormatsTo( "-1E-10",       -0.0000000001);
+      assertFormatsTo( "123.4567892",   123.45678919);  // excess decimals are simply rounded away
+      assertFormatsTo("-123.4567892",  -123.45678919);
+      assertFormatsTo( "1.234567893",   1.2345678925);  // rounding mode is half-up
+      assertFormatsTo("-1.234567893",  -1.2345678925);
+      assertFormatsTo( "1.23457E+19",   12345650000000000000d);
+      assertFormatsTo("-1.23457E+19",  -12345650000000000000d);
+      assertFormatsTo( "1.23457E-19",   0.0000000000000000001234565d);
+      assertFormatsTo("-1.23457E-19",  -0.0000000000000000001234565d);
+      assertFormatsTo( "1.000000001",   1.000000001);
+      assertFormatsTo( "1",             1.0000000001);
+      assertFormatsTo( "1234.567891",   1234.567891123456789d);
+      assertFormatsTo( "1234567.891",   1234567.891123456789d);
+      assertFormatsTo( "12345678912",   12345678911.63456789d);  // integer portion uses all 11 digits
+      assertFormatsTo( "12345678913",   12345678912.5d);  // half-up here too
+      assertFormatsTo("-12345678913",  -12345678912.5d);
+      assertFormatsTo( "1.23457E+11",   123456789112.3456789d);
+    }
+
+   private static void assertFormatsTo(String expected, double input) throws IOException {
+       Workbook wb = new HSSFWorkbook();
+       try {
+            Sheet s1 = wb.createSheet();
+            Row row = s1.createRow(0);
+            Cell rawValue = row.createCell(0);
+            rawValue.setCellValue(input);
+            CellStyle newStyle = wb.createCellStyle();
+            DataFormat dataFormat = wb.createDataFormat();
+            newStyle.setDataFormat(dataFormat.getFormat("General"));
+            String actual = new DataFormatter().formatCellValue(rawValue);
+            assertEquals(expected, actual);
+        }
+        finally {
+            wb.close();
+        }
+    }
 }