[Jython-checkins] jython: Re-work __format__, __str__ and __repr__ in float and complex.
jeff.allen
jython-checkins at python.org
Thu Apr 24 00:04:09 CEST 2014
http://hg.python.org/jython/rev/d2b41b8d8368
changeset: 7216:d2b41b8d8368
user: Jeff Allen <ja.py at farowl.co.uk>
date: Mon Apr 21 16:44:33 2014 +0100
summary:
Re-work __format__, __str__ and __repr__ in float and complex.
This addresses certain test failures in test_float and test_complex, and one in test_json.
files:
Lib/test/test_complex.py | 213 ++-
Lib/test/test_float.py | 46 +-
Lib/test/test_float_jy.py | 19 +-
src/org/python/core/PyComplex.java | 110 +-
src/org/python/core/PyFloat.java | 91 +-
src/org/python/core/stringlib/FloatFormatter.java | 880 ++++++++++
src/org/python/core/stringlib/Formatter.java | 422 ----
src/org/python/core/stringlib/InternalFormat.java | 824 +++++++++
8 files changed, 2073 insertions(+), 532 deletions(-)
diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py
--- a/Lib/test/test_complex.py
+++ b/Lib/test/test_complex.py
@@ -220,6 +220,7 @@
def test_conjugate(self):
self.assertClose(complex(5.3, 9.8).conjugate(), 5.3-9.8j)
+ @unittest.skipIf(test_support.is_jython, "FIXME: not working in Jython")
def test_constructor(self):
class OS:
def __init__(self, value): self.value = value
@@ -264,6 +265,188 @@
self.assertAlmostEqual(complex("-1"), -1)
self.assertAlmostEqual(complex("+1"), +1)
#FIXME: these are not working in Jython.
+ self.assertAlmostEqual(complex("(1+2j)"), 1+2j)
+ self.assertAlmostEqual(complex("(1.3+2.2j)"), 1.3+2.2j)
+ # ]
+ self.assertAlmostEqual(complex("3.14+1J"), 3.14+1j)
+ #FIXME: these are not working in Jython.
+ self.assertAlmostEqual(complex(" ( +3.14-6J )"), 3.14-6j)
+ self.assertAlmostEqual(complex(" ( +3.14-J )"), 3.14-1j)
+ self.assertAlmostEqual(complex(" ( +3.14+j )"), 3.14+1j)
+ # ]
+ self.assertAlmostEqual(complex("J"), 1j)
+ #FIXME: this is not working in Jython.
+ self.assertAlmostEqual(complex("( j )"), 1j)
+ # ]
+ self.assertAlmostEqual(complex("+J"), 1j)
+ #FIXME: this is not working in Jython.
+ self.assertAlmostEqual(complex("( -j)"), -1j)
+ # ]
+ self.assertAlmostEqual(complex('1e-500'), 0.0 + 0.0j)
+ self.assertAlmostEqual(complex('-1e-500j'), 0.0 - 0.0j)
+ self.assertAlmostEqual(complex('-1e-500+1e-500j'), -0.0 + 0.0j)
+
+ class complex2(complex): pass
+ self.assertAlmostEqual(complex(complex2(1+1j)), 1+1j)
+ self.assertAlmostEqual(complex(real=17, imag=23), 17+23j)
+ self.assertAlmostEqual(complex(real=17+23j), 17+23j)
+ self.assertAlmostEqual(complex(real=17+23j, imag=23), 17+46j)
+ self.assertAlmostEqual(complex(real=1+2j, imag=3+4j), -3+5j)
+
+ # check that the sign of a zero in the real or imaginary part
+ # is preserved when constructing from two floats. (These checks
+ # are harmless on systems without support for signed zeros.)
+ def split_zeros(x):
+ """Function that produces different results for 0. and -0."""
+ return atan2(x, -1.)
+
+ self.assertEqual(split_zeros(complex(1., 0.).imag), split_zeros(0.))
+ #FIXME: this is not working in Jython.
+ self.assertEqual(split_zeros(complex(1., -0.).imag), split_zeros(-0.))
+ # ]
+ self.assertEqual(split_zeros(complex(0., 1.).real), split_zeros(0.))
+ self.assertEqual(split_zeros(complex(-0., 1.).real), split_zeros(-0.))
+
+ c = 3.14 + 1j
+ self.assertTrue(complex(c) is c)
+ del c
+
+ self.assertRaises(TypeError, complex, "1", "1")
+ self.assertRaises(TypeError, complex, 1, "1")
+
+ if test_support.have_unicode:
+ self.assertEqual(complex(unicode(" 3.14+J ")), 3.14+1j)
+
+ # SF bug 543840: complex(string) accepts strings with \0
+ # Fixed in 2.3.
+ self.assertRaises(ValueError, complex, '1+1j\0j')
+
+ self.assertRaises(TypeError, int, 5+3j)
+ self.assertRaises(TypeError, long, 5+3j)
+ self.assertRaises(TypeError, float, 5+3j)
+ self.assertRaises(ValueError, complex, "")
+ self.assertRaises(TypeError, complex, None)
+ self.assertRaises(ValueError, complex, "\0")
+ self.assertRaises(ValueError, complex, "3\09")
+ self.assertRaises(TypeError, complex, "1", "2")
+ self.assertRaises(TypeError, complex, "1", 42)
+ self.assertRaises(TypeError, complex, 1, "2")
+ self.assertRaises(ValueError, complex, "1+")
+ self.assertRaises(ValueError, complex, "1+1j+1j")
+ self.assertRaises(ValueError, complex, "--")
+ self.assertRaises(ValueError, complex, "(1+2j")
+ self.assertRaises(ValueError, complex, "1+2j)")
+ self.assertRaises(ValueError, complex, "1+(2j)")
+ self.assertRaises(ValueError, complex, "(1+2j)123")
+ if test_support.have_unicode:
+ self.assertRaises(ValueError, complex, unicode("x"))
+ #FIXME: these are raising wrong errors in Jython.
+ self.assertRaises(ValueError, complex, "1j+2")
+ self.assertRaises(ValueError, complex, "1e1ej")
+ self.assertRaises(ValueError, complex, "1e++1ej")
+ self.assertRaises(ValueError, complex, ")1+2j(")
+ # ]
+
+ # the following three are accepted by Python 2.6
+ #FIXME: these are raising wrong errors in Jython.
+ self.assertRaises(ValueError, complex, "1..1j")
+ self.assertRaises(ValueError, complex, "1.11.1j")
+ self.assertRaises(ValueError, complex, "1e1.1j")
+ # ]
+
+ #FIXME: not working in Jython.
+ if test_support.have_unicode:
+ # check that complex accepts long unicode strings
+ self.assertEqual(type(complex(unicode("1"*500))), complex)
+ # ]
+
+ class EvilExc(Exception):
+ pass
+
+ class evilcomplex:
+ def __complex__(self):
+ raise EvilExc
+
+ self.assertRaises(EvilExc, complex, evilcomplex())
+
+ class float2:
+ def __init__(self, value):
+ self.value = value
+ def __float__(self):
+ return self.value
+
+ self.assertAlmostEqual(complex(float2(42.)), 42)
+ self.assertAlmostEqual(complex(real=float2(17.), imag=float2(23.)), 17+23j)
+ self.assertRaises(TypeError, complex, float2(None))
+
+ class complex0(complex):
+ """Test usage of __complex__() when inheriting from 'complex'"""
+ def __complex__(self):
+ return 42j
+
+ class complex1(complex):
+ """Test usage of __complex__() with a __new__() method"""
+ def __new__(self, value=0j):
+ return complex.__new__(self, 2*value)
+ def __complex__(self):
+ return self
+
+ class complex2(complex):
+ """Make sure that __complex__() calls fail if anything other than a
+ complex is returned"""
+ def __complex__(self):
+ return None
+
+ self.assertAlmostEqual(complex(complex0(1j)), 42j)
+ self.assertAlmostEqual(complex(complex1(1j)), 2j)
+ self.assertRaises(TypeError, complex, complex2(1j))
+
+ def test_constructor_jy(self):
+ # These are the parts of test_constructor that work in Jython.
+ # Delete this test when test_constructor skip is removed.
+ class OS:
+ def __init__(self, value): self.value = value
+ def __complex__(self): return self.value
+ class NS(object):
+ def __init__(self, value): self.value = value
+ def __complex__(self): return self.value
+ self.assertEqual(complex(OS(1+10j)), 1+10j)
+ self.assertEqual(complex(NS(1+10j)), 1+10j)
+ self.assertRaises(TypeError, complex, OS(None))
+ self.assertRaises(TypeError, complex, NS(None))
+
+ self.assertAlmostEqual(complex("1+10j"), 1+10j)
+ self.assertAlmostEqual(complex(10), 10+0j)
+ self.assertAlmostEqual(complex(10.0), 10+0j)
+ self.assertAlmostEqual(complex(10L), 10+0j)
+ self.assertAlmostEqual(complex(10+0j), 10+0j)
+ self.assertAlmostEqual(complex(1,10), 1+10j)
+ self.assertAlmostEqual(complex(1,10L), 1+10j)
+ self.assertAlmostEqual(complex(1,10.0), 1+10j)
+ self.assertAlmostEqual(complex(1L,10), 1+10j)
+ self.assertAlmostEqual(complex(1L,10L), 1+10j)
+ self.assertAlmostEqual(complex(1L,10.0), 1+10j)
+ self.assertAlmostEqual(complex(1.0,10), 1+10j)
+ self.assertAlmostEqual(complex(1.0,10L), 1+10j)
+ self.assertAlmostEqual(complex(1.0,10.0), 1+10j)
+ self.assertAlmostEqual(complex(3.14+0j), 3.14+0j)
+ self.assertAlmostEqual(complex(3.14), 3.14+0j)
+ self.assertAlmostEqual(complex(314), 314.0+0j)
+ self.assertAlmostEqual(complex(314L), 314.0+0j)
+ self.assertAlmostEqual(complex(3.14+0j, 0j), 3.14+0j)
+ self.assertAlmostEqual(complex(3.14, 0.0), 3.14+0j)
+ self.assertAlmostEqual(complex(314, 0), 314.0+0j)
+ self.assertAlmostEqual(complex(314L, 0L), 314.0+0j)
+ self.assertAlmostEqual(complex(0j, 3.14j), -3.14+0j)
+ self.assertAlmostEqual(complex(0.0, 3.14j), -3.14+0j)
+ self.assertAlmostEqual(complex(0j, 3.14), 3.14j)
+ self.assertAlmostEqual(complex(0.0, 3.14), 3.14j)
+ self.assertAlmostEqual(complex("1"), 1+0j)
+ self.assertAlmostEqual(complex("1j"), 1j)
+ self.assertAlmostEqual(complex(), 0)
+ self.assertAlmostEqual(complex("-1"), -1)
+ self.assertAlmostEqual(complex("+1"), +1)
+ #FIXME: these are not working in Jython.
#self.assertAlmostEqual(complex("(1+2j)"), 1+2j)
#self.assertAlmostEqual(complex("(1.3+2.2j)"), 1.3+2.2j)
self.assertAlmostEqual(complex("3.14+1J"), 3.14+1j)
@@ -458,7 +641,7 @@
for num in nums:
self.assertAlmostEqual((num.real**2 + num.imag**2) ** 0.5, abs(num))
- @unittest.skipIf(test_support.is_jython, "FIXME: not working in Jython")
+ @unittest.skipIf(test_support.is_jython, "FIXME: str.__complex__ not working in Jython")
def test_repr(self):
self.assertEqual(repr(1+6j), '(1+6j)')
self.assertEqual(repr(1-6j), '(1-6j)')
@@ -482,6 +665,32 @@
self.assertEqual(repr(complex(0, -INF)), "-infj")
self.assertEqual(repr(complex(0, NAN)), "nanj")
+ def test_repr_jy(self):
+ # These are just the cases that Jython can do from test_repr
+ # Delete this test when test_repr passes
+ self.assertEqual(repr(1+6j), '(1+6j)')
+ self.assertEqual(repr(1-6j), '(1-6j)')
+
+ self.assertNotEqual(repr(-(1+0j)), '(-1+-0j)')
+
+ # Fails to round-trip:
+# self.assertEqual(1-6j,complex(repr(1-6j)))
+# self.assertEqual(1+6j,complex(repr(1+6j)))
+# self.assertEqual(-6j,complex(repr(-6j)))
+# self.assertEqual(6j,complex(repr(6j)))
+
+ self.assertEqual(repr(complex(1., INF)), "(1+infj)")
+ self.assertEqual(repr(complex(1., -INF)), "(1-infj)")
+ self.assertEqual(repr(complex(INF, 1)), "(inf+1j)")
+ self.assertEqual(repr(complex(-INF, INF)), "(-inf+infj)")
+ self.assertEqual(repr(complex(NAN, 1)), "(nan+1j)")
+ self.assertEqual(repr(complex(1, NAN)), "(1+nanj)")
+ self.assertEqual(repr(complex(NAN, NAN)), "(nan+nanj)")
+
+ self.assertEqual(repr(complex(0, INF)), "infj")
+ self.assertEqual(repr(complex(0, -INF)), "-infj")
+ self.assertEqual(repr(complex(0, NAN)), "nanj")
+
def test_neg(self):
self.assertEqual(-(1+6j), -1-6j)
@@ -501,7 +710,6 @@
fo.close()
test_support.unlink(test_support.TESTFN)
- @unittest.skipIf(test_support.is_jython, "FIXME: not working in Jython")
def test_getnewargs(self):
self.assertEqual((1+2j).__getnewargs__(), (1.0, 2.0))
self.assertEqual((1-2j).__getnewargs__(), (1.0, -2.0))
@@ -557,7 +765,6 @@
self.assertFloatsAreIdentical(0.0 + z.imag,
0.0 + roundtrip.imag)
- @unittest.skipIf(test_support.is_jython, "FIXME: not working in Jython")
def test_format(self):
# empty format string is same as str()
self.assertEqual(format(1+3j, ''), str(1+3j))
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
@@ -638,16 +638,12 @@
if not math.isnan(arg) and copysign(1.0, arg) > 0.0:
self.assertEqual(fmt % -arg, '-' + rhs)
- @unittest.skipIf(test_support.is_jython,
- "FIXME: not working on Jython")
def test_issue5864(self):
self.assertEqual(format(123.456, '.4'), '123.5')
self.assertEqual(format(1234.56, '.4'), '1.235e+03')
self.assertEqual(format(12345.6, '.4'), '1.235e+04')
class ReprTestCase(unittest.TestCase):
- @unittest.skipIf(test_support.is_jython,
- "FIXME: not working on Jython")
def test_repr(self):
floats_file = open(os.path.join(os.path.split(__file__)[0],
'floating_points.txt'))
@@ -768,6 +764,8 @@
self.assertRaises(OverflowError, round, 1.6e308, -308)
self.assertRaises(OverflowError, round, -1.7e308, -308)
+ @unittest.skipIf(test_support.is_jython,
+ "FIXME: rounding incorrect in Jython")
@unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short',
"test applies only when using short float repr style")
def test_previous_round_bugs(self):
@@ -777,6 +775,8 @@
self.assertEqual(round(56294995342131.5, 3),
56294995342131.5)
+ @unittest.skipIf(test_support.is_jython,
+ "FIXME: rounding incorrect in Jython")
@unittest.skipUnless(getattr(sys, 'float_repr_style', '') == 'short',
"test applies only when using short float repr style")
def test_halfway_cases(self):
@@ -855,7 +855,7 @@
@unittest.skipIf(test_support.is_jython,
- "FIXME: formatting specials imperfect in Jython")
+ "FIXME: %-formatting specials imperfect in Jython")
@requires_IEEE_754
def test_format_specials(self):
# Test formatting of nans and infs.
@@ -890,6 +890,42 @@
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
@@ -14,19 +14,22 @@
def test_float_repr(self):
self.assertEqual(repr(12345678.000000005), '12345678.000000006')
self.assertEqual(repr(12345678.0000000005), '12345678.0')
- self.assertEqual(repr(math.pi**-100),
- jython and '1.9275814160560203e-50' or '1.9275814160560206e-50')
+ self.assertEqual(repr(math.pi**-100), '1.9275814160560206e-50')
self.assertEqual(repr(-1.0), '-1.0')
- self.assertEqual(repr(-9876.543210),
- jython and '-9876.54321' or '-9876.5432099999998')
+ self.assertEqual(repr(-9876.543210), '-9876.54321')
self.assertEqual(repr(0.123456789e+35), '1.23456789e+34')
+ def test_float_repr2(self):
+ # Quite possibly these divergences result from JDK bug JDK-4511638:
+ self.assertEqual(repr(9876.543210e+15),
+ jython and '9.876543209999999e+18' or '9.87654321e+18')
+ self.assertEqual(repr(1235235235235240000.0),
+ jython and '1.2352352352352399e+18' or '1.23523523523524e+18')
+
def test_float_str(self):
self.assertEqual(str(12345678.000005), '12345678.0')
- self.assertEqual(str(12345678.00005),
- jython and '12345678.0' or '12345678.0001')
- self.assertEqual(str(12345678.00005),
- jython and '12345678.0' or '12345678.0001')
+ self.assertEqual(str(12345678.00005), '12345678.0001')
+ self.assertEqual(str(12345678.00005), '12345678.0001')
self.assertEqual(str(12345678.0005), '12345678.0005')
self.assertEqual(str(math.pi**-100), '1.92758141606e-50')
self.assertEqual(str(0.0), '0.0')
diff --git a/src/org/python/core/PyComplex.java b/src/org/python/core/PyComplex.java
--- a/src/org/python/core/PyComplex.java
+++ b/src/org/python/core/PyComplex.java
@@ -2,9 +2,9 @@
// Copyright (c) Jython Developers
package org.python.core;
-import org.python.core.stringlib.Formatter;
-import org.python.core.stringlib.InternalFormatSpec;
-import org.python.core.stringlib.InternalFormatSpecParser;
+import org.python.core.stringlib.FloatFormatter;
+import org.python.core.stringlib.InternalFormat;
+import org.python.core.stringlib.InternalFormat.Spec;
import org.python.expose.ExposedGet;
import org.python.expose.ExposedMethod;
import org.python.expose.ExposedNew;
@@ -19,6 +19,11 @@
public static final PyType TYPE = PyType.fromClass(PyComplex.class);
+ /** Format specification used by repr(). */
+ static final Spec SPEC_REPR = InternalFormat.fromText(" >r"); // but also minFracDigits=0
+ /** Format specification used by str() and none-format. (As CPython, but is that right?) */
+ static final Spec SPEC_STR = InternalFormat.fromText(" >.12g");
+
static PyComplex J = new PyComplex(0, 1.);
@ExposedGet(doc = BuiltinDocs.complex_real_doc)
@@ -132,20 +137,51 @@
@Override
public String toString() {
- return complex_toString();
+ return __str__().toString();
}
- @ExposedMethod(names = {"__repr__", "__str__"}, doc = BuiltinDocs.complex___str___doc)
- final String complex_toString() {
- if (real == 0.) {
- return toString(imag) + "j";
+ @Override
+ public PyString __str__() {
+ return complex___str__();
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.complex___str___doc)
+ final PyString complex___str__() {
+ return Py.newString(formatComplex(SPEC_STR));
+ }
+
+ @Override
+ public PyString __repr__() {
+ return complex___repr__();
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.complex___repr___doc)
+ final PyString complex___repr__() {
+ return Py.newString(formatComplex(SPEC_REPR));
+ }
+
+ /**
+ * Format this complex according to the specification passed in. Supports <code>__str__</code>
+ * and <code>__repr__</code>, and none-format.
+ * <p>
+ * In general, the output is surrounded in parentheses, like <code>"(12.34+24.68j)"</code>.
+ * However, if the real part is zero, only the imaginary part is printed, and without
+ * parentheses like <code>"24.68j"</code>. The number format specification passed in is used
+ * without padding to width, for the real and imaginary parts individually.
+ *
+ * @param spec parsed format specification string
+ * @return formatted value
+ */
+ private String formatComplex(Spec spec) {
+ FloatFormatter f = new FloatFormatter(spec, 2, 3); // Two elements + "(j)".length
+ // Even in r-format, complex strips *all* the trailing zeros.
+ f.setMinFracDigits(0);
+ if (real == 0.0) {
+ f.format(imag).append('j');
} else {
- if (imag >= 0) {
- return String.format("(%s+%sj)", toString(real), toString(imag));
- } else {
- return String.format("(%s-%sj)", toString(real), toString(-imag));
- }
+ f.append('(').format(real).format(imag, "+").append("j)").pad();
}
+ return f.getResult();
}
@Override
@@ -763,7 +799,7 @@
@ExposedMethod(doc = BuiltinDocs.complex___getnewargs___doc)
final PyTuple complex___getnewargs__() {
- return new PyTuple(new PyComplex(real, imag));
+ return new PyTuple(new PyFloat(real), new PyFloat(imag));
}
@Override
@@ -778,10 +814,6 @@
@ExposedMethod(doc = BuiltinDocs.complex___format___doc)
final PyObject complex___format__(PyObject formatSpec) {
- return formatImpl(real, imag, formatSpec);
- }
-
- static PyObject formatImpl(double r, double i, PyObject formatSpec) {
if (!(formatSpec instanceof PyString)) {
throw Py.TypeError("__format__ requires str or unicode");
}
@@ -790,26 +822,32 @@
String result;
try {
String specString = formatSpecStr.getString();
- InternalFormatSpec spec = new InternalFormatSpecParser(specString).parse();
- switch (spec.type) {
- case '\0': // No format code: like 'g', but with at least one decimal.
- case 'e':
- case 'E':
- case 'f':
- case 'F':
- case 'g':
- case 'G':
- case 'n':
- case '%':
- result = Formatter.formatComplex(r, i, spec);
- break;
- default:
- /* unknown */
- throw Py.ValueError(String.format(
- "Unknown format code '%c' for object of type 'complex'", spec.type));
+ Spec spec = InternalFormat.fromText(specString);
+ if (spec.type!=Spec.NONE && "efgEFGn%".indexOf(spec.type) < 0) {
+ throw FloatFormatter.unknownFormat(spec.type, "complex");
+ } else if (spec.alternate) {
+ throw FloatFormatter.alternateFormNotAllowed("complex");
+ } else if (spec.fill == '0') {
+ throw FloatFormatter.zeroPaddingNotAllowed("complex");
+ } else if (spec.align == '=') {
+ throw FloatFormatter.alignmentNotAllowed('=', "complex");
+ } else {
+ if (spec.type == Spec.NONE) {
+ // In none-format, we take the default type and precision from __str__.
+ spec = spec.withDefaults(SPEC_STR);
+ // And then we use the __str__ mechanism to get parentheses or real 0 elision.
+ result = formatComplex(spec);
+ } else {
+ // In any other format, the defaults those commonly used for numeric formats.
+ spec = spec.withDefaults(Spec.NUMERIC);
+ FloatFormatter f = new FloatFormatter(spec, 2, 1);// 2 floats + "j"
+ // Convert as both parts per specification
+ f.format(real).format(imag, "+").append('j').pad();
+ result = f.getResult();
+ }
}
} catch (IllegalArgumentException e) {
- throw Py.ValueError(e.getMessage());
+ throw Py.ValueError(e.getMessage()); // XXX Can this be reached?
}
return formatSpecStr.createInstance(result);
}
diff --git a/src/org/python/core/PyFloat.java b/src/org/python/core/PyFloat.java
--- a/src/org/python/core/PyFloat.java
+++ b/src/org/python/core/PyFloat.java
@@ -5,9 +5,9 @@
import java.io.Serializable;
import java.math.BigDecimal;
-import org.python.core.stringlib.Formatter;
-import org.python.core.stringlib.InternalFormatSpec;
-import org.python.core.stringlib.InternalFormatSpecParser;
+import org.python.core.stringlib.FloatFormatter;
+import org.python.core.stringlib.InternalFormat;
+import org.python.core.stringlib.InternalFormat.Spec;
import org.python.expose.ExposedClassMethod;
import org.python.expose.ExposedGet;
import org.python.expose.ExposedMethod;
@@ -24,9 +24,10 @@
public static final PyType TYPE = PyType.fromClass(PyFloat.class);
- /** Precisions used by repr() and str(), respectively. */
- private static final int PREC_REPR = 17;
- private static final int PREC_STR = 12;
+ /** Format specification used by repr(). */
+ static final Spec SPEC_REPR = InternalFormat.fromText(" >r");
+ /** Format specification used by str(). */
+ static final Spec SPEC_STR = Spec.NUMERIC;
private final double value;
@@ -224,7 +225,7 @@
@ExposedMethod(doc = BuiltinDocs.float___str___doc)
final PyString float___str__() {
- return Py.newString(formatDouble(PREC_STR));
+ return Py.newString(formatDouble(SPEC_STR));
}
@Override
@@ -234,34 +235,19 @@
@ExposedMethod(doc = BuiltinDocs.float___repr___doc)
final PyString float___repr__() {
- return Py.newString(formatDouble(PREC_REPR));
+ return Py.newString(formatDouble(SPEC_REPR));
}
- private String formatDouble(int precision) {
- if (Double.isNaN(value)) {
- return "nan";
- } else if (value == Double.NEGATIVE_INFINITY) {
- return "-inf";
- } else if (value == Double.POSITIVE_INFINITY) {
- return "inf";
- }
-
- String result = String.format("%%.%dg", precision);
- result = Py.newString(result).__mod__(this).toString();
-
- int i = 0;
- if (result.startsWith("-")) {
- i++;
- }
- for (; i < result.length(); i++) {
- if (!Character.isDigit(result.charAt(i))) {
- break;
- }
- }
- if (i == result.length()) {
- result += ".0";
- }
- return result;
+ /**
+ * Format this float according to the specification passed in. Supports <code>__str__</code> and
+ * <code>__repr__</code>.
+ *
+ * @param spec parsed format specification string
+ * @return formatted value
+ */
+ private String formatDouble(Spec spec) {
+ FloatFormatter f = new FloatFormatter(spec);
+ return f.format(value).getResult();
}
@Override
@@ -919,10 +905,6 @@
@ExposedMethod(doc = BuiltinDocs.float___format___doc)
final PyObject float___format__(PyObject formatSpec) {
- return formatImpl(getValue(), formatSpec);
- }
-
- static PyObject formatImpl(double d, PyObject formatSpec) {
if (!(formatSpec instanceof PyString)) {
throw Py.TypeError("__format__ requires str or unicode");
}
@@ -931,29 +913,22 @@
String result;
try {
String specString = formatSpecStr.getString();
- InternalFormatSpec spec = new InternalFormatSpecParser(specString).parse();
- if (spec.type == '\0') {
- return (Py.newFloat(d)).__str__();
- }
- switch (spec.type) {
- case '\0': // No format code: like 'g', but with at least one decimal.
- case 'e':
- case 'E':
- case 'f':
- case 'F':
- case 'g':
- case 'G':
- case 'n':
- case '%':
- result = Formatter.formatFloat(d, spec);
- break;
- default:
- /* unknown */
- throw Py.ValueError(String.format(
- "Unknown format code '%c' for object of type 'float'", spec.type));
+ Spec spec = InternalFormat.fromText(specString);
+ if (spec.type!=Spec.NONE && "efgEFGn%".indexOf(spec.type) < 0) {
+ throw FloatFormatter.unknownFormat(spec.type, "float");
+ } else if (spec.alternate) {
+ throw FloatFormatter.alternateFormNotAllowed("float");
+ } else {
+ // spec may be incomplete. The defaults are those commonly used for numeric formats.
+ spec = spec.withDefaults(Spec.NUMERIC);
+ // Get a formatter for the spec.
+ FloatFormatter f = new FloatFormatter(spec);
+ // Convert as per specification.
+ f.format(value).pad();
+ result = f.getResult();
}
} catch (IllegalArgumentException e) {
- throw Py.ValueError(e.getMessage());
+ throw Py.ValueError(e.getMessage()); // XXX Can this be reached?
}
return formatSpecStr.createInstance(result);
}
diff --git a/src/org/python/core/stringlib/FloatFormatter.java b/src/org/python/core/stringlib/FloatFormatter.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/core/stringlib/FloatFormatter.java
@@ -0,0 +1,880 @@
+// Copyright (c) Jython Developers
+package org.python.core.stringlib;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+
+import org.python.core.stringlib.InternalFormat.Spec;
+
+/**
+ * A class that provides the implementation of floating-point formatting. In a limited way, it acts
+ * like a StringBuilder to which text and one or more numbers may be appended, formatted according
+ * to the format specifier supplied at construction. These are ephemeral objects that are not, on
+ * their own, thread safe.
+ */
+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
+
+ /** If it contains no decimal point, this length is zero, and 1 otherwise. */
+ private int lenPoint;
+ /** The length of the fractional part, right of the decimal point. */
+ private int lenFraction;
+ /** The length of the exponent marker ("e"), "inf" or "nan", or zero if there isn't one. */
+ private int lenMarker;
+ /** The length of the exponent sign and digits or zero if there isn't one. */
+ private int lenExponent;
+ /** if >=0, minimum digits to follow decimal point (where consulted) */
+ private int minFracDigits;
+
+ /**
+ * Construct the formatter from a specification. A reference is held to this specification, but
+ * it will not be modified by the actions of this class.
+ *
+ * @param spec parsed conversion specification
+ */
+ public FloatFormatter(Spec spec) {
+ // Space for result is based on padded width, or precision, whole part & furniture.
+ this(spec, 1, 0);
+ }
+
+ /**
+ * Construct the formatter from a specification and an explicit initial buffer capacity. A
+ * reference is held to this specification, but it will not be modified by the actions of this
+ * class.
+ *
+ * @param spec parsed conversion specification
+ * @param width expected for the formatted result
+ */
+ public FloatFormatter(Spec spec, int width) {
+ super(spec, width);
+ if (spec.alternate) {
+ // Alternate form means do not trim the zero fractional digits.
+ minFracDigits = -1;
+ } else if (spec.type == 'r' || spec.type == Spec.NONE) {
+ // These formats by default show at least one fractional digit.
+ minFracDigits = 1;
+ } else {
+ /*
+ * Every other format (if it does not ignore the setting) will by default trim off all
+ * the trailing zero fractional digits.
+ */
+ minFracDigits = 0;
+ }
+ }
+
+ /**
+ * Construct the formatter from a specification and two extra hints about the initial buffer
+ * capacity. A reference is held to this specification, but it will not be modified by the
+ * actions of this class.
+ *
+ * @param spec parsed conversion specification
+ * @param count of elements likely to be formatted
+ * @param margin for elements formatted only once
+ */
+ public FloatFormatter(Spec spec, int count, int margin) {
+ /*
+ * Rule of thumb used here: in e format w = (p-1) + len("+1.e+300") = p+7; in f format w = p
+ * + len("1,000,000.") = p+10. If we're wrong, the result will have to grow. No big deal.
+ */
+ this(spec, Math.max(spec.width + 1, count * (spec.precision + 10) + margin));
+ }
+
+ /**
+ * Override the default truncation behaviour for the specification originally supplied. Some
+ * formats remove trailing zero digits, trimming to zero or one. Set member
+ * <code>minFracDigits</code>, to modify this behaviour.
+ *
+ * @param minFracDigits if <0 prevent truncation; if >=0 the minimum number of fractional
+ * digits; when this is zero, and all fractional digits are zero, the decimal point
+ * will also be removed.
+ */
+ public void setMinFracDigits(int minFracDigits) {
+ this.minFracDigits = minFracDigits;
+ }
+
+ @Override
+ protected void reset() {
+ // Clear the variables describing the latest number in result.
+ super.reset();
+ lenPoint = lenFraction = lenMarker = lenExponent = 0;
+ }
+
+ @Override
+ protected int[] sectionLengths() {
+ return new int[] {lenSign, lenWhole, lenPoint, lenFraction, lenMarker, lenExponent};
+ }
+
+ /*
+ * Re-implement the text appends so they return the right type.
+ */
+ @Override
+ public FloatFormatter append(char c) {
+ super.append(c);
+ return this;
+ }
+
+ @Override
+ public FloatFormatter append(CharSequence csq) {
+ super.append(csq);
+ return this;
+ }
+
+ @Override
+ public FloatFormatter append(CharSequence csq, int start, int end) //
+ throws IndexOutOfBoundsException {
+ super.append(csq, start, end);
+ return this;
+ }
+
+ /**
+ * Format a floating-point number according to the specification represented by this
+ * <code>FloatFormatter</code>.
+ *
+ * @param value to convert
+ * @return this object
+ */
+ public FloatFormatter format(double value) {
+ return format(value, null);
+ }
+
+ /**
+ * Format a floating-point number according to the specification represented by this
+ * <code>FloatFormatter</code>. The conversion type, precision, and flags for grouping or
+ * percentage are dealt with here. At the point this is used, we know the {@link #spec} is one
+ * of the floating-point types. This entry point allows explicit control of the prefix of
+ * positive numbers, overriding defaults for the format type.
+ *
+ * @param value to convert
+ * @param positivePrefix to use before positive values (e.g. "+") or null to default to ""
+ * @return this object
+ */
+ @SuppressWarnings("fallthrough")
+ public FloatFormatter format(double value, String positivePrefix) {
+
+ // Puts all instance variables back to their starting defaults, and start = result.length().
+ setStart();
+
+ // Precision defaults to 6 (or 12 for none-format)
+ int precision = spec.getPrecision(Spec.specified(spec.type) ? 6 : 12);
+
+ /*
+ * By default, the prefix of a positive number is "", but the format specifier may override
+ * it, and the built-in type complex needs to override the format.
+ */
+ if (positivePrefix == null && Spec.specified(spec.sign) && spec.sign != '-') {
+ positivePrefix = Character.toString(spec.sign);
+ }
+
+ // Different process for each format type, ignoring case for now.
+ switch (Character.toLowerCase(spec.type)) {
+ case 'e':
+ // Exponential case: 1.23e-45
+ format_e(value, positivePrefix, precision);
+ break;
+
+ case 'f':
+ // Fixed case: 123.45
+ format_f(value, positivePrefix, precision);
+ break;
+
+ case 'n':
+ // Locale-sensitive version of g-format should be here. (Désolé de vous decevoir.)
+ // XXX Set a variable here to signal localisation in/after groupDigits?
+ case 'g':
+ // General format: fixed or exponential according to value.
+ format_g(value, positivePrefix, precision, 0);
+ break;
+
+ case Spec.NONE:
+ // None format like g-format but goes exponential at precision-1
+ format_g(value, positivePrefix, precision, -1);
+ break;
+
+ case 'r':
+ // For float.__repr__, very special case, breaks all the rules.
+ format_r(value, positivePrefix);
+ break;
+
+ case '%':
+ // Multiplies by 100 and displays in f-format, followed by a percent sign.
+ format_f(100. * value, positivePrefix, precision);
+ result.append('%');
+ break;
+
+ default:
+ // Should never get here, since this was checked in PyFloat.
+ throw unknownFormat(spec.type, "float");
+ }
+
+ // If the format type is an upper-case letter, convert the result to upper case.
+ if (Character.isUpperCase(spec.type)) {
+ uppercase();
+ }
+
+ // If required to, group the whole-part digits.
+ if (spec.grouping) {
+ groupDigits(3, ',');
+ }
+
+ return this;
+ }
+
+ /**
+ * Convert just the letters in the representation of the current number (in {@link #result}) to
+ * upper case. (That's the exponent marker or the "inf" or "nan".)
+ */
+ @Override
+ protected void uppercase() {
+ int letters = indexOfMarker();
+ int end = letters + lenMarker;
+ for (int i = letters; i < end; i++) {
+ char c = result.charAt(i);
+ result.setCharAt(i, Character.toUpperCase(c));
+ }
+ }
+
+ /**
+ * Common code to deal with the sign, and the special cases "0", "-0", "nan, "inf", or "-inf".
+ * If the method returns <code>false</code>, we have started a non-zero number and the sign is
+ * already in {@link #result}. The client need then only encode <i>abs(value)</i>. If the method
+ * returns <code>true</code>, and {@link #lenMarker}==0, the value was "0" or "-0": the caller
+ * may have to zero-extend this, and/or add an exponent, to match the requested format. If the
+ * method returns <code>true</code>, and {@link #lenMarker}>0, the method has placed "nan, "inf"
+ * in the {@link #result} buffer (preceded by a sign if necessary).
+ *
+ * @param value to convert
+ * @return true if the value was one of "0", "-0", "nan, "inf", or "-inf".
+ * @param positivePrefix to use before positive values (e.g. "+") or null to default to ""
+ */
+ private boolean signAndSpecialNumber(double value, String positivePrefix) {
+
+ // This is easiest via the raw bits
+ long bits = Double.doubleToRawLongBits(value);
+
+ // NaN is always positive
+ if (Double.isNaN(value)) {
+ bits &= ~SIGN_MASK;
+ }
+
+ if ((bits & SIGN_MASK) != 0) {
+ // Negative: encode a minus sign and strip it off bits
+ result.append('-');
+ lenSign = 1;
+ bits &= ~SIGN_MASK;
+
+ } else if (positivePrefix != null) {
+ // Positive, and a prefix is required. Note CPython 2.7 produces "+nan", " nan".
+ result.append(positivePrefix);
+ lenSign = positivePrefix.length();
+ }
+
+ if (bits == 0L) {
+ // All zero means it's zero. (It may have been negative, producing -0.)
+ result.append('0');
+ lenWhole = 1;
+ return true;
+
+ } else if ((bits & EXP_MASK) == EXP_MASK) {
+ // This is characteristic of NaN or Infinity.
+ result.append(((bits & ~EXP_MASK) == 0L) ? "inf" : "nan");
+ lenMarker = 3;
+ return true;
+
+ } else {
+ return false;
+ }
+ }
+
+ private static final long SIGN_MASK = 0x8000000000000000L;
+ private static final long EXP_MASK = 0x7ff0000000000000L;
+
+ /**
+ * The e-format helper function of {@link #format(double, String)} that uses Java's
+ * {@link BigDecimal} to provide conversion and rounding. The converted number is appended to
+ * the {@link #result} buffer, and {@link #start} will be set to the index of its first
+ * character.
+ *
+ * @param value to convert
+ * @param positivePrefix to use before positive values (e.g. "+") or null to default to ""
+ * @param precision precision (maximum number of fractional digits)
+ */
+ private void format_e(double value, String positivePrefix, int precision) {
+
+ // Exponent (default value is for 0.0 and -0.0)
+ int exp = 0;
+
+ if (!signAndSpecialNumber(value, positivePrefix)) {
+ // Convert abs(value) to decimal with p+1 digits of accuracy.
+ MathContext mc = new MathContext(precision + 1, ROUND_PY);
+ BigDecimal vv = new BigDecimal(Math.abs(value), mc);
+
+ // Take explicit control in order to get exponential notation out of BigDecimal.
+ String digits = vv.unscaledValue().toString();
+ int digitCount = digits.length();
+ result.append(digits.charAt(0));
+ lenWhole = 1;
+ if (digitCount > 1) {
+ // There is a fractional part
+ result.append('.').append(digits.substring(1));
+ lenPoint = 1;
+ lenFraction = digitCount - 1;
+ }
+ exp = lenFraction - vv.scale();
+ }
+
+ // Finally add zeros, as necessary, and stick on the exponent.
+
+ if (lenMarker == 0) {
+ appendTrailingZeros(precision);
+ appendExponent(exp);
+ }
+ }
+
+ /**
+ * The f-format inner helper function of {@link #format(double, String)} that uses Java's
+ * {@link BigDecimal} to provide conversion and rounding. The converted number is appended to
+ * the {@link #result} buffer, and {@link #start} will be set to the index of its first
+ * character.
+ *
+ * @param value to convert
+ * @param positivePrefix to use before positive values (e.g. "+") or null to default to ""
+ * @param precision precision (maximum number of fractional digits)
+ */
+ 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 {
+ // Convert value to decimal exactly. (This can be very long.)
+ BigDecimal vLong = new BigDecimal(Math.abs(value));
+
+ // Truncate to the defined number of places to the right of the decimal point).
+ BigDecimal vv = vLong.setScale(precision, ROUND_PY);
+
+ // When converted to text, the number of fractional digits is exactly the scale we set.
+ String raw = vv.toPlainString();
+ result.append(raw);
+ if ((lenFraction = vv.scale()) > 0) {
+ // There is a decimal point and some digits following
+ lenWhole = result.length() - (start + lenSign + (lenPoint = 1) + lenFraction);
+ } else {
+ lenWhole = result.length() - (start + lenSign);
+ }
+
+ }
+ }
+
+ /**
+ * Implementation of the variants of g-format, that uses Java's {@link BigDecimal} to provide
+ * conversion and rounding. These variants are g-format proper, alternate g-format (available
+ * for "%#g" formatting), n-format (as g but subsequently "internationalised"), and none-format
+ * (type code Spec.NONE).
+ * <p>
+ * None-format is the basis of <code>float.__str__</code>.
+ * <p>
+ * According to the Python documentation for g-format, the precise rules are as follows: suppose
+ * that the result formatted with presentation type <code>'e'</code> and precision <i>p-1</i>
+ * would have exponent exp. Then if <i>-4 <= exp < p</i>, the number is formatted with
+ * presentation type <code>'f'</code> and precision <i>p-1-exp</i>. Otherwise, the number is
+ * formatted with presentation type <code>'e'</code> and precision <i>p-1</i>. In both cases
+ * insignificant trailing zeros are removed from the significand, and the decimal point is also
+ * removed if there are no remaining digits following it.
+ * <p>
+ * The Python documentation says none-format is the same as g-format, but the observed behaviour
+ * differs from this, in that f-format is only used if <i>-4 <= exp < p-1</i> (i.e. one
+ * less), and at least one digit to the right of the decimal point is preserved in the f-format
+ * (but not the e-format). That behaviour is controlled through the following arguments, with
+ * these recommended values:
+ *
+ * <table>
+ * <tr>
+ * <th>type</th>
+ * <th>precision</th>
+ * <th>minFracDigits</th>
+ * <th>expThresholdAdj</th>
+ * <td>expThreshold</td>
+ * </tr>
+ * <tr>
+ * <th>g</th>
+ * <td>p</td>
+ * <td>0</td>
+ * <td>0</td>
+ * <td>p</td>
+ * </tr>
+ * <tr>
+ * <th>#g</th>
+ * <td>p</td>
+ * <td>-</td>
+ * <td>0</td>
+ * <td>p</td>
+ * </tr>
+ * <tr>
+ * <th>\0</th>
+ * <td>p</td>
+ * <td>1</td>
+ * <td>-1</td>
+ * <td>p-1</td>
+ * </tr>
+ * <tr>
+ * <th>__str__</th>
+ * <td>12</td>
+ * <td>1</td>
+ * <td>-1</td>
+ * <td>11</td>
+ * </tr>
+ * </table>
+ *
+ * @param value to convert
+ * @param positivePrefix to use before positive values (e.g. "+") or null to default to ""
+ * @param precision total number of significant digits (precision 0 behaves as 1)
+ * @param expThresholdAdj <code>+precision =</code> the exponent at which to resume using
+ * exponential notation
+ */
+ private void format_g(double value, String positivePrefix, int precision, int expThresholdAdj) {
+
+ // Precision 0 behaves as 1
+ precision = Math.max(1, precision);
+
+ // Use exponential notation if exponent would be bigger thatn:
+ int expThreshold = precision + expThresholdAdj;
+
+ if (signAndSpecialNumber(value, positivePrefix)) {
+ // Finish formatting if zero result. (This is a no-op for nan or inf.)
+ zeroHelper(precision, expThreshold);
+
+ } else {
+
+ // Convert abs(value) to decimal with p digits of accuracy.
+ MathContext mc = new MathContext(precision, ROUND_PY);
+ BigDecimal vv = new BigDecimal(Math.abs(value), mc);
+
+ // This gives us the digits we need for either fixed or exponential format.
+ String pointlessDigits = vv.unscaledValue().toString();
+
+ // If we were to complete this as e-format, the exponent would be:
+ int exp = pointlessDigits.length() - vv.scale() - 1;
+
+ if (-4 <= exp && exp < expThreshold) {
+ // Finish the job as f-format with variable-precision p-(exp+1).
+ appendFixed(pointlessDigits, exp, precision);
+
+ } else {
+ // Finish the job as e-format.
+ appendExponential(pointlessDigits, exp);
+ }
+ }
+ }
+
+ /**
+ * Implementation of r-format (<code>float.__repr__</code>) that uses Java's
+ * {@link Double#toString(double)} to provide conversion and rounding. That method gives us
+ * almost what we need, but not quite (sometimes it yields 18 digits): here we always round to
+ * 17 significant digits. Much of the formatting after conversion is shared with
+ * {@link #format_g(double, String, int, int, int)}. <code>minFracDigits</code> is consulted
+ * since while <code>float.__repr__</code> truncates to one digit, within
+ * <code>complex.__repr__</code> we truncate fully.
+ *
+ * @param value to convert
+ * @param positivePrefix to use before positive values (e.g. "+") or null to default to ""
+ */
+ private void format_r(double value, String positivePrefix) {
+
+ // Characteristics of repr (precision = 17 and go exponential at 16).
+ int precision = 17;
+ int expThreshold = precision - 1;
+
+ if (signAndSpecialNumber(value, positivePrefix)) {
+ // Finish formatting if zero result. (This is a no-op for nan or inf.)
+ zeroHelper(precision, expThreshold);
+
+ } else {
+
+ // Generate digit sequence (with no decimal point) with custom rounding.
+ StringBuilder pointlessBuffer = new StringBuilder(20);
+ int exp = reprDigits(Math.abs(value), precision, pointlessBuffer);
+
+ if (-4 <= exp && exp < expThreshold) {
+ // Finish the job as f-format with variable-precision p-(exp+1).
+ appendFixed(pointlessBuffer, exp, precision);
+
+ } else {
+ // Finish the job as e-format.
+ appendExponential(pointlessBuffer, exp);
+ }
+ }
+ }
+
+ /**
+ * Common code for g-format, none-format and r-format called when the conversion yields "inf",
+ * "nan" or zero. The method completes formatting of the zero, with the appropriate number of
+ * decimal places or (in particular circumstances) exponential; notation.
+ *
+ * @param precision of conversion (number of significant digits).
+ * @param expThreshold if zero, causes choice of exponential notation for zero.
+ */
+ private void zeroHelper(int precision, int expThreshold) {
+
+ if (lenMarker == 0) {
+ // May be 0 or -0 so we still need to ...
+ if (minFracDigits < 0) {
+ // In "alternate format", we won't economise trailing zeros.
+ appendPointAndTrailingZeros(precision - 1);
+ } else if (lenFraction < minFracDigits) {
+ // Otherwise, it should be at least the stated minimum length.
+ appendTrailingZeros(minFracDigits);
+ }
+
+ // And just occasionally (in none-format) we go exponential even when exp = 0...
+ if (0 >= expThreshold) {
+ appendExponent(0);
+ }
+ }
+ }
+
+ /**
+ * Common code for g-format, none-format and r-format used when the exponent is such that a
+ * fixed-point presentation is chosen. Normally the method removes trailing digits so as to
+ * shorten the presentation without loss of significance. This method respects the minimum
+ * number of fractional digits (digits after the decimal point), in member
+ * <code>minFracDigits</code>, which is 0 for g-format and 1 for none-format and r-format. When
+ * <code>minFracDigits<0</code> this signifies "no truncation" mode, in which trailing zeros
+ * generated in the conversion are not removed. This supports "%#g" format.
+ *
+ * @param digits from converting the value at a given precision.
+ * @param exp would be the exponent (in e-format), used to position the decimal point.
+ * @param precision of conversion (number of significant digits).
+ */
+
+ private void appendFixed(CharSequence digits, int exp, int precision) {
+
+ // Check for "alternate format", where we won't economise trailing zeros.
+ boolean noTruncate = (minFracDigits < 0);
+
+ int digitCount = digits.length();
+
+ if (exp < 0) {
+ // For a negative exponent, we must insert leading zeros 0.000 ...
+ result.append("0.");
+ lenWhole = lenPoint = 1;
+ for (int i = -1; i > exp; --i) {
+ result.append('0');
+ }
+ // Then the generated digits (always enough to satisfy no-truncate mode).
+ result.append(digits);
+ lenFraction = digitCount - exp - 1;
+
+ } else {
+ // For a non-negative exponent, it's a question of placing the decimal point.
+ int w = exp + 1;
+ if (w < digitCount) {
+ // There are w whole-part digits
+ result.append(digits.subSequence(0, w));
+ lenWhole = w;
+ result.append('.').append(digits.subSequence(w, digitCount));
+ lenPoint = 1;
+ lenFraction = digitCount - w;
+ } else {
+ // All the digits are whole-part digits.
+ result.append(digits);
+ // Just occasionally (in r-format) we need more digits than the precision.
+ while (digitCount < w) {
+ result.append('0');
+ digitCount += 1;
+ }
+ lenWhole = digitCount;
+ }
+
+ if (noTruncate) {
+ // Extend the fraction as BigDecimal will have economised on zeros.
+ appendPointAndTrailingZeros(precision - digitCount);
+ }
+ }
+
+ // Finally, ensure we have all and only the fractional digits we should.
+ if (!noTruncate) {
+ if (lenFraction < minFracDigits) {
+ // Otherwise, it should be at least the stated minimum length.
+ appendTrailingZeros(minFracDigits);
+ } else {
+ // And no more
+ removeTrailingZeros(minFracDigits);
+ }
+ }
+ }
+
+ /**
+ * Common code for g-format, none-format and r-format used when the exponent is such that an
+ * exponential presentation is chosen. Normally the method removes trailing digits so as to
+ * shorten the presentation without loss of significance. Although no minimum number of
+ * fractional digits is enforced in the exponential presentation, when
+ * <code>minFracDigits<0</code> this signifies "no truncation" mode, in which trailing zeros
+ * generated in the conversion are not removed. This supports "%#g" format.
+ *
+ * @param digits from converting the value at a given precision.
+ * @param exp would be the exponent (in e-format), used to position the decimal point.
+ */
+ private void appendExponential(CharSequence digits, int exp) {
+
+ // The whole-part is the first digit.
+ result.append(digits.charAt(0));
+ lenWhole = 1;
+
+ // And the rest of the digits form the fractional part
+ int digitCount = digits.length();
+ result.append('.').append(digits.subSequence(1, digitCount));
+ lenPoint = 1;
+ lenFraction = digitCount - 1;
+
+ // In no-truncate mode, the fraction is full precision. Otherwise trim it.
+ if (minFracDigits >= 0) {
+ // Note minFracDigits only applies to fixed formats.
+ removeTrailingZeros(0);
+ }
+
+ // Finally, append the exponent as e+nn.
+ appendExponent(exp);
+ }
+
+ /**
+ * Convert a double to digits and an exponent for use in <code>float.__repr__</code> (or
+ * r-format). This method takes advantage of (or assumes) a close correspondence between
+ * {@link Double#toString(double)} and Python <code>float.__repr__</code>. The correspondence
+ * appears to be exact, insofar as the Java method produces the minimal non-zero digit string.
+ * It mostly chooses the same number of digits (and the same digits) as the CPython repr, but in
+ * a few cases <code>Double.toString</code> produces more digits. This method truncates to the
+ * number <code>maxDigits</code>, which in practice is always 17.
+ *
+ * @param value to convert
+ * @param maxDigits maximum number of digits to return in <code>buf</code>.
+ * @param buf for digits of result (recommend size be 20)
+ * @return the exponent
+ */
+ private static int reprDigits(double value, int maxDigits, StringBuilder buf) {
+
+ // Most of the work is done by Double.
+ String s = Double.toString(value);
+
+ // Variables for scanning the string
+ int p = 0, end = s.length(), first = 0, point = end, exp;
+ char c = 0;
+ boolean allZero = true;
+
+ // Scan whole part and fractional part digits
+ while (p < end) {
+ c = s.charAt(p++);
+ if (Character.isDigit(c)) {
+ if (allZero) {
+ if (c != '0') {
+ // This is the first non-zero digit.
+ buf.append(c);
+ allZero = false;
+ // p is one *after* the first non-zero digit.
+ first = p;
+ }
+ // Only seen zeros so far: do nothing.
+ } else {
+ // We've started, so every digit counts.
+ buf.append(c);
+ }
+
+ } else if (c == '.') {
+ // We remember this location (one *after* '.') to calculate the exponent later.
+ point = p;
+
+ } else {
+ // Something after the mantissa. (c=='E' we hope.)
+ break;
+ }
+ }
+
+ // Possibly followed by an exponent. p has already advanced past the 'E'.
+ if (p < end && c == 'E') {
+ // If there is an exponent, the mantissa must be in standard form: m.mmmm
+ assert point == first + 1;
+ exp = Integer.parseInt(s.substring(p));
+
+ } else {
+ // Exponent is based on relationship of decimal point and first non-zero digit.
+ exp = point - first - 1;
+ // But that's only correct when the point is to the right (or absent).
+ if (exp < 0) {
+ // The point is to the left of the first digit
+ exp += 1; // = -(first-point)
+ }
+ }
+
+ /*
+ * XXX This still does not round in all the cases it could. I think Java stops generating
+ * digits when the residual is <= ulp/2. This is to neglect the possibility that the extra
+ * ulp/2 (before it becomes a different double) could take us to a rounder numeral. To fix
+ * this, we could express ulp/2 as digits in the same scale as those in the buffer, and
+ * consider adding them. But Java's behaviour here is probably a manifestation of bug
+ * JDK-4511638.
+ */
+
+ // Sometimes the result is more digits than we want for repr.
+ if (buf.length() > maxDigits) {
+ // Chop the trailing digits, remembering the most significant lost digit.
+ int d = buf.charAt(maxDigits);
+ buf.setLength(maxDigits);
+ // We round half up. Not absolutely correct since Double has already rounded.
+ if (d >= '5') {
+ // Treat this as a "carry one" into the numeral buf[0:maxDigits].
+ for (p = maxDigits - 1; p >= 0; p--) {
+ // Each pass of the loop does one carry from buf[p+1] to buf[p].
+ d = buf.charAt(p) + 1;
+ if (d <= '9') {
+ // Carry propagation stops here.
+ buf.setCharAt(p, (char)d);
+ break;
+ } else {
+ // 9 + 1 -> 0 carry 1. Keep looping.
+ buf.setCharAt(p, '0');
+ }
+ }
+ if (p < 0) {
+ /*
+ * We fell off the bottom of the buffer with one carry still to propagate. You
+ * may expect: buf.insert(0, '1') here, but note that every digit in
+ * buf[0:maxDigits] is currently '0', so all we need is:
+ */
+ buf.setCharAt(0, '1');
+ exp += 1;
+ }
+ }
+ }
+
+ return exp;
+ }
+
+ /**
+ * 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.
+ *
+ * @param minFracDigits smallest number of fractional digits on return
+ */
+ private void appendTrailingZeros(int minFracDigits) {
+
+ int f = lenFraction;
+
+ if (minFracDigits > f) {
+ if (lenPoint == 0) {
+ // First need to add a decimal point. (Implies lenFraction=0.)
+ result.append('.');
+ lenPoint = 1;
+ }
+
+ // Now make up the required number of zeros.
+ for (; f < minFracDigits; f++) {
+ result.append('0');
+ }
+ lenFraction = f;
+ }
+ }
+
+ /**
+ * Append the trailing fractional zeros, as required by certain formats, so that the total
+ * number of fractional digits is no less than specified. If there is no decimal point
+ * 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
+ */
+ private void appendPointAndTrailingZeros(int minFracDigits) {
+
+ if (lenPoint == 0) {
+ // First need to add a decimal point. (Implies lenFraction=0.)
+ result.append('.');
+ lenPoint = 1;
+ }
+
+ // Now make up the required number of zeros.
+ int f;
+ for (f = lenFraction; f < minFracDigits; f++) {
+ result.append('0');
+ }
+ lenFraction = f;
+ }
+
+ /**
+ * Remove trailing zeros from the fractional part, as required by certain formats, leaving at
+ * 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
+ */
+ private void removeTrailingZeros(int minFracDigits) {
+
+ int f = lenFraction;
+
+ if (lenPoint > 0) {
+ // There's a decimal point at least, and there may be some fractional digits.
+ if (minFracDigits == 0 || f > minFracDigits) {
+
+ int fracStart = result.length() - f;
+ for (; f > minFracDigits; --f) {
+ if (result.charAt(fracStart - 1 + f) != '0') {
+ // Keeping this one as it isn't a zero
+ break;
+ }
+ }
+
+ // f is now the number of fractional digits we wish to retain.
+ if (f == 0 && lenPoint > 0) {
+ // We will be stripping all the fractional digits. Take the decimal point too.
+ lenPoint = lenFraction = 0;
+ f = -1;
+ } else {
+ lenFraction = f;
+ }
+
+ // Snip the characters we are going to remove (if any).
+ if (fracStart + f < result.length()) {
+ result.setLength(fracStart + f);
+ }
+ }
+ }
+ }
+
+ /**
+ * Append the current value of {@code exp} in the format <code>"e{:+02d}"</code> (for example
+ * <code>e+05</code>, <code>e-10</code>, <code>e+308</code> , etc.).
+ *
+ * @param exp exponent value to append
+ */
+ private void appendExponent(int exp) {
+
+ int marker = result.length();
+ String e;
+
+ // Deal with sign and leading-zero convention by explicit tests.
+ if (exp < 0) {
+ e = (exp <= -10) ? "e-" : "e-0";
+ exp = -exp;
+ } else {
+ e = (exp < 10) ? "e+0" : "e+";
+ }
+
+ result.append(e).append(exp);
+ lenMarker = 1;
+ lenExponent = result.length() - marker - 1;
+ }
+
+ /**
+ * Return the index in {@link #result} of the first letter. helper for {@link #uppercase()} and
+ * {@link #getExponent()}
+ */
+ private int indexOfMarker() {
+ return start + lenSign + lenWhole + lenPoint + lenFraction;
+ }
+
+}
diff --git a/src/org/python/core/stringlib/Formatter.java b/src/org/python/core/stringlib/Formatter.java
deleted file mode 100644
--- a/src/org/python/core/stringlib/Formatter.java
+++ /dev/null
@@ -1,422 +0,0 @@
-package org.python.core.stringlib;
-
-import java.text.DecimalFormat;
-import java.text.DecimalFormatSymbols;
-
-import org.python.core.Py;
-import org.python.core.util.ExtraMath;
-
-/**
- * This class provides an approximate equivalent to corresponding parts of CPython's
- * "~/Objects/stringlib/formatter.h", by concentrating in one place the formatting capabilities of
- * built-in numeric types float and complex.
- */
-public class Formatter {
-
- /**
- * Format a floating-point value according to a conversion specification (created by
- * {@link InternalFormatSpecParser#parse()}), the type of which must be one of
- * <code>{efgEFG%}</code>, including padding to width.
- *
- * @param value to convert
- * @param spec for a floating-point conversion
- * @return formatted result
- */
- public static String formatFloat(double value, InternalFormatSpec spec) {
- InternalFormatter f = new InternalFormatter(spec);
- String string = f.format(value);
- return spec.pad(string, '>', 0);
- }
-
- /**
- * Format a complex value according to a conversion specification (created by
- * {@link InternalFormatSpecParser#parse()}), the type of which must be one of
- * <code>{efgEFG}</code>, including padding to width. The operation is effectively the
- * application of the floating-point format to the real an imaginary parts, then the addition of
- * padding once.
- *
- * @param value to convert
- * @param spec for a floating-point conversion
- * @return formatted result
- */
- public static String formatComplex(double real, double imag, InternalFormatSpec spec) {
- String string;
- InternalFormatter f = new InternalFormatter(spec);
- String r = f.format(real);
- String i = f.format(imag);
- if (i.charAt(0) == '-') {
- string = r + i + "j";
- } else {
- string = r + "+" + i + "j";
- }
- return spec.pad(string, '>', 0);
- }
-}
-
-
-/**
- * A class that provides the implementation of floating-point formatting, and holds a conversion
- * specification (created by {@link InternalFormatSpecParser#parse()}), a derived precision, and the
- * sign of the number being converted.
- */
-// Adapted from PyString's StringFormatter class.
-final class InternalFormatter {
-
- InternalFormatSpec spec;
- boolean negative;
- int precision;
-
- /**
- * Construct the formatter from a specification: default missing {@link #precision} to 6.
- *
- * @param spec parsed conversion specification
- */
- public InternalFormatter(InternalFormatSpec spec) {
- this.spec = spec;
- this.precision = spec.precision;
- if (this.precision == -1) {
- this.precision = 6;
- }
- }
-
- /**
- * If {@link #precision} exceeds an implementation limit, raise {@link Py#OverflowError}.
- *
- * @param type to name as the type being formatted
- */
- private void checkPrecision(String type) {
- if (precision > 250) {
- // A magic number. Larger than in CPython.
- throw Py.OverflowError("formatted " + type + " is too long (precision too long?)");
- }
-
- }
-
- /**
- * Format <code>abs(e)</code> (in the given radix) with zero-padding to 2 decimal places, and
- * store <code>sgn(e)</code> in {@link #negative}.
- *
- * @param e to convert
- * @param radix in which to express
- * @return string value of <code>abs(e)</code> base <code>radix</code>.
- */
- private String formatExp(long e, int radix) {
- checkPrecision("integer");
- if (e < 0) {
- negative = true;
- e = -e;
- }
- String s = Long.toString(e, radix);
- while (s.length() < 2) {
- s = "0" + s;
- }
- return s;
- }
-
- /**
- * Holds in its {@link #template} member, a {@link DecimalFormat} initialised for fixed point
- * float formatting.
- */
- 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);
- }
- }
-
- /**
- * Return a copy of the pre-configured {@link DecimalFormatTemplate#template}, which may be
- * further customised by the client.
- *
- * @return the template
- */
- private static final DecimalFormat getDecimalFormat() {
- return (DecimalFormat)DecimalFormatTemplate.template.clone();
- }
-
- /**
- * Holds in its {@link #template} member, a {@link DecimalFormat} initialised for fixed point
- * float formatting with percentage scaling and furniture.
- */
- static class PercentageFormatTemplate {
-
- 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);
- }
- }
-
- /**
- * Return a copy of the pre-configured {@link PercentageFormatTemplate#template}, which may be
- * further customised by the client.
- *
- * @return the template
- */
- private static final DecimalFormat getPercentageFormat() {
- return (DecimalFormat)PercentageFormatTemplate.template.clone();
- }
-
- /**
- * Format <code>abs(v)</code> in <code>'{f}'</code> format to {@link #precision} (places after
- * decimal point), and store <code>sgn(v)</code> in {@link #negative}. Truncation is provided
- * for that will remove trailing zeros and the decimal point (e.g. <code>1.200</code> becomes
- * <code>1.2</code>, and <code>4.000</code> becomes <code>4</code>. This treatment is to support
- * <code>'{g}'</code> format. (Also potentially <code>'%g'</code> format.) Truncation is not
- * used (cannot validly be specified) for <code>'{f}'</code> format.
- *
- * @param v to convert
- * @param truncate if <code>true</code> strip trailing zeros and decimal point
- * @return converted value
- */
- private String formatFloatDecimal(double v, boolean truncate) {
-
- checkPrecision("decimal");
-
- // Separate the sign from v
- if (v < 0) {
- v = -v;
- negative = true;
- }
-
- // Configure a DecimalFormat: express truncation via minimumFractionDigits
- DecimalFormat decimalFormat = getDecimalFormat();
- decimalFormat.setMaximumFractionDigits(precision);
- decimalFormat.setMinimumFractionDigits(truncate ? 0 : precision);
-
- // The DecimalFormat is already configured to group by comma at group size 3.
- if (spec.thousands_separators) {
- decimalFormat.setGroupingUsed(true);
- }
-
- String ret = decimalFormat.format(v);
- return ret;
- }
-
- /**
- * Format <code>100*abs(v)</code> to {@link #precision} (places after decimal point), with a '%'
- * (percent) sign following, and store <code>sgn(v)</code> in {@link #negative}.
- *
- * @param v to convert
- * @param truncate if <code>true</code> strip trailing zeros
- * @return converted value
- */
- private String formatPercentage(double v, boolean truncate) {
-
- checkPrecision("decimal");
-
- // Separate the sign from v
- if (v < 0) {
- v = -v;
- negative = true;
- }
-
- // Configure a DecimalFormat: express truncation via minimumFractionDigits
- // XXX but truncation cannot be specified with % format!
- DecimalFormat decimalFormat = getPercentageFormat();
- decimalFormat.setMaximumFractionDigits(precision);
- decimalFormat.setMinimumFractionDigits(truncate ? 0 : precision);
-
- String ret = decimalFormat.format(v);
- return ret;
- }
-
- /**
- * Format <code>abs(v)</code> in <code>'{e}'</code> format to {@link #precision} (places after
- * decimal point), and store <code>sgn(v)</code> in {@link #negative}. Truncation is provided
- * for that will remove trailing zeros and the decimal point before the exponential part (e.g.
- * <code>1.200e+04</code> becomes <code>1.2e+04</code>, and <code>4.000e+05</code> becomes
- * <code>4e+05</code>. This treatment is to support <code>'{g}'</code> format. (Also potentially
- * <code>'%g'</code> format.) Truncation is not used (cannot validly be specified) for
- * <code>'{e}'</code> format.
- *
- * @param v to convert
- * @param truncate if <code>true</code> strip trailing zeros and decimal point
- * @return converted value
- */
- private String formatFloatExponential(double v, char e, boolean truncate) {
-
- // Separate the sign from v
- boolean isNegative = false;
- if (v < 0) {
- v = -v;
- isNegative = true;
- }
-
- /*
- * Separate treatment is given to the exponent (I think) because java.text.DecimalFormat
- * will insert a sign in a positive exponent, as in 1.234e+45 where Java writes 1.234E45.
- */
-
- // Power of 10 that will be the exponent.
- double power = 0.0;
- if (v > 0) {
- // That is, if not zero (or NaN)
- power = ExtraMath.closeFloor(Math.log10(v));
- }
-
- // Get exponent (as text)
- String exp = formatExp((long)power, 10);
- if (negative) {
- // This is the sign of the power-of-ten *exponent*
- negative = false;
- exp = '-' + exp;
- } else {
- exp = '+' + exp;
- }
-
- // Format the mantissa as a fixed point number
- double base = v / Math.pow(10, power);
- StringBuilder buf = new StringBuilder();
- buf.append(formatFloatDecimal(base, truncate));
- buf.append(e);
-
- buf.append(exp);
- negative = isNegative;
-
- return buf.toString();
- }
-
- /**
- * Format a floating-point number according to the specification represented by this
- * <code>InternalFormatter</code>. The conversion type, precision, and flags for grouping or
- * percentage are dealt with here. At the point this is used, we know the {@link #spec} has type
- * in <code>{efgEFG}</code>.
- *
- * @param value to convert
- * @return formatted version
- */
- @SuppressWarnings("fallthrough")
- public String format(double value) {
-
- // XXX Possible duplication in handling NaN and upper/lower case here when methiods
- // floatFormatDecimal, formatFloatExponential, etc. appear to do those things.
-
- String string; // return value
-
- if (spec.alternate) {
- // XXX in %g, but not {:g} alternate form means always include a decimal point
- throw Py.ValueError("Alternate form (#) not allowed in float format specifier");
- }
-
- int sign = Double.compare(value, 0.0d);
-
- if (Double.isNaN(value)) {
- // Express NaN cased according to the conversion type.
- if (spec.type == 'E' || spec.type == 'F' || spec.type == 'G') {
- string = "NAN";
- } else {
- string = "nan";
- }
-
- } else if (Double.isInfinite(value)) {
- // Express signed infinity cased according to the conversion type.
- if (spec.type == 'E' || spec.type == 'F' || spec.type == 'G') {
- if (value > 0) {
- string = "INF";
- } else {
- string = "-INF";
- }
- } else {
- if (value > 0) {
- string = "inf";
- } else {
- string = "-inf";
- }
- }
-
- } else {
-
- switch (spec.type) {
- case 'e':
- case 'E':
- // Exponential case: 1.23e-45
- string = formatFloatExponential(value, spec.type, false);
- if (spec.type == 'E') {
- string = string.toUpperCase();
- }
- break;
-
- case 'f':
- case 'F':
- // Fixed case: 123.45
- string = formatFloatDecimal(value, false);
- if (spec.type == 'F') {
- string = string.toUpperCase();
- }
- break;
-
- case 'g':
- case 'G':
- // Mixed "general" case: e or f format according to exponent.
- // XXX technique not wholly effective, for example on 0.0000999999999999995.
- int exponent =
- (int)ExtraMath.closeFloor(Math.log10(Math.abs(value == 0 ? 1 : value)));
- int origPrecision = precision;
- /*
- * (Python docs) Suppose formatting with presentation type 'e' and precision p-1
- * would give exponent exp. Then if -4 <= exp < p, ...
- */
- if (exponent >= -4 && exponent < precision) {
- /*
- * ... the number is formatted with presentation type 'f' and precision
- * p-1-exp.
- */
- precision -= exponent + 1;
- string = formatFloatDecimal(value, !spec.alternate);
- } else {
- /*
- * ... Otherwise, the number is formatted with presentation type 'e' and
- * precision p-1.
- */
- precision--;
- string =
- formatFloatExponential(value, (char)(spec.type - 2),
- !spec.alternate);
- }
- if (spec.type == 'G') {
- string = string.toUpperCase();
- }
- precision = origPrecision;
- break;
-
- case '%':
- // Multiplies by 100 and displays in f-format, followed by a percent sign.
- string = formatPercentage(value, false);
- break;
-
- default:
- // Should never get here, since this was checked in PyFloat.
- throw Py.ValueError(String.format(
- "Unknown format code '%c' for object of type 'float'", spec.type));
- }
- }
-
- // If positive, deal with mandatory sign, or mandatory space.
- if (sign >= 0) {
- if (spec.sign == '+') {
- string = "+" + string;
- } else if (spec.sign == ' ') {
- string = " " + string;
- }
- }
-
- // If negative, insert a minus sign where needed, and we haven't already (e.g. "-inf").
- if (sign < 0 && string.charAt(0) != '-') {
- string = "-" + string;
- }
- return string;
- }
-}
diff --git a/src/org/python/core/stringlib/InternalFormat.java b/src/org/python/core/stringlib/InternalFormat.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/core/stringlib/InternalFormat.java
@@ -0,0 +1,824 @@
+// Copyright (c) Jython Developers
+package org.python.core.stringlib;
+
+import org.python.core.Py;
+import org.python.core.PyException;
+
+public class InternalFormat {
+
+ /**
+ * Create a {@link Spec} object by parsing a format specification.
+ *
+ * @param text to parse
+ * @return parsed equivalent to text
+ */
+ public static Spec fromText(String text) {
+ Parser parser = new Parser(text);
+ return parser.parse();
+ }
+
+ /**
+ * A class that provides the base for implementations of type-specific formatting. In a limited
+ * way, it acts like a StringBuilder to which text and one or more numbers may be appended,
+ * formatted according to the format specifier supplied at construction. These are ephemeral
+ * objects that are not, on their own, thread safe.
+ */
+ public static class Formatter implements Appendable {
+
+ /** The specification according to which we format any number supplied to the method. */
+ protected final Spec spec;
+ /** The (partial) result. */
+ protected StringBuilder result;
+
+ /** The number we are working on floats at the end of the result, and starts here. */
+ protected int start;
+ /** If it contains no sign, this length is zero, and 1 otherwise. */
+ protected int lenSign;
+ /** The length of the whole part (to left of the decimal point or exponent) */
+ protected int lenWhole;
+
+ /**
+ * Construct the formatter from a specification and initial buffer capacity. A reference is
+ * held to this specification, but it will not be modified by the actions of this class.
+ *
+ * @param spec parsed conversion specification
+ * @param width of buffer initially
+ */
+ public Formatter(Spec spec, int width) {
+ this.spec = spec;
+ result = new StringBuilder(width);
+ }
+
+ /**
+ * Current (possibly final) result of the formatting, as a <code>String</code>.
+ *
+ * @return formatted result
+ */
+ public String getResult() {
+ return result.toString();
+ }
+
+ /*
+ * Implement Appendable interface by delegation to the result buffer.
+ *
+ * @see java.lang.Appendable#append(char)
+ */
+ @Override
+ public Formatter append(char c) {
+ result.append(c);
+ return this;
+ }
+
+ @Override
+ public Formatter append(CharSequence csq) {
+ result.append(csq);
+ return this;
+ }
+
+ @Override
+ public Formatter append(CharSequence csq, int start, int end) //
+ throws IndexOutOfBoundsException {
+ result.append(csq, start, end);
+ return this;
+ }
+
+ /**
+ * Clear the instance variables describing the latest object in {@link #result}, ready to
+ * receive a new number
+ */
+ public void setStart() {
+ // Mark the end of the buffer as the start of the current object and reset all.
+ start = result.length();
+ // Clear the variable describing the latest number in result.
+ reset();
+ }
+
+ /**
+ * Clear the instance variables describing the latest object in {@link #result}, ready to
+ * receive a new one.
+ */
+ protected void reset() {
+ // Clear the variable describing the latest object in result.
+ lenSign = lenWhole = 0;
+ }
+
+ /**
+ * Supports {@link #toString()} by returning the lengths of the successive sections in the
+ * result buffer, used for navigation relative to {@link #start}. The <code>toString</code>
+ * method shows a '|' character between each section when it prints out the buffer. Override
+ * this when you define more lengths in the subclass.
+ *
+ * @return
+ */
+ protected int[] sectionLengths() {
+ return new int[] {lenSign, lenWhole};
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * Overridden to provide a debugging view in which the actual text is shown divided up by
+ * the <code>len*</code> member variables. If the dividers don't look right, those variables
+ * have not remained consistent with the text.
+ */
+ @Override
+ public String toString() {
+ if (result == null) {
+ return ("[]");
+ } else {
+ StringBuilder buf = new StringBuilder(result.length() + 20);
+ buf.append(result);
+ try {
+ int p = start;
+ buf.insert(p++, '[');
+ for (int len : sectionLengths()) {
+ p += len;
+ buf.insert(p++, '|');
+ }
+ buf.setCharAt(p - 1, ']');
+ } catch (IndexOutOfBoundsException e) {
+ // Some length took us beyond the end of the result buffer. Pass.
+ }
+ return buf.toString();
+ }
+ }
+
+ /**
+ * Insert grouping characters (conventionally commas) into the whole part of the number.
+ * {@link #lenWhole} will increase correspondingly.
+ *
+ * @param groupSize normally 3.
+ * @param comma or some other character to use as a separator.
+ */
+ protected void groupDigits(int groupSize, char comma) {
+
+ // Work out how many commas (or whatever) it takes to group the whole-number part.
+ int commasNeeded = (lenWhole - 1) / groupSize;
+
+ if (commasNeeded > 0) {
+ // Index *just after* the current last digit of the whole part of the number.
+ int from = start + lenSign + lenWhole;
+ // Open a space into which the whole part will expand.
+ makeSpaceAt(from, commasNeeded);
+ // Index *just after* the end of that space.
+ int to = from + commasNeeded;
+ // The whole part will be longer by the number of commas to be inserted.
+ lenWhole += commasNeeded;
+
+ /*
+ * Now working from high to low, copy all the digits that have to move. Each pass
+ * copies one group and inserts a comma, which makes the to-pointer move one place
+ * extra. The to-pointer descends upon the from-pointer from the right.
+ */
+ while (to > from) {
+ // Copy a group
+ for (int i = 0; i < groupSize; i++) {
+ result.setCharAt(--to, result.charAt(--from));
+ }
+ // Write the comma that precedes it.
+ result.setCharAt(--to, comma);
+ }
+ }
+ }
+
+ /**
+ * Make a space in {@link #result} of a certain size and position. On return, the segment
+ * lengths are likely to be invalid until the caller adjusts them corresponding to the
+ * insertion. There is no guarantee what the opened space contains.
+ *
+ * @param pos at which to make the space
+ * @param size of the space
+ */
+ protected void makeSpaceAt(int pos, int size) {
+ int n = result.length();
+ if (pos < n) {
+ // Space is not at the end: must copy what's to the right of pos.
+ String tail = result.substring(pos);
+ result.setLength(n + size);
+ result.replace(pos + size, n + size, tail);
+ } else {
+ // Space is at the end.
+ result.setLength(n + size);
+ }
+ }
+
+ /**
+ * Convert letters in the representation of the current number (in {@link #result}) to upper
+ * case.
+ */
+ protected void uppercase() {
+ int end = result.length();
+ for (int i = start; i < end; i++) {
+ char c = result.charAt(i);
+ result.setCharAt(i, Character.toUpperCase(c));
+ }
+ }
+
+ /**
+ * Pad the result so far (defined as the entire contents of {@link #result}) using the
+ * alignment, target width and fill character defined in {@link #spec}. The action of
+ * padding will increase the overall length of the result to the target width, if that is
+ * greater than the current length.
+ * <p>
+ * When the padding method has decided that that it needs to add n padding characters, it
+ * will affect {@link #start} or {@link #lenSign} as follows.
+ * <table border style>
+ * <tr>
+ * <th>align</th>
+ * <th>meaning</th>
+ * <th>start</th>
+ * <th>lenSign</th>
+ * <th>result.length()</th>
+ * </tr>
+ * <tr>
+ * <th><</th>
+ * <td>left-aligned</td>
+ * <td>+0</td>
+ * <td>+0</td>
+ * <td>+n</td>
+ * </tr>
+ * <tr>
+ * <th>></th>
+ * <td>right-aligned</td>
+ * <td>+n</td>
+ * <td>+0</td>
+ * <td>+n</td>
+ * </tr>
+ * <tr>
+ * <th>^</th>
+ * <td>centred</td>
+ * <td>+(n/2)</td>
+ * <td>+0</td>
+ * <td>+n</td>
+ * </tr>
+ * <tr>
+ * <th>=</th>
+ * <td>pad after sign</td>
+ * <td>+0</td>
+ * <td>+n</td>
+ * <td>+n</td>
+ * </tr>
+ * </table>
+ * Note that we may have converted more than one value into the result buffer (for example
+ * when formatting a complex number). The pointer <code>start</code> is at the start of the
+ * last number converted. Padding with zeros, and the "pad after sign" mode, will produce a
+ * result you probably don't want. It is up to the client to disallow this (which
+ * <code>complex</code> does).
+ *
+ * @param value to pad
+ * @return this object
+ */
+ public Formatter pad() {
+
+ // We'll need this many pad characters (if>0). Note Spec.UNDEFINED<0.
+ int n = spec.width - result.length();
+ if (n > 0) {
+
+ char align = spec.getAlign('>'); // Right for numbers (wrong for strings)
+ char fill = spec.getFill(' ');
+
+ // Start by assuming padding is all leading ('>' case or '=')
+ int leading = n;
+
+ // Split the total padding according to the alignment
+ if (align == '^') {
+ // Half the padding before
+ leading = n / 2;
+ } else if (align == '<') {
+ // All the padding after
+ leading = 0;
+ }
+
+ // All padding that is not leading is trailing
+ int trailing = n - leading;
+
+ // Insert the leading space
+ if (leading > 0) {
+ int pos;
+ if (align == '=') {
+ // Incorporate into the (latest) whole part
+ pos = start + lenSign;
+ lenWhole += leading;
+ } else {
+ // Insert at the very beginning (not start) by default.
+ pos = 0;
+ start += leading;
+ }
+ makeSpaceAt(pos, leading);
+ for (int i = 0; i < leading; i++) {
+ result.setCharAt(pos + i, fill);
+ }
+ }
+
+ // Append the trailing space
+ for (int i = 0; i < trailing; i++) {
+ result.append(fill);
+ }
+
+ // Check for special case
+ if (align == '=' && fill == '0' && spec.grouping) {
+ // We must extend the grouping separator into the padding
+ zeroPadAfterSignWithGroupingFixup(3, ',');
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * Fix-up the zero-padding of the last formatted number in {@link #result()} in the special
+ * case where a sign-aware padding (<code>{@link #spec}.align='='</code>) was requested, the
+ * fill character is <code>'0'</code>, and the digits are to be grouped. In these exact
+ * circumstances, the grouping, which must already have been applied to the (whole part)
+ * number itself, has to be extended into the zero-padding.
+ *
+ * <pre>
+ * >>> format(-12e8, " =30,.3f")
+ * '- 1,200,000,000.000'
+ * >>> format(-12e8, "*=30,.3f")
+ * '-************1,200,000,000.000'
+ * >>> format(-12e8, "*>30,.3f")
+ * '************-1,200,000,000.000'
+ * >>> format(-12e8, "0>30,.3f")
+ * '000000000000-1,200,000,000.000'
+ * >>> format(-12e8, "0=30,.3f")
+ * '-0,000,000,001,200,000,000.000'
+ * </pre>
+ *
+ * The padding has increased the overall length of the result to the target width. About one
+ * in three call to this method adds one to the width, because the whole part cannot start
+ * with a comma.
+ *
+ * <pre>
+ * >>> format(-12e8, " =30,.4f")
+ * '- 1,200,000,000.0000'
+ * >>> format(-12e8, "0=30,.4f")
+ * '-<b>0</b>,000,000,001,200,000,000.0000'
+ * </pre>
+ *
+ * Insert grouping characters (conventionally commas) into the whole part of the number.
+ * {@link #lenWhole} will increase correspondingly.
+ *
+ * @param groupSize normally 3.
+ * @param comma or some other character to use as a separator.
+ */
+ protected void zeroPadAfterSignWithGroupingFixup(int groupSize, char comma) {
+ /*
+ * Suppose the format call was format(-12e8, "0=30,.3f"). At this point, we have
+ * something like this in result: .. [-|0000000000001,200,000,000|.|000||]
+ *
+ * All we need do is over-write some of the zeros with the separator comma, in the
+ * portion marked as the whole-part: [-|0,000,000,001,200,000,000|.|000||]
+ */
+
+ // First digit of the whole-part.
+ int firstZero = start + lenSign;
+ // One beyond last digit of the whole-part.
+ int p = firstZero + lenWhole;
+ // Step back down the result array visiting the commas. (Easiest to do all of them.)
+ int step = groupSize + 1;
+ for (p = p - step; p >= firstZero; p -= step) {
+ result.setCharAt(p, comma);
+ }
+
+ // Sometimes the last write was exactly at the first padding zero.
+ if (p + step == firstZero) {
+ /*
+ * Suppose the format call was format(-12e8, "0=30,.4f"). At the beginning, we had
+ * something like this in result: . [-|000000000001,200,000,000|.|0000||]
+ *
+ * And now, result looks like this: [-|0000,000,001,200,000,000|.|0000||] in which
+ * the first zero is wrong as it stands, nor can it just be over-written with a
+ * comma. We have to insert another zero, even though this makes the result longer
+ * than we were given.
+ */
+ result.insert(firstZero, '0');
+ lenWhole += 1;
+ }
+ }
+
+ /**
+ * Convenience method returning a {@link Py#ValueError} reporting:
+ * <p>
+ * <code>"Unknown format code '"+code+"' for object of type '"+forType+"'"</code>
+ *
+ * @param code the presentation type
+ * @param forType the type it was found applied to
+ * @return exception to throw
+ */
+ public static PyException unknownFormat(char code, String forType) {
+ String msg = "Unknown format code '" + code + "' for object of type '" + forType + "'";
+ return Py.ValueError(msg);
+ }
+
+ /**
+ * Convenience method returning a {@link Py#ValueError} reporting that alternate form is not
+ * allowed in a format specifier for the named type.
+ *
+ * @param forType the type it was found applied to
+ * @return exception to throw
+ */
+ public static PyException alternateFormNotAllowed(String forType) {
+ return notAllowed("Alternate form (#)", forType);
+ }
+
+ /**
+ * Convenience method returning a {@link Py#ValueError} reporting that the given alignment
+ * flag is not allowed in a format specifier for the named type.
+ *
+ * @param align type of alignment
+ * @param forType the type it was found applied to
+ * @return exception to throw
+ */
+ public static PyException alignmentNotAllowed(char align, String forType) {
+ return notAllowed("'" + align + "' alignment flag", forType);
+ }
+
+ /**
+ * Convenience method returning a {@link Py#ValueError} reporting that zero padding is not
+ * allowed in a format specifier for the named type.
+ *
+ * @param forType the type it was found applied to
+ * @return exception to throw
+ */
+ public static PyException zeroPaddingNotAllowed(String forType) {
+ return notAllowed("Zero padding", forType);
+ }
+
+ /**
+ * Convenience method returning a {@link Py#ValueError} reporting that some format specifier
+ * feature is not allowed for the named type.
+ *
+ * @param particularOutrage committed in the present case
+ * @param forType the type it where it is an outrage
+ * @return exception to throw
+ */
+ protected static PyException notAllowed(String particularOutrage, String forType) {
+ String msg = particularOutrage + " is not allowed in " + forType + " format specifier";
+ return Py.ValueError(msg);
+ }
+
+ }
+
+ /**
+ * Parsed PEP-3101 format specification of a single field, encapsulating the format for use by
+ * formatting methods. This class holds the several attributes that might be decoded from a
+ * format specifier. Each attribute has a reserved value used to indicate "unspecified".
+ * <code>Spec</code> objects may be merged such that one <code>Spec</code> provides values,
+ * during the construction of a new <code>Spec</code>, for attributes that are unspecified in a
+ * primary source.
+ * <p>
+ * This structure is returned by factory method {@link #fromText(CharSequence)}, and having
+ * public final members is freely accessed by formatters such as {@link FloatBuilder}, and the
+ * __format__ methods of client object types.
+ * <p>
+ * The fields correspond to the elements of a format specification. The grammar of a format
+ * specification is:
+ *
+ * <pre>
+ * [[fill]align][sign][#][0][width][,][.precision][type]
+ * </pre>
+ *
+ * A typical idiom is:
+ *
+ * <pre>
+ * private static final InternalFormatSpec FLOAT_DEFAULT = InternalFormatSpec.from(">");
+ * ...
+ * InternalFormatSpec spec = InternalFormatSpec.from(specString, FLOAT_DEFAULT);
+ * ... // Validation of spec.type, and other attributes, for this type.
+ * FloatBuilder buf = new FloatBuilder(spec);
+ * buf.format(value);
+ * String result = buf.getResult();
+ *
+ * </pre>
+ */
+ public static class Spec {
+
+ /** The fill character specified, or '\uffff' if unspecified. */
+ public final char fill;
+ /**
+ * Alignment indicator is one of {<code>'<', '^', '>', '='</code>, or '\uffff' if
+ * unspecified.
+ */
+ public final char align;
+ /**
+ * Sign-handling flag, one of <code>'+'</code>, <code>'-'</code>, or <code>' '</code>, or
+ * '\uffff' if unspecified.
+ */
+ public final char sign;
+ /** The alternative format flag '#' was given. */
+ public final boolean alternate;
+ /** Width to which to pad the result, or -1 if unspecified. */
+ public final int width;
+ /** Insert the grouping separator (which in Python always indicates a group-size of 3). */
+ public final boolean grouping;
+ /** Precision decoded from the format, or -1 if unspecified. */
+ public final int precision;
+ /** Type key from the format, or '\uffff' if unspecified. */
+ public final char type;
+
+ /** Non-character code point used to represent "no value" in <code>char</code> attributes. */
+ public static final char NONE = '\uffff';
+ /** Negative value used to represent "no value" in <code>int</code> attributes. */
+ public static final int UNSPECIFIED = -1;
+
+ /**
+ * Test to see if an attribute has been specified.
+ *
+ * @param c attribute
+ * @return true only if the attribute is not equal to {@link #NONE}
+ */
+ public static final boolean specified(char c) {
+ return c != NONE;
+ }
+
+ /**
+ * Test to see if an attribute has been specified.
+ *
+ * @param value of attribute
+ * @return true only if the attribute is >=0 (meaning that it has been specified).
+ */
+ public static final boolean specified(int value) {
+ return value >= 0;
+ }
+
+ /**
+ * Constructor to set all the fields in the format specifier.
+ *
+ * <pre>
+ * [[fill]align][sign][#][0][width][,][.precision][type]
+ * </pre>
+ *
+ * @param fill fill character (or {@link #NONE}
+ * @param align alignment indicator, one of {<code>'<', '^', '>', '='</code>
+ * @param sign policy, one of <code>'+'</code>, <code>'-'</code>, or <code>' '</code>.
+ * @param alternate true to request alternate formatting mode (<code>'#'</code> flag).
+ * @param width of field after padding or -1 to default
+ * @param grouping true to request comma-separated groups
+ * @param precision (e.g. decimal places) or -1 to default
+ * @param type indicator character
+ */
+ public Spec(char fill, char align, char sign, boolean alternate, int width,
+ boolean grouping, int precision, char type) {
+ this.fill = fill;
+ this.align = align;
+ this.sign = sign;
+ this.alternate = alternate;
+ this.width = width;
+ this.grouping = grouping;
+ this.precision = precision;
+ this.type = type;
+ }
+
+ /**
+ * Return a format specifier (text) equivalent to the value of this Spec.
+ */
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ if (specified(fill)) {
+ buf.append(fill);
+ }
+ if (specified(align)) {
+ buf.append(align);
+ }
+ if (specified(sign)) {
+ buf.append(sign);
+ }
+ if (alternate) {
+ buf.append('#');
+ }
+ if (specified(width)) {
+ buf.append(width);
+ }
+ if (grouping) {
+ buf.append(',');
+ }
+ if (specified(precision)) {
+ buf.append('.').append(precision);
+ }
+ if (specified(type)) {
+ buf.append(type);
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Return a merged <code>Spec</code> object, in which any attribute of this object, that is
+ * specified (or <code>true</code>) has the same value in the result, and any attribute of
+ * this object that is unspecified (or <code>false</code>) has the value that attribute
+ * takes in the other object. This the second object supplies default values. (These
+ * defaults may also be unspecified.) The use of this method is to allow a <code>Spec</code>
+ * constructed from text to record exactly, and only, what was in the textual specification,
+ * while the __format__ method of a client object supplies its type-specific defaults. Thus
+ * "20" means "<20s" to a <code>str</code>, ">20.12" to a <code>float</code> and ">20.12g"
+ * to a <code>complex</code>.
+ *
+ * @param defaults to merge where this object does not specify the attribute.
+ * @return a new Spec object.
+ */
+ public Spec withDefaults(Spec other) {
+ return new Spec(//
+ specified(fill) ? fill : other.fill, //
+ specified(align) ? align : other.align, //
+ specified(sign) ? sign : other.sign, //
+ alternate || other.alternate, //
+ specified(width) ? width : other.width, //
+ grouping || other.grouping, //
+ specified(precision) ? precision : other.precision, //
+ specified(type) ? type : other.type //
+ );
+ }
+
+ /**
+ * Defaults applicable to most numeric types. Equivalent to " >"
+ */
+ public static final Spec NUMERIC = new Spec(' ', '>', Spec.NONE, false, Spec.UNSPECIFIED,
+ false, Spec.UNSPECIFIED, Spec.NONE);
+
+ /**
+ * Constructor offering just precision and type.
+ *
+ * <pre>
+ * [.precision][type]
+ * </pre>
+ *
+ * @param precision (e.g. decimal places)
+ * @param type indicator character
+ */
+ public Spec(int width, int precision, char type) {
+ this(' ', '>', Spec.NONE, false, UNSPECIFIED, false, precision, type);
+ }
+
+ /** The alignment from the parsed format specification, or default. */
+ public char getFill(char defaultFill) {
+ return specified(fill) ? fill : defaultFill;
+ }
+
+ /** The alignment from the parsed format specification, or default. */
+ public char getAlign(char defaultAlign) {
+ return specified(align) ? align : defaultAlign;
+ }
+
+ /** The precision from the parsed format specification, or default. */
+ public int getPrecision(int defaultPrecision) {
+ return specified(precision) ? precision : defaultPrecision;
+ }
+
+ /** The type code from the parsed format specification, or default supplied. */
+ public char getType(char defaultType) {
+ return specified(type) ? type : defaultType;
+ }
+
+ }
+
+ /**
+ * Parser for PEP-3101 field format specifications. This class provides a {@link #parse()}
+ * method that translates the format specification into an <code>Spec</code> object.
+ */
+ private static class Parser {
+
+ private String spec;
+ private int ptr;
+
+ /**
+ * Constructor simply holds the specification string ahead of the {@link #parse()}
+ * operation.
+ *
+ * @param spec format specifier to parse (e.g. "<+12.3f")
+ */
+ Parser(String spec) {
+ this.spec = spec;
+ this.ptr = 0;
+ }
+
+ /**
+ * Parse the specification with which this object was initialised into an {@link Spec},
+ * which is an object encapsulating the format for use by formatting methods. This parser
+ * deals only with the format specifiers themselves, as accepted by the
+ * <code>__format__</code> method of a type, or the <code>format()</code> built-in, not
+ * format strings in general as accepted by <code>str.format()</code>.
+ *
+ * @return the <code>Spec</code> equivalent to the string given.
+ */
+ /*
+ * This method is the equivalent of CPython's parse_internal_render_format_spec() in
+ * ~/Objects/stringlib/formatter.h, but we deal with defaults another way.
+ */
+ Spec parse() {
+
+ char fill = Spec.NONE, align = Spec.NONE;
+ char sign = Spec.NONE, type = Spec.NONE;
+ boolean alternate = false, grouping = false;
+ int width = Spec.UNSPECIFIED, precision = Spec.UNSPECIFIED;
+
+ // Scan [[fill]align] ...
+ if (isAlign()) {
+ // First is alignment. fill not specified.
+ align = spec.charAt(ptr++);
+ } else {
+ // Peek ahead
+ ptr += 1;
+ if (isAlign()) {
+ // Second character is alignment, so first is fill
+ fill = spec.charAt(0);
+ align = spec.charAt(ptr++);
+ } else {
+ // Second character is not alignment. We are still at square zero.
+ ptr = 0;
+ }
+ }
+
+ // Scan [sign] ...
+ if (isAt("+- ")) {
+ sign = spec.charAt(ptr++);
+ }
+
+ // Scan [#] ...
+ alternate = scanPast('#');
+
+ // Scan [0] ...
+ if (scanPast('0')) {
+ // Accept 0 here as equivalent to zero-fill but only not set already.
+ if (!Spec.specified(fill)) {
+ fill = '0';
+ if (!Spec.specified(align)) {
+ // Also accept it as equivalent to "=" aligment but only not set already.
+ align = '=';
+ }
+ }
+ }
+
+ // Scan [width]
+ if (isDigit()) {
+ width = scanInteger();
+ }
+
+ // Scan [,][.precision][type]
+ grouping = scanPast(',');
+
+ // Scan [.precision]
+ if (scanPast('.')) {
+ if (isDigit()) {
+ precision = scanInteger();
+ } else {
+ throw new IllegalArgumentException("Format specifier missing precision");
+ }
+ }
+
+ // Scan [type]
+ if (ptr < spec.length()) {
+ type = spec.charAt(ptr++);
+ }
+
+ // If we haven't reached the end, something is wrong
+ if (ptr != spec.length()) {
+ throw new IllegalArgumentException("Invalid conversion specification");
+ }
+
+ // Restrict grouping to known formats. (Mirrors CPython, but misplaced?)
+ if (grouping && "defgEG%F\0".indexOf(type) == -1) {
+ throw new IllegalArgumentException("Cannot specify ',' with '" + type + "'.");
+ }
+
+ // Create a specification
+ return new Spec(fill, align, sign, alternate, width, grouping, precision, type);
+ }
+
+ /** Test that the next character is exactly the one specified, and advance past it if it is. */
+ private boolean scanPast(char c) {
+ if (ptr < spec.length() && spec.charAt(ptr) == c) {
+ ptr++;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /** Test that the next character is one of a specified set. */
+ private boolean isAt(String chars) {
+ return ptr < spec.length() && (chars.indexOf(spec.charAt(ptr)) >= 0);
+ }
+
+ /** Test that the next character is one of the alignment characters. */
+ private boolean isAlign() {
+ return ptr < spec.length() && ("<^>=".indexOf(spec.charAt(ptr)) >= 0);
+ }
+
+ /** Test that the next character is a digit. */
+ private boolean isDigit() {
+ return ptr < spec.length() && Character.isDigit(spec.charAt(ptr));
+ }
+
+ /** The current character is a digit (maybe a sign). Scan the integer, */
+ private int scanInteger() {
+ int p = ptr++;
+ while (isDigit()) {
+ ptr++;
+ }
+ return Integer.parseInt(spec.substring(p, ptr));
+ }
+
+ }
+
+}
--
Repository URL: http://hg.python.org/jython
More information about the Jython-checkins
mailing list