Skip to content

Commit 165d233

Browse files
committed
Allow formatters to write results directly into StringFormatter.
Changes the formatting framework to accept a client-owned StringBuilder. Some related clean-up, and a test.
1 parent cb0e4e7 commit 165d233

9 files changed

Lines changed: 209 additions & 130 deletions

File tree

Lib/test/test_format_jy.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,18 @@ def padcheck(self, s):
5555
class FormatMisc(unittest.TestCase):
5656
# Odd tests Jython used to fail
5757

58+
def test_mixtures(self) :
59+
# Check formatting to a common buffer in PyString
60+
result = 'The cube of 0.5 -0.866j is -1 to 0.01%.'
61+
self.assertEqual(result, 'The %s of %.3g -%.3fj is -%d to %.2f%%.' %
62+
('cube', 0.5, 0.866, 1, 0.01))
63+
self.assertEqual(result, 'The %s of %.3g %.3fj is %d to %.2f%%.' %
64+
('cube', 0.5, -0.866, -1, 0.01))
65+
self.assertEqual(result, 'The%5s of%4.3g%7.3fj is%3d to%5.2f%%.' %
66+
('cube', 0.5, -0.866, -1, 0.01))
67+
self.assertEqual(result, 'The %-5.4sof %-4.3g%.3fj is %-3dto %.4g%%.' %
68+
('cubensis', 0.5, -0.866, -1, 0.01))
69+
5870
def test_percent_padded(self) :
5971
self.assertEqual('%hello', '%%%s' % 'hello')
6072
self.assertEqual(u' %hello', '%6%%s' % u'hello')

src/org/python/core/PyComplex.java

Lines changed: 79 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import org.python.core.stringlib.FloatFormatter;
66
import org.python.core.stringlib.InternalFormat;
7+
import org.python.core.stringlib.InternalFormat.Formatter;
78
import org.python.core.stringlib.InternalFormat.Spec;
89
import org.python.expose.ExposedGet;
910
import org.python.expose.ExposedMethod;
@@ -174,7 +175,9 @@ final PyString complex___repr__() {
174175
* @return formatted value
175176
*/
176177
private String formatComplex(Spec spec) {
177-
FloatFormatter f = new FloatFormatter(spec, 2, 3); // Two elements + "(j)".length
178+
int size = 2 * FloatFormatter.size(spec) + 3; // 2 floats + "(j)"
179+
FloatFormatter f = new FloatFormatter(new StringBuilder(size), spec);
180+
f.setBytes(true);
178181
// Even in r-format, complex strips *all* the trailing zeros.
179182
f.setMinFracDigits(0);
180183
if (Double.doubleToLongBits(real) == 0L) {
@@ -816,42 +819,87 @@ public PyObject __format__(PyObject formatSpec) {
816819

817820
@ExposedMethod(doc = BuiltinDocs.complex___format___doc)
818821
final PyObject complex___format__(PyObject formatSpec) {
819-
if (!(formatSpec instanceof PyString)) {
820-
throw Py.TypeError("__format__ requires str or unicode");
821-
}
822822

823+
// Parse the specification
824+
Spec spec = InternalFormat.fromText(formatSpec, "__format__");
825+
826+
// fromText will have thrown if formatSpecStr is not a PyString (including PyUnicode)
823827
PyString formatSpecStr = (PyString)formatSpec;
824828
String result;
825-
try {
826-
String specString = formatSpecStr.getString();
827-
Spec spec = InternalFormat.fromText(specString);
828-
if (spec.type != Spec.NONE && "efgEFGn%".indexOf(spec.type) < 0) {
829-
throw FloatFormatter.unknownFormat(spec.type, "complex");
830-
} else if (spec.alternate) {
831-
throw FloatFormatter.alternateFormNotAllowed("complex");
832-
} else if (spec.fill == '0') {
833-
throw FloatFormatter.zeroPaddingNotAllowed("complex");
834-
} else if (spec.align == '=') {
835-
throw FloatFormatter.alignmentNotAllowed('=', "complex");
836-
} else {
837-
if (spec.type == Spec.NONE) {
838-
// In none-format, we take the default type and precision from __str__.
839-
spec = spec.withDefaults(SPEC_STR);
840-
// And then we use the __str__ mechanism to get parentheses or real 0 elision.
841-
result = formatComplex(spec);
829+
830+
// Validate the specification and detect the special case for none-format
831+
switch (checkSpecification(spec)) {
832+
833+
case 0: // None-format
834+
// In none-format, we take the default type and precision from __str__.
835+
spec = spec.withDefaults(SPEC_STR);
836+
// And then we use the __str__ mechanism to get parentheses or real 0 elision.
837+
result = formatComplex(spec);
838+
break;
839+
840+
case 1: // Floating-point formats
841+
// In any other format, defaults are those commonly used for numeric formats.
842+
spec = spec.withDefaults(Spec.NUMERIC);
843+
int size = 2 * FloatFormatter.size(spec) + 1; // 2 floats + "j"
844+
FloatFormatter f = new FloatFormatter(new StringBuilder(size), spec);
845+
f.setBytes(!(formatSpecStr instanceof PyUnicode));
846+
// Convert both parts as per specification
847+
f.format(real).format(imag, "+").append('j');
848+
result = f.pad().getResult();
849+
break;
850+
851+
default: // The type code was not recognised
852+
throw Formatter.unknownFormat(spec.type, "complex");
853+
}
854+
855+
// Wrap the result in the same type as the format string
856+
return formatSpecStr.createInstance(result);
857+
}
858+
859+
/**
860+
* Validate a parsed specification, for <code>PyComplex</code>, returning 0 if it is a valid
861+
* none-format specification, 1 if it is a valid float specification, and some other value if it
862+
* not a valid type. If it has any other faults (e.g. alternate form was specified) the method
863+
* raises a descriptive exception.
864+
*
865+
* @param spec a parsed PEP-3101 format specification.
866+
* @return 0, 1, or other value for none-format, a float format, or incorrect type.
867+
* @throws PyException(ValueError) if the specification is faulty.
868+
*/
869+
@SuppressWarnings("fallthrough")
870+
private static int checkSpecification(Spec spec) {
871+
872+
// Slight differences between format types
873+
switch (spec.type) {
874+
875+
case 'n':
876+
if (spec.grouping) {
877+
throw Formatter.notAllowed("Grouping", "complex", spec.type);
878+
}
879+
// Fall through
880+
881+
case Spec.NONE:
882+
case 'e':
883+
case 'f':
884+
case 'g':
885+
case 'E':
886+
case 'F':
887+
case 'G':
888+
// Check for disallowed parts of the specification
889+
if (spec.alternate) {
890+
throw FloatFormatter.alternateFormNotAllowed("complex");
891+
} else if (spec.fill == '0') {
892+
throw FloatFormatter.zeroPaddingNotAllowed("complex");
893+
} else if (spec.align == '=') {
894+
throw FloatFormatter.alignmentNotAllowed('=', "complex");
842895
} else {
843-
// In any other format, defaults are those commonly used for numeric formats.
844-
spec = spec.withDefaults(Spec.NUMERIC);
845-
FloatFormatter f = new FloatFormatter(spec, 2, 1);// 2 floats + "j"
846-
// Convert both parts as per specification
847-
f.format(real).format(imag, "+").append('j');
848-
result = f.pad().getResult();
896+
return (spec.type == Spec.NONE) ? 0 : 1;
849897
}
850-
}
851-
} catch (IllegalArgumentException e) {
852-
throw Py.ValueError(e.getMessage()); // XXX Can this be reached?
898+
899+
default:
900+
// spec.type is invalid for complex
901+
return 2;
853902
}
854-
return formatSpecStr.createInstance(result);
855903
}
856904

857905
@Override

src/org/python/core/PyFloat.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44

55
import java.io.Serializable;
66
import java.math.BigDecimal;
7-
import java.math.BigInteger;
87

98
import org.python.core.stringlib.FloatFormatter;
10-
import org.python.core.stringlib.IntegerFormatter;
119
import org.python.core.stringlib.InternalFormat;
1210
import org.python.core.stringlib.InternalFormat.Formatter;
1311
import org.python.core.stringlib.InternalFormat.Spec;
@@ -951,7 +949,7 @@ static FloatFormatter prepareFormatter(Spec spec) {
951949

952950
case 'n':
953951
if (spec.grouping) {
954-
throw IntegerFormatter.notAllowed("Grouping", "float", spec.type);
952+
throw Formatter.notAllowed("Grouping", "float", spec.type);
955953
}
956954
// Fall through
957955

@@ -969,7 +967,7 @@ static FloatFormatter prepareFormatter(Spec spec) {
969967
}
970968
// spec may be incomplete. The defaults are those commonly used for numeric formats.
971969
spec = spec.withDefaults(Spec.NUMERIC);
972-
return new FloatFormatter(spec, 1);
970+
return new FloatFormatter(spec);
973971

974972
default:
975973
return null;

src/org/python/core/PyInteger.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1099,7 +1099,7 @@ static IntegerFormatter prepareFormatter(Spec spec) throws PyException {
10991099
// spec may be incomplete. The defaults are those commonly used for numeric formats.
11001100
spec = spec.withDefaults(Spec.NUMERIC);
11011101
// Get a formatter for the spec.
1102-
return new IntegerFormatter(spec, 1);
1102+
return new IntegerFormatter(spec);
11031103

11041104
default:
11051105
return null;

src/org/python/core/PyString.java

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4436,7 +4436,7 @@ public PyString format(PyObject args) {
44364436
// Get hold of the actual object to display (may set needUnicode)
44374437
PyString argAsString = asText(spec.type == 's' ? arg : arg.__repr__());
44384438
// Format the str/unicode form of the argument using this Spec.
4439-
f = ft = new TextFormatter(spec);
4439+
f = ft = new TextFormatter(buffer, spec);
44404440
ft.setBytes(!needUnicode);
44414441
ft.format(argAsString.getString());
44424442
break;
@@ -4450,7 +4450,7 @@ public PyString format(PyObject args) {
44504450
case 'i': // Compatibility with scanf().
44514451

44524452
// Format the argument using this Spec.
4453-
f = fi = new IntegerFormatter.Traditional(spec);
4453+
f = fi = new IntegerFormatter.Traditional(buffer, spec);
44544454
// If not producing PyUnicode, disallow codes >255.
44554455
fi.setBytes(!needUnicode);
44564456

@@ -4495,7 +4495,7 @@ public PyString format(PyObject args) {
44954495
case 'G':
44964496

44974497
// Format using this Spec the double form of the argument.
4498-
f = ff = new FloatFormatter(spec);
4498+
f = ff = new FloatFormatter(buffer, spec);
44994499
ff.setBytes(!needUnicode);
45004500

45014501
// Note various types accepted here as long as they have a __float__ method.
@@ -4516,7 +4516,7 @@ public PyString format(PyObject args) {
45164516
case '%': // Percent symbol, but surprisingly, padded.
45174517

45184518
// We use an integer formatter.
4519-
f = fi = new IntegerFormatter.Traditional(spec);
4519+
f = fi = new IntegerFormatter.Traditional(buffer, spec);
45204520
fi.setBytes(!needUnicode);
45214521
fi.format('%');
45224522
break;
@@ -4527,8 +4527,8 @@ public PyString format(PyObject args) {
45274527
+ Integer.toHexString(spec.type) + ") at index " + (index - 1));
45284528
}
45294529

4530-
// Pad the result as required in the format and append to the overall result.
4531-
buffer.append(f.pad().getResult());
4530+
// Pad the result as specified (in-place, in the buffer).
4531+
f.pad();
45324532
}
45334533

45344534
/*
@@ -4543,10 +4543,7 @@ public PyString format(PyObject args) {
45434543
}
45444544

45454545
// Return the final buffer contents as a str or unicode as appropriate.
4546-
if (needUnicode) {
4547-
return new PyUnicode(buffer);
4548-
}
4549-
return new PyString(buffer);
4546+
return needUnicode ? new PyUnicode(buffer) : new PyString(buffer);
45504547
}
45514548

45524549
}

src/org/python/core/stringlib/FloatFormatter.java

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,14 @@ public class FloatFormatter extends InternalFormat.Formatter {
3434
private int minFracDigits;
3535

3636
/**
37-
* Construct the formatter from a specification. A reference is held to this specification, but
38-
* it will not be modified by the actions of this class.
37+
* Construct the formatter from a client-supplied buffer, to which the result will be appended,
38+
* and a specification. Sets {@link #mark} to the end of the buffer.
3939
*
40+
* @param result destination buffer
4041
* @param spec parsed conversion specification
4142
*/
42-
public FloatFormatter(Spec spec) {
43-
// Space for result is based on padded width, or precision, whole part & furniture.
44-
this(spec, 1, 0);
45-
}
46-
47-
/**
48-
* Construct the formatter from a specification and an explicit initial buffer capacity. A
49-
* reference is held to this specification, but it will not be modified by the actions of this
50-
* class.
51-
*
52-
* @param spec parsed conversion specification
53-
* @param width expected for the formatted result
54-
*/
55-
public FloatFormatter(Spec spec, int width) {
56-
super(spec, width);
43+
public FloatFormatter(StringBuilder result, Spec spec) {
44+
super(result, spec);
5745
if (spec.alternate) {
5846
// Alternate form means do not trim the zero fractional digits.
5947
minFracDigits = -1;
@@ -70,20 +58,26 @@ public FloatFormatter(Spec spec, int width) {
7058
}
7159

7260
/**
73-
* Construct the formatter from a specification and two extra hints about the initial buffer
74-
* capacity. A reference is held to this specification, but it will not be modified by the
75-
* actions of this class.
61+
* Construct the formatter from a specification, allocating a buffer internally for the result.
7662
*
7763
* @param spec parsed conversion specification
78-
* @param count of elements likely to be formatted
79-
* @param margin for elements formatted only once
8064
*/
81-
public FloatFormatter(Spec spec, int count, int margin) {
82-
/*
83-
* Rule of thumb used here: in e format w = (p-1) + len("+1.e+300") = p+7; in f format w = p
84-
* + len("1,000,000.") = p+10. If we're wrong, the result will have to grow. No big deal.
85-
*/
86-
this(spec, Math.max(spec.width + 1, count * (spec.precision + 10) + margin));
65+
public FloatFormatter(Spec spec) {
66+
this(new StringBuilder(size(spec)), spec);
67+
}
68+
69+
/**
70+
* Recommend a buffer size for a given specification, assuming one float is converted. This will
71+
* be a "right" answer for e and g-format, and for f-format with values up to 9,999,999.
72+
*
73+
* @param spec parsed conversion specification
74+
*/
75+
public static int size(Spec spec) {
76+
// Rule of thumb used here (no right answer):
77+
// in e format each float occupies: (p-1) + len("+1.e+300") = p+7;
78+
// in f format each float occupies: p + len("1,000,000.%") = p+11;
79+
// or an explicit (minimum) width may be given, with one overshoot possible.
80+
return Math.max(spec.width + 1, spec.getPrecision(6) + 11);
8781
}
8882

8983
/**

0 commit comments

Comments
 (0)