|
From: <pj...@us...> - 2011-03-21 05:57:46
|
Revision: 7257
http://jython.svn.sourceforge.net/jython/?rev=7257&view=rev
Author: pjenvey
Date: 2011-03-21 05:57:39 +0000 (Mon, 21 Mar 2011)
Log Message:
-----------
a somewhat complete PEP 3101 implementation (str.format)
refs #1718
from Dmitry Jemerov @ PyCon 2011 (thanks!)
Modified Paths:
--------------
trunk/jython/CoreExposed.includes
trunk/jython/src/org/python/core/PyInstance.java
trunk/jython/src/org/python/core/PyInteger.java
trunk/jython/src/org/python/core/PyLong.java
trunk/jython/src/org/python/core/PyObject.java
trunk/jython/src/org/python/core/PyString.java
trunk/jython/src/org/python/core/__builtin__.java
Added Paths:
-----------
trunk/jython/src/org/python/core/stringlib/
trunk/jython/src/org/python/core/stringlib/FieldNameIterator.java
trunk/jython/src/org/python/core/stringlib/InternalFormatSpec.java
trunk/jython/src/org/python/core/stringlib/InternalFormatSpecParser.java
trunk/jython/src/org/python/core/stringlib/MarkupIterator.java
trunk/jython/tests/java/org/python/core/StringFormatTest.java
Modified: trunk/jython/CoreExposed.includes
===================================================================
--- trunk/jython/CoreExposed.includes 2011-03-21 05:46:52 UTC (rev 7256)
+++ trunk/jython/CoreExposed.includes 2011-03-21 05:57:39 UTC (rev 7257)
@@ -44,6 +44,8 @@
org/python/core/PyType.class
org/python/core/PyUnicode.class
org/python/core/PyXRange.class
+org/python/core/stringlib/MarkupIterator.class
+org/python/core/stringlib/FieldNameIterator.class
org/python/modules/PyStruct.class
org/python/modules/PyTeeIterator.class
org/python/jsr223/PyScriptEngineScope.class
Modified: trunk/jython/src/org/python/core/PyInstance.java
===================================================================
--- trunk/jython/src/org/python/core/PyInstance.java 2011-03-21 05:46:52 UTC (rev 7256)
+++ trunk/jython/src/org/python/core/PyInstance.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -787,6 +787,19 @@
ret.getType().fastGetName()));
}
+ @Override
+ public PyObject __format__(PyObject format_spec) {
+ return instance___format__(format_spec);
+ }
+
+ @ExposedMethod
+ final PyObject instance___format__(PyObject format_spec) {
+ PyObject func = __findattr__("__format__");
+ if (func == null)
+ return super.__format__(format_spec);
+ return func.__call__(format_spec);
+ }
+
// Generated by make_binops.py
// Unary ops
Modified: trunk/jython/src/org/python/core/PyInteger.java
===================================================================
--- trunk/jython/src/org/python/core/PyInteger.java 2011-03-21 05:46:52 UTC (rev 7256)
+++ trunk/jython/src/org/python/core/PyInteger.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -7,6 +7,8 @@
import java.io.Serializable;
import java.math.BigInteger;
+import org.python.core.stringlib.InternalFormatSpec;
+import org.python.core.stringlib.InternalFormatSpecParser;
import org.python.expose.ExposedGet;
import org.python.expose.ExposedMethod;
import org.python.expose.ExposedNew;
@@ -912,6 +914,118 @@
}
@Override
+ public PyObject __format__(PyObject format_spec) {
+ return int___format__(format_spec);
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.int___format___doc)
+ final PyObject int___format__(PyObject format_spec) {
+ return formatImpl(getValue(), format_spec);
+ }
+
+ static PyObject formatImpl(Object value, PyObject format_spec) {
+ if (format_spec instanceof PyString) {
+ String result;
+ try {
+ String specString = ((PyString) format_spec).getString();
+ InternalFormatSpec spec = new InternalFormatSpecParser(specString).parse();
+ result = formatIntOrLong(value, spec);
+ } catch (IllegalArgumentException e) {
+ throw Py.ValueError(e.getMessage());
+ }
+ if (format_spec instanceof PyUnicode) {
+ return new PyUnicode(result);
+ }
+ return new PyString(result);
+ }
+ throw Py.TypeError("__format__ requires str or unicode");
+ }
+
+ /**
+ * Formats an integer or long number according to a PEP-3101 format specification.
+ *
+ * @param value Integer or BigInteger object specifying the value to format.
+ * @param spec parsed PEP-3101 format specification.
+ * @return result of the formatting.
+ */
+ public static String formatIntOrLong(Object value, InternalFormatSpec spec) {
+ if (spec.precision != -1) {
+ throw new IllegalArgumentException("Precision not allowed in integer format specifier");
+ }
+ int sign;
+ if (value instanceof Integer) {
+ int intValue = (Integer) value;
+ sign = intValue < 0 ? -1 : intValue == 0 ? 0 : 1;
+ }
+ else {
+ sign = ((BigInteger) value).signum();
+ }
+ String strValue;
+ if (spec.type == 'c') {
+ if (spec.sign != '\0') {
+ throw new IllegalArgumentException("Sign not allowed with " +
+ "integer format specifier 'c'");
+ }
+ if (value instanceof Integer) {
+ int intValue = (Integer) value;
+ if (intValue > 0xffff) {
+ throw new IllegalArgumentException("%c arg not in range(0x10000)");
+ }
+ strValue = Character.toString((char) intValue);
+ }
+ else {
+ BigInteger bigInt = (BigInteger) value;
+ if (bigInt.intValue() > 0xffff || bigInt.bitCount() > 16) {
+ throw new IllegalArgumentException("%c arg not in range(0x10000)");
+ }
+ strValue = Character.toString((char) bigInt.intValue());
+ }
+ } else {
+ int radix = 10;
+ if (spec.type == 'o') {
+ radix = 8;
+ } else if (spec.type == 'x' || spec.type == 'X') {
+ radix = 16;
+ } else if (spec.type == 'b') {
+ radix = 2;
+ }
+
+ // TODO locale-specific formatting for 'n'
+ if (value instanceof BigInteger) {
+ strValue = ((BigInteger) value).toString(radix);
+ }
+ else {
+ strValue = Integer.toString((Integer) value, radix);
+ }
+
+ if (spec.alternate) {
+ if (radix == 2)
+ strValue = "0b" + strValue;
+ else if (radix == 8)
+ strValue = "0o" + strValue;
+ else if (radix == 16)
+ strValue = "0x" + strValue;
+ }
+ if (spec.type == 'X') {
+ strValue = strValue.toUpperCase();
+ }
+
+ if (sign >= 0) {
+ if (spec.sign == '+') {
+ strValue = "+" + strValue;
+ } else if (spec.sign == ' ') {
+ strValue = " " + strValue;
+ }
+ }
+ }
+ if (spec.align == '=' && (sign < 0 || spec.sign == '+' || spec.sign == ' ')) {
+ char signChar = strValue.charAt(0);
+ return signChar + spec.pad(strValue.substring(1), '>', 1);
+ }
+ return spec.pad(strValue, '>', 0);
+ }
+
+ @Override
public boolean isIndex() {
return true;
}
Modified: trunk/jython/src/org/python/core/PyLong.java
===================================================================
--- trunk/jython/src/org/python/core/PyLong.java 2011-03-21 05:46:52 UTC (rev 7256)
+++ trunk/jython/src/org/python/core/PyLong.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -982,6 +982,16 @@
}
@Override
+ public PyObject __format__(PyObject format_spec) {
+ return long___format__(format_spec);
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.long___format___doc)
+ final PyObject long___format__(PyObject format_spec) {
+ return PyInteger.formatImpl(getValue(), format_spec);
+ }
+
+ @Override
public boolean isIndex() {
return true;
}
Modified: trunk/jython/src/org/python/core/PyObject.java
===================================================================
--- trunk/jython/src/org/python/core/PyObject.java 2011-03-21 05:46:52 UTC (rev 7256)
+++ trunk/jython/src/org/python/core/PyObject.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -1710,6 +1710,16 @@
return false;
}
+ public PyObject __format__(PyObject format_spec) {
+ return object___format__(format_spec);
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.object___format___doc)
+ final PyObject object___format__(PyObject format_spec) {
+ PyString str = __str__();
+ return str.__format__(format_spec);
+ }
+
/**
* Implements boolean not
*
Modified: trunk/jython/src/org/python/core/PyString.java
===================================================================
--- trunk/jython/src/org/python/core/PyString.java 2011-03-21 05:46:52 UTC (rev 7256)
+++ trunk/jython/src/org/python/core/PyString.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -3,6 +3,10 @@
import java.math.BigInteger;
+import org.python.core.stringlib.FieldNameIterator;
+import org.python.core.stringlib.InternalFormatSpec;
+import org.python.core.stringlib.InternalFormatSpecParser;
+import org.python.core.stringlib.MarkupIterator;
import org.python.core.util.ExtraMath;
import org.python.core.util.StringUtil;
import org.python.expose.ExposedMethod;
@@ -2510,6 +2514,155 @@
return codecs.decode(this, encoding, errors);
}
+ @ExposedMethod(doc = BuiltinDocs.str__formatter_parser_doc)
+ final PyObject str__formatter_parser() {
+ return new MarkupIterator(getString());
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.str__formatter_field_name_split_doc)
+ final PyObject str__formatter_field_name_split() {
+ FieldNameIterator iterator = new FieldNameIterator(getString());
+ Object headObj = iterator.head();
+ PyObject head = headObj instanceof Integer
+ ? new PyInteger((Integer) headObj)
+ : new PyString((String) headObj);
+ return new PyTuple(head, iterator);
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.str_format_doc)
+ final PyObject str_format(PyObject[] args, String[] keywords) {
+ try {
+ return new PyString(buildFormattedString(getString(), args, keywords));
+ } catch(IllegalArgumentException e) {
+ throw Py.ValueError(e.getMessage());
+ }
+ }
+
+ private String buildFormattedString(String value, PyObject[] args, String[] keywords) {
+ StringBuilder result = new StringBuilder();
+ MarkupIterator it = new MarkupIterator(value);
+ while (true) {
+ MarkupIterator.Chunk chunk = it.nextChunk();
+ if (chunk == null) {
+ break;
+ }
+ result.append(chunk.literalText);
+ if (chunk.fieldName.length() > 0) {
+ outputMarkup(result, chunk, args, keywords);
+ }
+ }
+ return result.toString();
+ }
+
+ private void outputMarkup(StringBuilder result, MarkupIterator.Chunk chunk,
+ PyObject[] args, String[] keywords) {
+ PyObject fieldObj = getFieldObject(chunk.fieldName, args, keywords);
+ if (fieldObj == null) {
+ return;
+ }
+ if ("r".equals(chunk.conversion)) {
+ fieldObj = fieldObj.__repr__();
+ }
+ else if ("s".equals(chunk.conversion)) {
+ fieldObj = fieldObj.__str__();
+ }
+ else if (chunk.conversion != null) {
+ throw Py.ValueError("Unknown conversion specifier " + chunk.conversion);
+ }
+ String formatSpec = chunk.formatSpec;
+ if (chunk.formatSpecNeedsExpanding) {
+ formatSpec = buildFormattedString(formatSpec, args, keywords);
+ }
+ renderField(fieldObj, formatSpec, result);
+ }
+
+ private PyObject getFieldObject(String fieldName, PyObject[] args, String[] keywords) {
+ FieldNameIterator iterator = new FieldNameIterator(fieldName);
+ Object head = iterator.head();
+ PyObject obj = null;
+ int positionalCount = args.length - keywords.length;
+ if (head instanceof Integer) {
+ int index = (Integer) head;
+ if (index >= positionalCount) {
+ throw Py.IndexError("tuple index out of range");
+ }
+ obj = args[index];
+ }
+ else {
+ for (int i = 0; i < keywords.length; i++) {
+ if (keywords[i].equals(head)) {
+ obj = args[positionalCount+i];
+ break;
+ }
+ }
+ if (obj == null) {
+ throw Py.KeyError((String) head);
+ }
+ }
+ if (obj != null) {
+ while (true) {
+ FieldNameIterator.Chunk chunk = iterator.nextChunk();
+ if (chunk == null) {
+ break;
+ }
+ if (chunk.is_attr) {
+ obj = obj.__getattr__((String) chunk.value);
+ }
+ else {
+ PyObject key = chunk.value instanceof String
+ ? new PyString((String) chunk.value)
+ : new PyInteger((Integer) chunk.value);
+ obj = obj.__getitem__(key);
+ }
+ if (obj == null) break;
+ }
+ }
+ return obj;
+ }
+
+ private void renderField(PyObject fieldObj, String formatSpec, StringBuilder result) {
+ PyString formatSpecStr = formatSpec == null ? Py.EmptyString : new PyString(formatSpec);
+ result.append(fieldObj.__format__(formatSpecStr).asString());
+ }
+
+ @Override
+ public PyObject __format__(PyObject format_spec) {
+ return str___format__(format_spec);
+ }
+
+ @ExposedMethod(doc = BuiltinDocs.str___format___doc)
+ final PyObject str___format__(PyObject format_spec) {
+ if (format_spec instanceof PyString) {
+ String result;
+ try {
+ String specString = ((PyString) format_spec).getString();
+ InternalFormatSpec spec = new InternalFormatSpecParser(specString).parse();
+ result = formatString(getString(), spec);
+ } catch (IllegalArgumentException e) {
+ throw Py.ValueError(e.getMessage());
+ }
+ if (format_spec instanceof PyUnicode) {
+ return new PyUnicode(result);
+ }
+ return new PyString(result);
+ }
+ throw Py.TypeError("__format__ requires str or unicode");
+ }
+
+ /**
+ * Internal implementation of str.__format__()
+ *
+ * @param text the text to format
+ * @param spec the PEP 3101 formatting specification
+ * @return the result of the formatting
+ */
+ public static String formatString(String text, InternalFormatSpec spec) {
+ if (spec.precision >= 0 && text.length() > spec.precision) {
+ text = text.substring(0, spec.precision);
+ }
+ return spec.pad(text, '<', 0);
+ }
+
/* arguments' conversion helper */
@Override
Modified: trunk/jython/src/org/python/core/__builtin__.java
===================================================================
--- trunk/jython/src/org/python/core/__builtin__.java 2011-03-21 05:46:52 UTC (rev 7256)
+++ trunk/jython/src/org/python/core/__builtin__.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -358,6 +358,7 @@
dict.__setitem__("sorted", new SortedFunction());
dict.__setitem__("all", new AllFunction());
dict.__setitem__("any", new AnyFunction());
+ dict.__setitem__("format", new FormatFunction());
}
public static PyObject abs(PyObject o) {
@@ -1303,6 +1304,25 @@
}
}
+class FormatFunction extends PyBuiltinFunctionNarrow {
+ FormatFunction() {
+ super("format", 1, 2,
+ "format(value[, format_spec]) -> string\n\n" +
+ "Returns value.__format__(format_spec)\n" +
+ "format_spec defaults to \"\"");
+ }
+
+ @Override
+ public PyObject __call__(PyObject arg1) {
+ return __call__(arg1, new PyString(""));
+ }
+
+ @Override
+ public PyObject __call__(PyObject arg1, PyObject arg2) {
+ return arg1.__format__(arg2);
+ }
+}
+
class MaxFunction extends PyBuiltinFunction {
MaxFunction() {
super("max",
Added: trunk/jython/src/org/python/core/stringlib/FieldNameIterator.java
===================================================================
--- trunk/jython/src/org/python/core/stringlib/FieldNameIterator.java (rev 0)
+++ trunk/jython/src/org/python/core/stringlib/FieldNameIterator.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -0,0 +1,117 @@
+package org.python.core.stringlib;
+
+import org.python.core.*;
+import org.python.expose.ExposedMethod;
+import org.python.expose.ExposedType;
+
+/**
+ * Provides an implementation of str._formatter_field_name_split()
+ */
+@ExposedType(name = "fieldnameiterator", base = PyObject.class, isBaseType = false)
+public class FieldNameIterator extends PyObject {
+ private String markup;
+ private Object head;
+ private int index;
+
+ public FieldNameIterator(String markup) {
+ this.markup = markup;
+ this.index = nextDotOrBracket(markup);
+ String headStr = markup.substring(0, index);
+ try {
+ this.head = Integer.parseInt(headStr);
+ } catch (NumberFormatException e) {
+ this.head = headStr;
+ }
+ }
+
+ @Override
+ public PyObject __iter__() {
+ return this;
+ }
+
+ @ExposedMethod
+ public PyObject fieldnameiterator___iter__() {
+ return this;
+ }
+
+ public PyObject __iternext__() {
+ Chunk chunk = nextChunk();
+ if (chunk == null) {
+ return null;
+ }
+ PyObject[] elements = new PyObject[2];
+ elements [0] = new PyBoolean(chunk.is_attr);
+ if (chunk.value instanceof Integer) {
+ elements [1] = new PyInteger((Integer) chunk.value);
+ }
+ else {
+ elements [1] = new PyString((String) chunk.value);
+ }
+ return new PyTuple(elements);
+ }
+
+ @ExposedMethod
+ public PyObject fieldnameiterator___iternext__() {
+ return __iternext__();
+ }
+
+ private int nextDotOrBracket(String markup) {
+ int dotPos = markup.indexOf('.', index);
+ if (dotPos < 0) dotPos = markup.length();
+ int bracketPos = markup.indexOf('[', index);
+ if (bracketPos < 0) bracketPos = markup.length();
+ return Math.min(dotPos, bracketPos);
+ }
+
+ public Object head() {
+ return head;
+ }
+
+ public Chunk nextChunk() {
+ if (index == markup.length()) {
+ return null;
+ }
+ Chunk chunk = new Chunk();
+ if (markup.charAt(index) == '[') {
+ parseItemChunk(chunk);
+ }
+ else if (markup.charAt(index) == '.') {
+ parseAttrChunk(chunk);
+ }
+ return chunk;
+ }
+
+ private void parseItemChunk(Chunk chunk) {
+ chunk.is_attr = false;
+ int endBracket = markup.indexOf(']', index+1);
+ if (endBracket < 0) {
+ throw new IllegalArgumentException("Missing ']' in format string");
+ }
+ String itemValue = markup.substring(index+1, endBracket);
+ if (itemValue.length() == 0) {
+ throw new IllegalArgumentException("Empty attribute in format string");
+ }
+ try {
+ chunk.value = Integer.parseInt(itemValue);
+ } catch (NumberFormatException e) {
+ chunk.value = itemValue;
+ }
+ index = endBracket+1;
+ }
+
+ private void parseAttrChunk(Chunk chunk) {
+ index++; // skip dot
+ chunk.is_attr = true;
+ int pos = nextDotOrBracket(markup);
+ if (pos == index) {
+ throw new IllegalArgumentException("Empty attribute in format string");
+ }
+ chunk.value = markup.substring(index, pos);
+ index = pos;
+ }
+
+ public static class Chunk {
+ public boolean is_attr;
+ public Object value; // Integer or String
+ }
+}
Added: trunk/jython/src/org/python/core/stringlib/InternalFormatSpec.java
===================================================================
--- trunk/jython/src/org/python/core/stringlib/InternalFormatSpec.java (rev 0)
+++ trunk/jython/src/org/python/core/stringlib/InternalFormatSpec.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -0,0 +1,42 @@
+package org.python.core.stringlib;
+
+/**
+ * Parsed PEP-3101 format specification of a single field.
+ */
+public final class InternalFormatSpec {
+ public char fill_char;
+ public char align;
+ public boolean alternate;
+ public char sign;
+ public int width = -1;
+ public int precision = -1;
+ public char type;
+
+ public String pad(String value, char defaultAlign, int leaveWidth) {
+ int remaining = width - value.length() - leaveWidth;
+ if (remaining <= 0) {
+ return value;
+ }
+ StringBuilder result = new StringBuilder();
+ int leading = remaining;
+ int useAlign = align;
+ if (useAlign == 0) {
+ useAlign = defaultAlign;
+ }
+ if (useAlign == '^') {
+ leading = remaining/2;
+ }
+ else if (useAlign == '<') {
+ leading = 0;
+ }
+ char fill = fill_char != 0 ? fill_char : ' ';
+ for (int i = 0; i < leading; i++) {
+ result.append(fill);
+ }
+ result.append(value);
+ for (int i = 0; i < remaining-leading; i++) {
+ result.append(fill);
+ }
+ return result.toString();
+ }
+}
Added: trunk/jython/src/org/python/core/stringlib/InternalFormatSpecParser.java
===================================================================
--- trunk/jython/src/org/python/core/stringlib/InternalFormatSpecParser.java (rev 0)
+++ trunk/jython/src/org/python/core/stringlib/InternalFormatSpecParser.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -0,0 +1,83 @@
+package org.python.core.stringlib;
+
+/**
+ * Parser for PEP-3101 field format specifications.
+ */
+public class InternalFormatSpecParser {
+ private String spec;
+ private int index;
+
+ public InternalFormatSpecParser(String spec) {
+ this.spec = spec;
+ this.index = 0;
+ }
+
+ private static boolean isAlign(char c) {
+ switch(c) {
+ case '<':
+ case '>':
+ case '=':
+ case '^':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public InternalFormatSpec parse() {
+ InternalFormatSpec result = new InternalFormatSpec();
+ if (spec.length() >= 1 && isAlign(spec.charAt(0))) {
+ result.align = spec.charAt(index);
+ index++;
+ }
+ else if (spec.length() >= 2 && isAlign(spec.charAt(1))) {
+ result.fill_char = spec.charAt(0);
+ result.align = spec.charAt(1);
+ index += 2;
+ }
+ if (isAt("+- ")) {
+ result.sign = spec.charAt(index);
+ index++;
+ }
+ if (isAt("#")) {
+ result.alternate = true;
+ index++;
+ }
+ if (isAt("0")) {
+ result.align = '=';
+ result.fill_char = '0';
+ index++;
+ }
+ result.width = getInteger();
+ if (isAt(".")) {
+ index++;
+ result.precision = getInteger();
+ if (result.precision == -1) {
+ throw new IllegalArgumentException("Format specifier missing precision");
+ }
+ }
+ if (index < spec.length()) {
+ result.type = spec.charAt(index);
+ if (index + 1 != spec.length()) {
+ throw new IllegalArgumentException("Invalid conversion specification");
+ }
+ }
+ return result;
+ }
+
+ private int getInteger() {
+ int value = 0;
+ boolean empty = true;
+ while (index < spec.length() && spec.charAt(index) >= '0' && spec.charAt(index) <= '9') {
+ value = value * 10 + spec.charAt(index) - '0';
+ index++;
+ empty = false;
+ }
+ if (empty) return -1;
+ return value;
+ }
+
+ private boolean isAt(String chars) {
+ return index < spec.length() && chars.indexOf(spec.charAt(index)) >= 0;
+ }
+}
Added: trunk/jython/src/org/python/core/stringlib/MarkupIterator.java
===================================================================
--- trunk/jython/src/org/python/core/stringlib/MarkupIterator.java (rev 0)
+++ trunk/jython/src/org/python/core/stringlib/MarkupIterator.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -0,0 +1,159 @@
+package org.python.core.stringlib;
+
+import org.python.core.*;
+import org.python.expose.ExposedMethod;
+import org.python.expose.ExposedType;
+
+/**
+ * Provides an implementation of str._formatter_parser()
+ */
+@ExposedType(name = "formatteriterator", base = PyObject.class, isBaseType = false)
+public class MarkupIterator extends PyObject {
+ private final String markup;
+ private int index;
+
+ public MarkupIterator(String markup) {
+ this.markup = markup;
+ }
+
+ @Override
+ public PyObject __iter__() {
+ return this;
+ }
+
+ @ExposedMethod
+ public PyObject formatteriterator___iter__() {
+ return this;
+ }
+
+ public PyObject __iternext__() {
+ Chunk chunk;
+ try {
+ chunk = nextChunk();
+ } catch (IllegalArgumentException e) {
+ throw Py.ValueError(e.getMessage());
+ }
+ if (chunk == null) {
+ return null;
+ }
+ PyObject[] elements = new PyObject[4];
+ elements[0] = new PyString(chunk.literalText);
+ elements[1] = new PyString(chunk.fieldName);
+ if (chunk.fieldName.length() > 0) {
+ elements[2] = chunk.formatSpec == null ? Py.EmptyString : new PyString(chunk.formatSpec);
+ }
+ else {
+ elements[2] = Py.None;
+ }
+ elements[3] = chunk.conversion == null ? Py.None : new PyString(chunk.conversion);
+ return new PyTuple(elements);
+ }
+
+ @ExposedMethod
+ public PyObject formatteriterator___iternext__() {
+ return __iternext__();
+ }
+
+ public Chunk nextChunk() {
+ if (index == markup.length()) {
+ return null;
+ }
+ Chunk result = new Chunk();
+ int pos = index;
+ while(true) {
+ pos = indexOfFirst(markup, pos, '{', '}');
+ if (pos >= 0 && pos < markup.length()-1 &&
+ markup.charAt(pos+1) == markup.charAt(pos)) {
+ pos += 2; // skip escaped bracket
+ }
+ else if (pos >= 0 && markup.charAt(pos) == '}') {
+ throw new IllegalArgumentException("Single '}' encountered in format string");
+ }
+ else {
+ break;
+ }
+ }
+ if (pos < 0) {
+ result.literalText = unescapeBraces(markup.substring(index));
+ result.fieldName = "";
+ index = markup.length();
+ }
+ else {
+ result.literalText = unescapeBraces(markup.substring(index, pos));
+ pos++;
+ int fieldStart = pos;
+ int count = 1;
+ while (pos < markup.length()) {
+ if (markup.charAt(pos) == '{') {
+ count++;
+ result.formatSpecNeedsExpanding = true;
+ }
+ else if (markup.charAt(pos) == '}') {
+ count--;
+ if (count == 0) {
+ parseField(result, markup.substring(fieldStart, pos));
+ pos++;
+ break;
+ }
+ }
+ pos++;
+ }
+ if (count > 0)
+ throw new IllegalArgumentException("Single '{' encountered in format string");
+ index = pos;
+ }
+ return result;
+ }
+
+ private String unescapeBraces(String substring) {
+ return substring.replace("{{", "{").replace("}}", "}");
+ }
+
+ private void parseField(Chunk result, String fieldMarkup) {
+ int pos = indexOfFirst(fieldMarkup, 0, '!', ':');
+ if (pos >= 0) {
+ result.fieldName = fieldMarkup.substring(0, pos);
+ if (fieldMarkup.charAt(pos) == '!') {
+ if (pos == fieldMarkup.length() - 1) {
+ throw new IllegalArgumentException("end of format while " +
+ "looking for conversion specifier");
+ }
+ result.conversion = fieldMarkup.substring(pos + 1, pos + 2);
+ pos += 2;
+ if (pos < fieldMarkup.length()) {
+ if (fieldMarkup.charAt(pos) != ':') {
+ throw new IllegalArgumentException("expected ':' " +
+ "after conversion specifier");
+ }
+ result.formatSpec = fieldMarkup.substring(pos+1);
+ }
+ }
+ else {
+ result.formatSpec = fieldMarkup.substring(pos+1);
+ }
+ }
+ else {
+ result.fieldName = fieldMarkup;
+ }
+ }
+
+ private int indexOfFirst(String s, int start, char c1, char c2) {
+ int i1 = s.indexOf(c1, start);
+ int i2 = s.indexOf(c2, start);
+ if (i1 == -1) {
+ return i2;
+ }
+ if (i2 == -1) {
+ return i1;
+ }
+ return Math.min(i1, i2);
+ }
+
+ public static final class Chunk {
+ public String literalText;
+ public String fieldName;
+ public String formatSpec;
+ public String conversion;
+ public boolean formatSpecNeedsExpanding;
+ }
+}
Added: trunk/jython/tests/java/org/python/core/StringFormatTest.java
===================================================================
--- trunk/jython/tests/java/org/python/core/StringFormatTest.java (rev 0)
+++ trunk/jython/tests/java/org/python/core/StringFormatTest.java 2011-03-21 05:57:39 UTC (rev 7257)
@@ -0,0 +1,216 @@
+package org.python.core;
+
+import junit.framework.TestCase;
+import org.python.core.stringlib.FieldNameIterator;
+import org.python.core.stringlib.InternalFormatSpec;
+import org.python.core.stringlib.InternalFormatSpecParser;
+import org.python.core.stringlib.MarkupIterator;
+
+/**
+ * Tests for internal bits and pieces of string.format implementation.
+ */
+public class StringFormatTest extends TestCase {
+ public void testInternalFormatSpec() {
+ InternalFormatSpec spec = new InternalFormatSpecParser("x").parse();
+ assertEquals('x', spec.type);
+
+ spec = new InternalFormatSpecParser("<x").parse();
+ assertEquals('<', spec.align);
+ assertEquals('x', spec.type);
+
+ spec = new InternalFormatSpecParser("~<x").parse();
+ assertEquals('~', spec.fill_char);
+ assertEquals('<', spec.align);
+ assertEquals('x', spec.type);
+
+ spec = new InternalFormatSpecParser("+x").parse();
+ assertEquals('+', spec.sign);
+ assertEquals('x', spec.type);
+
+ spec = new InternalFormatSpecParser("#x").parse();
+ assertEquals(true, spec.alternate);
+
+ spec = new InternalFormatSpecParser("0x").parse();
+ assertEquals('=', spec.align);
+ assertEquals('0', spec.fill_char);
+
+ spec = new InternalFormatSpecParser("123x").parse();
+ assertEquals(123, spec.width);
+
+ spec = new InternalFormatSpecParser("123.456x").parse();
+ assertEquals(123, spec.width);
+ assertEquals(456, spec.precision);
+
+ assertParseError("123.x", "Format specifier missing precision");
+
+ assertParseError("123xx", "Invalid conversion specification");
+
+ spec = new InternalFormatSpecParser("").parse();
+ assertEquals(0, spec.type);
+ }
+
+ private void assertParseError(String spec, String expected) {
+ String error = null;
+ try {
+ new InternalFormatSpecParser(spec).parse();
+ } catch (IllegalArgumentException e) {
+ error = e.getMessage();
+ }
+ assertEquals(expected, error);
+ }
+
+ public void testFormatIntOrLong() {
+ InternalFormatSpec spec = new InternalFormatSpec();
+ spec.type = 'd';
+ assertEquals("123", PyInteger.formatIntOrLong(123, spec));
+ spec.type = 'o';
+ assertEquals("173", PyInteger.formatIntOrLong(123, spec));
+ spec.type = 'x';
+ assertEquals("7b", PyInteger.formatIntOrLong(123, spec));
+ spec.type = 'X';
+ assertEquals("7B", PyInteger.formatIntOrLong(123, spec));
+ spec.type = 'b';
+ assertEquals("1111011", PyInteger.formatIntOrLong(123, spec));
+
+ spec.alternate = true;
+ spec.type = 'o';
+ assertEquals("0o173", PyInteger.formatIntOrLong(123, spec));
+ spec.type = 'X';
+ assertEquals("0X7B", PyInteger.formatIntOrLong(123, spec));
+ spec.alternate = false;
+
+ spec.type = 'c';
+ assertEquals("{", PyInteger.formatIntOrLong(123, spec));
+
+ spec.type = 'd';
+ spec.sign = '+';
+ assertEquals("+123", PyInteger.formatIntOrLong(123, spec));
+ spec.sign = ' ';
+ assertEquals(" 123", PyInteger.formatIntOrLong(123, spec));
+
+ spec.sign = 0;
+ spec.width = 5;
+ assertEquals(" 123", PyInteger.formatIntOrLong(123, spec));
+
+ spec.align = '^';
+ spec.width = 6;
+ assertEquals(" 123 ", PyInteger.formatIntOrLong(123, spec));
+
+ spec.align = '<';
+ spec.width = 5;
+ spec.fill_char = '~';
+ assertEquals("123~~", PyInteger.formatIntOrLong(123, spec));
+
+ spec.align = '=';
+ spec.width = 6;
+ spec.fill_char = '0';
+ spec.sign = '+';
+ assertEquals("+00123", PyInteger.formatIntOrLong(123, spec));
+
+ spec.precision = 1;
+ assertFormatError(123, spec, "Precision not allowed in integer format specifier");
+
+ spec.precision = -1;
+ spec.sign = '+';
+ spec.type = 'c';
+ assertFormatError(123, spec, "Sign not allowed with integer format specifier 'c'");
+
+ spec.sign = 0;
+ assertFormatError(0x11111, spec, "%c arg not in range(0x10000)");
+ }
+
+ public void testFormatString() {
+ InternalFormatSpec spec = new InternalFormatSpec();
+ assertEquals("abc", PyString.formatString("abc", spec));
+
+ spec.precision = 3;
+ assertEquals("abc", PyString.formatString("abcdef", spec));
+
+ spec.precision = -1;
+ spec.width = 6;
+ assertEquals("abc ", PyString.formatString("abc", spec));
+ }
+
+ private void assertFormatError(int value, InternalFormatSpec spec, String expected) {
+ String error = null;
+ try {
+ PyInteger.formatIntOrLong(value, spec);
+ } catch (IllegalArgumentException e) {
+ error = e.getMessage();
+ }
+ assertEquals(expected, error);
+ }
+
+ public void testMarkupIterator() {
+ MarkupIterator iterator = new MarkupIterator("abc");
+ assertEquals("abc", iterator.nextChunk().literalText);
+ assertNull(iterator.nextChunk());
+
+ iterator = new MarkupIterator("First, thou shalt count to {0}");
+ MarkupIterator.Chunk chunk = iterator.nextChunk();
+ assertEquals("First, thou shalt count to ", chunk.literalText);
+ assertEquals("0", chunk.fieldName);
+ assertNull(iterator.nextChunk());
+
+ iterator = new MarkupIterator("Weight in tons {0.weight!r:s}");
+ chunk = iterator.nextChunk();
+ assertEquals("Weight in tons ", chunk.literalText);
+ assertEquals("0.weight", chunk.fieldName);
+ assertEquals("r", chunk.conversion);
+ assertEquals("s", chunk.formatSpec);
+
+ chunk = new MarkupIterator("{{").nextChunk();
+ assertEquals("{", chunk.literalText);
+
+ chunk = new MarkupIterator("}}").nextChunk();
+ assertEquals("}", chunk.literalText);
+
+ chunk = new MarkupIterator("{{}}").nextChunk();
+ assertEquals("{}", chunk.literalText);
+
+ chunk = new MarkupIterator("{0:.{1}}").nextChunk();
+ assertEquals("0", chunk.fieldName);
+ assertEquals(".{1}", chunk.formatSpec);
+ assertTrue(chunk.formatSpecNeedsExpanding);
+
+ assertMarkupError("{!}", "end of format while looking for conversion specifier");
+ assertMarkupError("{!rrrr}", "expected ':' after conversion specifier");
+ assertMarkupError("{", "Single '{' encountered in format string");
+ assertMarkupError("}", "Single '}' encountered in format string");
+ }
+
+ private void assertMarkupError(String markup, String expected) {
+ MarkupIterator iterator = new MarkupIterator(markup);
+ String error = null;
+ try {
+ iterator.nextChunk();
+ } catch (IllegalArgumentException e) {
+ error = e.getMessage();
+ }
+ assertEquals(expected, error);
+ }
+
+ public void testFieldNameIterator() {
+ FieldNameIterator it = new FieldNameIterator("abc");
+ assertEquals("abc", it.head());
+ assertNull(it.nextChunk());
+
+ it = new FieldNameIterator("3");
+ assertEquals(3, it.head());
+ assertNull(it.nextChunk());
+
+ it = new FieldNameIterator("abc[0]");
+ assertEquals("abc", it.head());
+ FieldNameIterator.Chunk chunk = it.nextChunk();
+ assertEquals(0, chunk.value);
+ assertFalse(chunk.is_attr);
+ assertNull(it.nextChunk());
+
+ it = new FieldNameIterator("abc.def");
+ assertEquals("abc", it.head());
+ chunk = it.nextChunk();
+ assertEquals("def", chunk.value);
+ assertTrue(chunk.is_attr);
+ assertNull(it.nextChunk());
+ }
+}
This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.
|