[Jython-checkins] jython: Fixes for %-formatting floats.

jeff.allen jython-checkins at python.org
Mon May 19 00:49:36 CEST 2014


http://hg.python.org/jython/rev/60838278e668
changeset:   7259:60838278e668
user:        Jeff Allen <ja.py at farowl.co.uk>
date:        Thu May 08 21:23:39 2014 +0100
summary:
  Fixes for %-formatting floats.
Replaces the float rendering in PyString StringFormatter with that used for float.__format__(). Skips removed from test_float. Some change to FloatFormatter to support alternative mode in %#e and %#f.

files:
  Lib/test/test_float.py                            |   40 --
  Lib/test/test_float_jy.py                         |    5 +-
  src/org/python/core/PyString.java                 |  149 +--------
  src/org/python/core/stringlib/FloatFormatter.java |   91 ++++-
  4 files changed, 83 insertions(+), 202 deletions(-)


diff --git a/Lib/test/test_float.py b/Lib/test/test_float.py
--- a/Lib/test/test_float.py
+++ b/Lib/test/test_float.py
@@ -617,8 +617,6 @@
         self.assertEqual('{0:f}'.format(NAN), 'nan')
         self.assertEqual('{0:F}'.format(NAN), 'NAN')
 
-    @unittest.skipIf(test_support.is_jython,
-                     "FIXME: not working on Jython")
     @requires_IEEE_754
     def test_format_testfile(self):
         with open(format_testfile) as testfile:
@@ -842,8 +840,6 @@
         self.assertAlmostEqual(round(1.5e22, -22), 2e22)
 
 
-    @unittest.skipIf(test_support.is_jython,
-                     "FIXME: %-formatting specials imperfect in Jython")
     @requires_IEEE_754
     def test_format_specials(self):
         # Test formatting of nans and infs.
@@ -878,42 +874,6 @@
             test(sfmt, NAN, ' nan')
             test(sfmt, -NAN, ' nan')
 
-    @requires_IEEE_754
-    def test_format_specials_jy(self):
-        # Test formatting of nans and infs (suppressing %-formatting).
-        # This is just a crudely restricted copy of test_format_specials.
-        # Delete this test when we no longer have to skip test_format_specials.
-
-        def test(fmt, value, expected):
-            # Test with only format().
-            #self.assertEqual(fmt % value, expected, fmt)
-            if not '#' in fmt:
-                # Until issue 7094 is implemented, format() for floats doesn't
-                #  support '#' formatting
-                fmt = fmt[1:] # strip off the %
-                self.assertEqual(format(value, fmt), expected, fmt)
-
-        for fmt in ['%e', '%f', '%g', '%.0e', '%.6f', '%.20g',
-                    '%#e', '%#f', '%#g', '%#.20e', '%#.15f', '%#.3g']:
-            pfmt = '%+' + fmt[1:]
-            sfmt = '% ' + fmt[1:]
-            test(fmt, INF, 'inf')
-            test(fmt, -INF, '-inf')
-            test(fmt, NAN, 'nan')
-            test(fmt, -NAN, 'nan')
-            # When asking for a sign, it's always provided. nans are
-            #  always positive.
-            test(pfmt, INF, '+inf')
-            test(pfmt, -INF, '-inf')
-            test(pfmt, NAN, '+nan')
-            test(pfmt, -NAN, '+nan')
-            # When using ' ' for a sign code, only infs can be negative.
-            #  Others have a space.
-            test(sfmt, INF, ' inf')
-            test(sfmt, -INF, '-inf')
-            test(sfmt, NAN, ' nan')
-            test(sfmt, -NAN, ' nan')
-
 
 # Beginning with Python 2.6 float has cross platform compatible
 # ways to create and represent inf and nan
diff --git a/Lib/test/test_float_jy.py b/Lib/test/test_float_jy.py
--- a/Lib/test/test_float_jy.py
+++ b/Lib/test/test_float_jy.py
@@ -40,11 +40,8 @@
 
     def test_float_str_formatting(self):
         self.assertEqual('%.13g' % 12345678.00005, '12345678.00005')
-        self.assertEqual('%.12g' % 12345678.00005,
-                         jython and '12345678' or '12345678.0001')
+        self.assertEqual('%.12g' % 12345678.00005, '12345678.0001')
         self.assertEqual('%.11g' % 12345678.00005, '12345678')
-        # XXX: The exponential formatter isn't totally correct, e.g. our
-        # output here is really .13g
         self.assertEqual('%.12g' % math.pi**-100, '1.92758141606e-50')
         self.assertEqual('%.5g' % 123.005, '123')
         self.assertEqual('%#.5g' % 123.005, '123.00')
diff --git a/src/org/python/core/PyString.java b/src/org/python/core/PyString.java
--- a/src/org/python/core/PyString.java
+++ b/src/org/python/core/PyString.java
@@ -4,17 +4,15 @@
 import java.lang.ref.Reference;
 import java.lang.ref.SoftReference;
 import java.math.BigInteger;
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-
-import org.python.core.StringFormatter.DecimalFormatTemplate;
+
 import org.python.core.buffer.BaseBuffer;
 import org.python.core.buffer.SimpleStringBuffer;
 import org.python.core.stringlib.FieldNameIterator;
+import org.python.core.stringlib.FloatFormatter;
+import org.python.core.stringlib.InternalFormat.Spec;
 import org.python.core.stringlib.InternalFormatSpec;
 import org.python.core.stringlib.InternalFormatSpecParser;
 import org.python.core.stringlib.MarkupIterator;
-import org.python.core.util.ExtraMath;
 import org.python.core.util.StringUtil;
 import org.python.expose.ExposedMethod;
 import org.python.expose.ExposedNew;
@@ -4246,7 +4244,6 @@
                 // safe to call __int__:
                 argAsInt = arg.__int__();
             } else {
-                // Same case noted on formatFloatDecimal:
                 // We can't simply call arg.__int__() because PyString implements
                 // it without exposing it to python (i.e, str instances has no
                 // __int__ attribute). So, we would support strings as arguments
@@ -4255,7 +4252,7 @@
                 try {
                     argAsInt = arg.__getattr__("__int__").__call__();
                 } catch (PyException e) {
-                    // XXX: Swallow customs AttributeError throws from __float__ methods
+                    // XXX: Swallow custom AttributeError throws from __int__ methods
                     // No better alternative for the moment
                     if (e.match(Py.AttributeError)) {
                         throw Py.TypeError("int argument required");
@@ -4317,79 +4314,6 @@
         }
     }
 
-    static class DecimalFormatTemplate {
-
-        static DecimalFormat template;
-        static {
-            template =
-                    new DecimalFormat("#,##0.#####", new DecimalFormatSymbols(java.util.Locale.US));
-            DecimalFormatSymbols symbols = template.getDecimalFormatSymbols();
-            symbols.setNaN("nan");
-            symbols.setInfinity("inf");
-            template.setDecimalFormatSymbols(symbols);
-            template.setGroupingUsed(false);
-        }
-    }
-
-    private static final DecimalFormat getDecimalFormat() {
-        return (DecimalFormat)DecimalFormatTemplate.template.clone();
-    }
-
-    private String formatFloatDecimal(double v, boolean truncate) {
-        checkPrecision("decimal");
-        int prec = precision;
-        if (prec == -1) {
-            prec = 6;
-        }
-        if (v < 0) {
-            v = -v;
-            negative = true;
-        }
-
-        DecimalFormat decimalFormat = getDecimalFormat();
-        decimalFormat.setMaximumFractionDigits(prec);
-        decimalFormat.setMinimumFractionDigits(truncate ? 0 : prec);
-
-        String ret = decimalFormat.format(v);
-        return ret;
-    }
-
-    private String formatFloatExponential(PyObject arg, char e, boolean truncate) {
-        StringBuilder buf = new StringBuilder();
-        double v = asDouble(arg);
-        boolean isNegative = false;
-        if (v < 0) {
-            v = -v;
-            isNegative = true;
-        }
-        double power = 0.0;
-        if (v > 0) {
-            power = ExtraMath.closeFloor(Math.log10(v));
-        }
-        // System.err.println("formatExp: "+v+", "+power);
-        int savePrecision = precision;
-        precision = 2;
-
-        String exp = formatInteger((long)power, 10, false);
-        if (negative) {
-            negative = false;
-            exp = '-' + exp;
-        } else {
-            exp = '+' + exp;
-        }
-
-        precision = savePrecision;
-
-        double base = v / Math.pow(10, power);
-        buf.append(formatFloatDecimal(base, truncate));
-        buf.append(e);
-
-        buf.append(exp);
-        negative = isNegative;
-
-        return buf.toString();
-    }
-
     /**
      * Main service of this class: format one or more arguments with the format string supplied at
      * construction.
@@ -4655,61 +4579,26 @@
 
                 case 'e':
                 case 'E':
-                    // Floating point exponential format (+case).
-                    string = formatFloatExponential(arg, c, false);
-                    if (c == 'E') {
-                        string = string.toUpperCase();
-                    }
-                    break;
-
                 case 'f':
                 case 'F':
-                    // Floating point decimal format (+case). Note ints accepted.
-                    string = formatFloatDecimal(asDouble(arg), false);
-                    if (c == 'F') {
-                        string = string.toUpperCase();
-                    }
-                    break;
-
                 case 'g':
                 case 'G':
-                    // Value-adaptive floating point format (+case). Note ints accepted.
-                    int origPrecision = precision;
-                    if (precision == -1) {
-                        precision = 6;
-                    }
-
+                    // All floating point formats (+case).
+
+                    // Convert the flags (local variables) to the form needed in the Spec object.
+                    char align = ljustFlag ? '<' : '>';
+                    char sign = signFlag ? '+' : (blankFlag ? ' ' : Spec.NONE);
+                    int w = Spec.UNSPECIFIED;
+                    Spec spec = new Spec(fill, align, sign, altFlag, w, false, precision, c);
+
+                    // Format using this Spec the double form of the argument.
+                    FloatFormatter f = new FloatFormatter(spec);
                     double v = asDouble(arg);
-                    int exponent = (int)ExtraMath.closeFloor(Math.log10(Math.abs(v == 0 ? 1 : v)));
-                    if (v == Double.POSITIVE_INFINITY) {
-                        string = "inf";
-                    } else if (v == Double.NEGATIVE_INFINITY) {
-                        string = "-inf";
-                    } else if (exponent >= -4 && exponent < precision) {
-                        precision -= exponent + 1;
-                        string = formatFloatDecimal(v, !altFlag);
-
-                        // XXX: this block may be unnecessary now
-                        if (altFlag && string.indexOf('.') == -1) {
-                            int zpad = origPrecision - string.length();
-                            string += '.';
-                            if (zpad > 0) {
-                                char zeros[] = new char[zpad];
-                                for (int ci = 0; ci < zpad; zeros[ci++] = '0') {}
-                                string += new String(zeros);
-                            }
-                        }
-                    } else {
-                        // Exponential precision is the number of digits after the decimal
-                        // point, whereas 'g' precision is the number of significant digits --
-                        // and exponential always provides one significant digit before the
-                        // decimal point
-                        precision--;
-                        string = formatFloatExponential(arg, (char)(c - 2), !altFlag);
-                    }
-                    if (c == 'G') {
-                        string = string.toUpperCase();
-                    }
+                    f.format(v);
+                    string = f.getResult();
+
+                    // Suppress subsequent attempts to insert a correct sign, done already.
+                    signFlag = blankFlag = negative = false;
                     break;
 
                 case 'c':
diff --git a/src/org/python/core/stringlib/FloatFormatter.java b/src/org/python/core/stringlib/FloatFormatter.java
--- a/src/org/python/core/stringlib/FloatFormatter.java
+++ b/src/org/python/core/stringlib/FloatFormatter.java
@@ -16,7 +16,7 @@
 public class FloatFormatter extends InternalFormat.Formatter {
 
     /** The rounding mode dominant in the formatter. */
-    static final RoundingMode ROUND_PY = RoundingMode.HALF_UP; // Believed to be HALF_EVEN in Py3k
+    static final RoundingMode ROUND_PY = RoundingMode.HALF_EVEN;
 
     /** If it contains no decimal point, this length is zero, and 1 otherwise. */
     private int lenPoint;
@@ -325,10 +325,9 @@
             exp = lenFraction - vv.scale();
         }
 
-        // Finally add zeros, as necessary, and stick on the exponent.
-
+        // If the result is not already complete, add point and zeros as necessary, and exponent.
         if (lenMarker == 0) {
-            appendTrailingZeros(precision);
+            ensurePointAndTrailingZeros(precision);
             appendExponent(exp);
         }
     }
@@ -345,14 +344,7 @@
      */
     private void format_f(double value, String positivePrefix, int precision) {
 
-        if (signAndSpecialNumber(value, positivePrefix)) {
-
-            if (lenMarker == 0) {
-                // May be 0 or -0 so we still need to ...
-                appendTrailingZeros(precision);
-            }
-
-        } else {
+        if (!signAndSpecialNumber(value, positivePrefix)) {
             // Convert value to decimal exactly. (This can be very long.)
             BigDecimal vLong = new BigDecimal(Math.abs(value));
 
@@ -366,9 +358,53 @@
                 // There is a decimal point and some digits following
                 lenWhole = result.length() - (start + lenSign + (lenPoint = 1) + lenFraction);
             } else {
+                // There are no fractional digits and so no decimal point
                 lenWhole = result.length() - (start + lenSign);
             }
+        }
 
+        // Finally, ensure we have all the fractional digits we should.
+        if (lenMarker == 0) {
+            ensurePointAndTrailingZeros(precision);
+        }
+    }
+
+    /**
+     * Append a decimal point and trailing fractional zeros if necessary for 'e' and 'f' format.
+     * This should not be called if the result is not numeric ("inf" for example). This method deals
+     * with the following complexities: on return there will be at least the number of fractional
+     * digits specified in the argument <code>n</code>, and at least {@link #minFracDigits};
+     * further, if <code>minFracDigits<0</code>, signifying the "alternate mode" of certain
+     * formats, the method will ensure there is a decimal point, even if there are no fractional
+     * digits to follow.
+     *
+     * @param n smallest number of fractional digits on return
+     */
+    private void ensurePointAndTrailingZeros(int n) {
+
+        // Set n to the number of fractional digits we should have.
+        if (n < minFracDigits) {
+            n = minFracDigits;
+        }
+
+        // Do we have a decimal point already?
+        if (lenPoint == 0) {
+            // No decimal point: add one if there will be any fractional digits or
+            if (n > 0 || minFracDigits < 0) {
+                // First need to add a decimal point.
+                result.append('.');
+                lenPoint = 1;
+            }
+        }
+
+        // Do we have enough fractional digits already?
+        int f = lenFraction;
+        if (n > f) {
+            // Make up the required number of zeros.
+            for (; f < n; f++) {
+                result.append('0');
+            }
+            lenFraction = f;
         }
     }
 
@@ -635,7 +671,7 @@
 
         // In no-truncate mode, the fraction is full precision. Otherwise trim it.
         if (minFracDigits >= 0) {
-            // Note minFracDigits only applies to fixed formats.
+            // Note positive minFracDigits only applies to fixed formats.
             removeTrailingZeros(0);
         }
 
@@ -757,16 +793,16 @@
 
     /**
      * Append the trailing fractional zeros, as required by certain formats, so that the total
-     * number of fractional digits is no less than specified. If <code>minFracDigits<=0</code>,
-     * the method leaves the {@link #result} buffer unchanged.
+     * number of fractional digits is no less than specified. If <code>n<=0</code>, the method
+     * leaves the {@link #result} buffer unchanged.
      *
-     * @param minFracDigits smallest number of fractional digits on return
+     * @param n smallest number of fractional digits on return
      */
-    private void appendTrailingZeros(int minFracDigits) {
+    private void appendTrailingZeros(int n) {
 
         int f = lenFraction;
 
-        if (minFracDigits > f) {
+        if (n > f) {
             if (lenPoint == 0) {
                 // First need to add a decimal point. (Implies lenFraction=0.)
                 result.append('.');
@@ -774,7 +810,7 @@
             }
 
             // Now make up the required number of zeros.
-            for (; f < minFracDigits; f++) {
+            for (; f < n; f++) {
                 result.append('0');
             }
             lenFraction = f;
@@ -787,9 +823,9 @@
      * originally (and therefore no fractional part), the method will add a decimal point, even if
      * it adds no zeros.
      *
-     * @param minFracDigits smallest number of fractional digits on return
+     * @param n smallest number of fractional digits on return
      */
-    private void appendPointAndTrailingZeros(int minFracDigits) {
+    private void appendPointAndTrailingZeros(int n) {
 
         if (lenPoint == 0) {
             // First need to add a decimal point. (Implies lenFraction=0.)
@@ -799,7 +835,7 @@
 
         // Now make up the required number of zeros.
         int f;
-        for (f = lenFraction; f < minFracDigits; f++) {
+        for (f = lenFraction; f < n; f++) {
             result.append('0');
         }
         lenFraction = f;
@@ -810,18 +846,17 @@
      * least the number of fractional digits specified. If the resultant number of fractional digits
      * is zero, this method will also remove the trailing decimal point (if there is one).
      *
-     * @param minFracDigits smallest number of fractional digits on return
+     * @param n smallest number of fractional digits on return
      */
-    private void removeTrailingZeros(int minFracDigits) {
-
-        int f = lenFraction;
+    private void removeTrailingZeros(int n) {
 
         if (lenPoint > 0) {
             // There's a decimal point at least, and there may be some fractional digits.
-            if (minFracDigits == 0 || f > minFracDigits) {
+            int f = lenFraction;
+            if (n == 0 || f > n) {
 
                 int fracStart = result.length() - f;
-                for (; f > minFracDigits; --f) {
+                for (; f > n; --f) {
                     if (result.charAt(fracStart - 1 + f) != '0') {
                         // Keeping this one as it isn't a zero
                         break;

-- 
Repository URL: http://hg.python.org/jython


More information about the Jython-checkins mailing list