/**
* Convert %-format strings to {}-format.
*
* @author André Berg
* @contact http://github.com/andreberg
*/
package org.python.pydev.editor.correctionassist.heuristics;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.python.pydev.core.docutils.StringUtils;
import org.eclipse.core.runtime.Assert;
/**
* The <tt>PercentToBraceConverter</tt> class deals with converting
* traditional Python format strings (also known as <i>percent notation</i> or
* <i>string interpolation syntax</i>) into the more recently advocated
* template format mini-language.
*
* <p>Usage<p>
*
* <tt>String strToConvert = "'Hi, my name is %s' % name";</tt><br>
* <tt>PercentToBraceConverter ptbc = new PercentToBraceConverter(strToConvert);</tt><br>
* <tt>String convertedResult = ptbc.convert();</tt>
*
* <p><tt>convertedResult</tt> would now be <tt>'Hi, my name is {0!s}'.format(name)</tt></p>
*
* <p>Caveats</p>
*
* <p><b>PercentToBraceConverter is not synchronized.</b></p>
*
* <p>A <tt>PercentToBraceConverter</tt> instance also can't be reused or reset.
* If you want to convert another format string, you need to create a new instance.</p>
*
* <p>If either the <i>format string contents</i> or the <i>interpolation values</i>
* span multiple lines, <tt>PercentToBraceConverter</tt> will currently (falsely) process
* them as being a single line. It can, however, process multiple lines containing multiple
* format strings where each format string is on one line.</p>
*
* @version 0.7
* @author André Berg
*/
public final class PercentToBraceConverter {
private int argIndex;
private String initialSourceString;
private String matchedFormatString;
private String head;
private String tail;
private boolean skipFormatCallReplacement;
private int length;
private static final boolean DEBUG = false;
// for the pattern below <num>: gives the matched group number
/**
* <p>Pattern for matching a complete Python format string.<br>
* Matches i.e.: <code>"value: %0.2f" % price</code>, etc.</p>
*
* <p>Groups:</p>
*
* <ol>
* <li>complete format string (e.g. <code>"value: %0.2f"</code>)</li>
* <li>string character (e.g. <code>",'</code>)</li>
* <li>interpolant token (e.g. <code> % </code>)</li>
* <li>interpolation values (e.g. <code>price</code>)</li>
* </ol>
*/
private static final Pattern FMTSTR_PATTERN = Pattern.compile(
"(" + // 1: format string
"(?:r|u|ru|ur)?" + // literal specifier (optional)
"([\"']{1}|['\"]{3})" + // 2: string character " ', etc.
"(?!\\2)" + // make sure we don't match ''' or """ as '.' and "."
".+?" + // format string contents
"(?:\\2)" + // 2: string character backref
")" + //
"(\\s*?%\\s*)" + // 3: interpolant token (%)
"(.+)$", // 4: interpolation values
Pattern.COMMENTS
);
/**
* Initialize a new <tt>PercentToBraceConverter</tt> instance.
* @param formatStringToConvert - the <i>percent syntax</i> format
* string to convert.
* @throws IllegalArgumentException if <tt>formatStringToConvert</tt>
* is null.
* @pre formatStringToConvert != null
*/
public PercentToBraceConverter(final String formatStringToConvert) {
if (null == formatStringToConvert) {
throw new IllegalArgumentException("formatStringToConvert can't be null!");
}
initialSourceString = formatStringToConvert;
matchedFormatString = "";
head = "";
tail = "";
length = 0;
argIndex = 0;
}
/**
* Process the format string that the instance was initialized with.
* @return the converted format string, including a replaced interpolant
* token if applicable (i.e. <tt>"…" % (…)</tt> replaced with a <tt>format()</tt>
* call: <tt>"…".format(…)</tt>).
* @pre argIndex != 0 && length == 0
* @post argIndex == 0 && length != 0
*/
public String convert() {
if (StringUtils.isEmpty(initialSourceString)) {
return initialSourceString;
}
final String input = initialSourceString;
final String[] lines = input.split("\\r?\\n");
final List<String> results = new ArrayList<String>();
// FIXME: might need to get the appropriate line delimiter from Workbench prefs store.
// see org.eclipse.core.runtime.Platform.PREF_LINE_SEPARATOR
final String sep = System.getProperty("line.separator");
for (String line : lines) {
String result = "";
// needs to be stored back because of head and tail assessment
initialSourceString = line;
final Matcher formatStringMatcher = FMTSTR_PATTERN.matcher(line);
final Matcher tokenMatcher = PercentConversion.getTokenPattern().matcher(new String(line));
boolean isFormatString = false;
if (formatStringMatcher.find()) {
if (4 == formatStringMatcher.groupCount()) {
isFormatString = true;
matchedFormatString = formatStringMatcher.group(0);
if (DEBUG) {
System.out.printf(
"------" + sep +
"Match: ‘%s‘" + sep +
"------" + sep, matchedFormatString);
final String fmtString = formatStringMatcher.group(1);
final String strChar = formatStringMatcher.group(2);
final String interpToken = formatStringMatcher.group(3);
final String interpValues = formatStringMatcher.group(4);
System.out.printf(sep +
" Format String: ‘%s‘" + sep +
" String Character: ‘%s‘" + sep +
" Interpolant Token: ‘%s‘" + sep +
" Interpolant Values: ‘%s‘" + sep,
fmtString, strChar, interpToken, interpValues);
}
storeHeadAndTail();
if (!skipFormatCallReplacement) {
String replaced = replaceInterpolantTokenWithFormatCall(formatStringMatcher);
if (!matchedFormatString.equals(replaced)) {
updateStrings(replaced);
}
}
} else {
isFormatString = false;
}
}
if (isFormatString) {
final List<PercentConversion> conversions = new ArrayList<PercentConversion>();
PercentConversion conv = null;
while(tokenMatcher.find()) {
if (tokenMatcher.groupCount() >= 1) {
if (DEBUG) {
System.out.printf(sep +
"------" + sep +
"Match: ‘%s‘" + sep +
"------" + sep, tokenMatcher.group(0));
final String key = tokenMatcher.group(1);
final String flags = tokenMatcher.group(2);
final String width = tokenMatcher.group(3);
final String precision = tokenMatcher.group(4);
final String length = tokenMatcher.group(5);
final String conversion = tokenMatcher.group(6);
System.out.printf(sep +
" Mapping Key: ‘%s‘" + sep +
" Flags: ‘%s‘" + sep +
" Width ‘%s‘" + sep +
" Precision: ‘%s‘" + sep +
" Length: ‘%s‘" + sep +
" Conversion: ‘%s‘" + sep,
key, flags, width, precision, length, conversion);
}
conv = new PercentConversion(this, tokenMatcher.toMatchResult());
//System.out.println("Conversion.toString = " + conv.toString());
conversions.add(conv);
}
}
final ListIterator<PercentConversion> li = conversions.listIterator(conversions.size());
String converted = null;
while(li.hasPrevious()) {
conv = li.previous();
int[] span = conv.getSpan();
converted = conv.toBrace();
if (DEBUG) { // $codepro.audit.disable constantCondition, constantConditionalExpression
System.out.println(sep +
"Converted: " + converted);
System.out.format( "Span: [%d:%d]" + sep, span[0], span[1]);
}
result = insertIntoResult(converted, span);
updateStrings(result);
}
// check post-condition
Assert.isTrue(argIndex <= 0, "W: argIndex shouldn't be greater than zero when all tokens are consumed, but is " + argIndex);
} else {
result = initialSourceString;
}
results.add(result);
}
String convertedString = null;
if (1 == results.size()) {
// single line - most common case
convertedString = results.get(0);
} else if (results.size() > 1) {
// multiple lines
convertedString = com.aptana.shared_core.string.StringUtils.join(sep, results.toArray());
} else {
// this should never happen
Assert.isTrue(false, "E: there must always be one result even if "+
"the source string is not a valid percent format string.");
}
return convertedString;
}
/**
* Two <tt>PercentToBraceConverter</tt> instances are equal if
* their <tt>initialSourceString</tt> and <tt>skipFormatCallReplacement</tt>
* fields are equal.
*/
@Override
public boolean equals(Object obj) {
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof PercentToBraceConverter)) {
return false;
}
PercentToBraceConverter other = (PercentToBraceConverter) obj;
if (!initialSourceString.equals(other.initialSourceString)) {
return false;
}
if (skipFormatCallReplacement != other.skipFormatCallReplacement) {
return false;
}
return true;
}
/**
* Get the final length.
* @return length of the processed and converted string
*/
public int getLength() {
return length;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return (1 + initialSourceString.hashCode()) << (skipFormatCallReplacement ? 1 : 0);
}
/**
* Return status of the <tt>.format(…)</tt> call replacement.
*/
public boolean isSkippingFormatCallReplacement() {
return skipFormatCallReplacement;
}
/**
* <p>Pass <tt>true</tt> to disable support for replacement of the interpolant
* term with a <tt>.format(…)</tt> call.</p>
*
* <p>Technically there should be no need to do this because Python will respond
* to a converted format string that attempts to use an interpolant term instead of a
* <tt>.format(…)</tt> call with a <tt>TypeError: not all arguments converted during
* string formatting</tt>.</p>
*
* <p>Only use this if you have tricky input data where you need to perform the
* replacement yourself.</p>
*/
public void setSkipFormatCallReplacement(boolean skipFormatCallReplacement) {
this.skipFormatCallReplacement = skipFormatCallReplacement;
}
/**
* <p>Returns <tt>true</tt> if <tt>aString</tt> is or contains a Python
* format string in <i>interpolation syntax</i>.</p>
*
* <p>Used as a means to determine up-front if a conversion is necessary.</p>
*
* @param aString - the string to test
* @param splitLines - if true, split the string using regex <tt>\r?\n</tt> and
* return true if one line contains a format string. Otherwise, the
* {@link #FMTSTR_PATTERN} will be matched against the input string,
* without special behavior if it is spanning multiple lines.
* @return <tt>true</tt>/<tt>false</tt>
*/
public static boolean isValidPercentFormatString(final String aString, final boolean splitLines) {
boolean result = false;
Matcher matcher = null;
if (true == splitLines) {
final String[] lines = aString.split("\\r?\\n");
for (String line : lines) {
matcher = PercentToBraceConverter.getFormatStringPattern().matcher(line);
if (matcher.find()) {
result = true;
break;
}
}
} else {
matcher = PercentToBraceConverter.getFormatStringPattern().matcher(aString);
if (matcher.find()) {
result = true;
}
}
return result;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
final String description =
MessageFormat.format("<{0}@0x{1} | source={2} match={3} argIndex={4} head={5} tail={6}>",
this.getClass().getSimpleName(), Integer.toHexString(this.hashCode()),
initialSourceString, matchedFormatString, argIndex, head, tail);
return description;
}
/**
* @return the Pattern used to match Python format strings.<br>
* @see {@link #FMTSTR_PATTERN},
* @see {@link #extractFormatStringGroups}
*/
private static Pattern getFormatStringPattern() {
return PercentToBraceConverter.FMTSTR_PATTERN;
}
/**
* Build a group dictionary from the matched groups. <br>
* Guaranteed to contain 4 keys:<br>
* <ol>
* <li><tt>FormatString</tt>: the complete format string part</li>
* <li><tt>StringCharacter</tt>: the literal delimiter used (e.g. <tt>",',''',"""</tt></li>
* <li><tt>InterpolantToken</tt>: the percent sign, used to signal string interpolation</li>
* <li><tt>InterpolationValues</tt>: the values for filling in the specifier tokens in the format string.</li>
* </ol>
* @param matchResult
* @return the K, V mapped groups.
* @see {@link #FMTSTR_PATTERN}
*/
private static Map<String, String> extractFormatStringGroups(final MatchResult matchResult) {
// in a strict sense the assertion here is superfluous since the enclosing
// context in the caller already does a check if the group count is 4 and
// the execution branch in which this method is called will not be run if
// it isn't 4.
Assert.isLegal(4 == matchResult.groupCount(),
"E: Match result from FMTSTR_PATTERN is malformed. Group count must be 4.");
final Map<String, String> result = new HashMap<String,String>(4);
final String fmtString = matchResult.group(1);
final String strChar = matchResult.group(2);
final String interpToken = matchResult.group(3);
final String interpValues = matchResult.group(4);
result.put("FormatString", fmtString);
result.put("StringCharacter", strChar);
result.put("InterpolantToken", interpToken);
result.put("InterpolationValues", interpValues);
return result;
}
/**
* Partitions the initial source string using the passed span, as well as the
* offset between source and match string, and inserts in the middle the converted
* token.
*
* @param convertedToken the converted token substring to insert
* @param span int array with <tt>from,to</tt> values of the match span
* @return the string with the converted token inserted.
*/
private String insertIntoResult(String convertedToken, int[] span) {
String result = null;
String formatStringMatch = matchedFormatString;
if (!StringUtils.isEmpty(head) && formatStringMatch.indexOf(head) == -1) {
formatStringMatch = head + formatStringMatch;
}
if (!StringUtils.isEmpty(tail) && formatStringMatch.lastIndexOf(tail) == -1) {
formatStringMatch += tail;
}
final int from = span[1];
final int to = span[0];
final int len = formatStringMatch.length();
// Prepare a solution string from the whole match earlier
final String beginning = formatStringMatch.substring(0, to);
final String end = formatStringMatch.substring(from, len);
// Keep track of how many args we have consumed. but only
// decrement if the specifier actually is positional
if (Pattern.matches("\\{[0-9]{1,}.*?\\}", convertedToken)) {
argIndex--;
}
result = beginning + convertedToken + end;
length = result.length();
return result;
}
/**
* Get the next argument index. Used when determining the
* index of positional specifiers in the format string.
* <p>This method is not intended to be called by
* clients of <tt>PercentToBraceConverter</tt>. Instead,
* It is called by {@link #PercentConversion}.</p>
* <p>Note that the underlying implementation of this method
* may change in the future. This will be reflected then in
* the <tt>PercentConversion</tt>.</p>
* @return the argument index as string
*/
private String nextIndex() {
final String result = String.format("%d", argIndex);
argIndex++;
return result;
}
/**
* <p>Replace the interpolant term with a <tt>.format(…)</tt> call.</p>
*
* <p>E.g. replaces <tt>"Hi, my name is %s" % (person_name)</tt> with
* <tt>"Hi, my name is %s".format(person_name)</tt></p>
*
* @param formatStringMatcher the format string matcher for extracting
* the needed groups
* @return string with replaced substring or unmodified string
* @note if the interpolation values match group includes a previously identified tail
* it will be chopped off here as a fix. Inclusion of the tail may happen because
* the {@link #FMTSTR_PATTERN} can't be made unambiguous enough.
*/
private String replaceInterpolantTokenWithFormatCall(Matcher formatStringMatcher) {
String result = initialSourceString;
final Map<String, String> groups = extractFormatStringGroups(formatStringMatcher.toMatchResult());
final String fmtStr = groups.get("FormatString");
final String interpToken = groups.get("InterpolantToken");
final String interpValues = groups.get("InterpolationValues");
if (null != fmtStr &&
null != interpToken &&
null != interpValues) {
// When replacing the interpolation token with a .format() call
// one needs to be careful not to turn a tuple of interpolation values
// into a tuple of a tuple of interpolation values, because when
// there's multiple format specifiers in the format string, the
// tuple will be inserted for the first positional specifier and
// the second specifier will not have any values left to being
// interpolated with, resulting in a runtime error.
String s = "(";
String e = ")";
if ('(' == interpValues.charAt(0)) {
s = "";
e = "";
}
// fix falsely included tail in interpolation values
if (!StringUtils.isEmpty(tail)) {
int index = interpValues.indexOf(tail);
if (index > 0) { // yes, '>' not '>=', since substring(0,0) doesn't make sense
result = String.format("%s%s%s%s%s", fmtStr, ".format", s, interpValues.substring(0, index), e);
}
} else {
result = String.format("%s%s%s%s%s", fmtStr, ".format", s, interpValues, e);
}
}
return result;
}
/**
* <p>Store the left and right substring parts of the initial source string
* that are not matched by {@link #FMTSTR_PATTERN} so they
* can be re-attached at the end of processing.</p>
*
* <p>Normally we would expect to be only passed complete and compact format
* strings. Unfortunately it can't be assumed this will always be the case.</p>
*
* <p>E.g. we expect this:<br>
* <br>
* <tt>"Hello, my name is %s" % (personName)</tt>
* </p>
*
* <p>However we also need to handle cases like this:<br>
* <br>
* <tt>s = "Hello, my name is %s" % (personName)
* # this is a comment</tt>
* </p>
*
* <p>The latter case also has a head (the variable assignment) and a tail (the comment part).
* This method then chops off head and tail and stores them for later.</p>
*
*/
private void storeHeadAndTail() {
final String initial = initialSourceString;
final String matched = matchedFormatString;
if (!initial.equals(matched)) {
final int to = initial.indexOf(matched);
final int from = matched.length();
final int initlen = initial.length();
head = initial.substring(0, to);
tail = initial.substring(from + to, initlen);
if (StringUtils.isEmpty(tail)) {
// As a last effort, try to find the tail because the FMTSTR_PATTERN matcher may have
// failed in the face of ambiguity.
//
// Ambiguity may arise because the interpolant values group can be an identifier (e.g.
// a variable), a literal, or a tuple of identifiers or literals.
// Take for example the string literal, which itself can contain an arbitrary mix of
// brace characters, hash characters (which would otherwise indicate an end-of-line
// comment), or escaped versions thereof.
// The best way to combat this ambiguity I could find was to employ a statemachine
// variation, that works its way inwards from both sides, to the most probable location
// that marks the true end of the interpolation values group.
//
String valuesPart = StringUtils.findSubstring(matched, '%', true);
int lastBracePos = StringUtils.lastIndexOf(valuesPart, "\\)");
int lastDoubleQuotePos = StringUtils.lastIndexOf(valuesPart, "\\\"");
int lastSingleQuotePos = StringUtils.lastIndexOf(valuesPart, "'");
int hashPos = StringUtils.indexOf(valuesPart, '#', true);
int lastAlnumPos = StringUtils.lastIndexOf(valuesPart, "[a-zA-Z0-9_]");
if (hashPos == -1) {
// has no comment tail, set to max length
hashPos = from;
}
int[] positions = {hashPos, lastBracePos, lastDoubleQuotePos, lastSingleQuotePos, lastAlnumPos};
int min = lastBracePos;
int max = 0;
for (int i = 0; i < positions.length; i++) {
int cur = positions[i];
if (cur == -1) {
continue;
}
if (cur < hashPos) {
if (cur < min) {
min = cur;
}
if (cur > max) {
max = cur;
}
}
}
tail = valuesPart.substring(max+1);
}
} else {
head = "";
tail = "";
}
}
/**
* Update the strings with a new result.
* @param newResult the new value
*/
private void updateStrings(final String newResult) throws IllegalArgumentException {
Assert.isNotNull(newResult, "E: newResult can't be null!");
String processedNewResult = new String(newResult);
initialSourceString = processedNewResult;
matchedFormatString = processedNewResult;
length = processedNewResult.length();
}
/**
* <p>The purpose of the <tt>PercentConversion</tt> class is two-fold:</p>
*
* <ol>
* <li>it identifies and stores all the combined parts that make up
* <b>one</b> complete specifier token in a format string.</li>
* <li>
* it splits up and converts each complete specifier token into
* the brace notation used by the format mini-language, e.g. from
* <tt>%0.2f</tt> to <tt>{0:>0.2f}</tt>.
* </li>
* </ol>
*
* <p>It's only client is the outer class {@link #PercentToBraceConverter}.</p>
*
* @see {@link PercentToBraceConverter#convert()}.
* @author André Berg
* @version 0.5
*/
private static final class PercentConversion {
// for the pattern below <num>: gives the matched group number
/**
* <p>Pattern for matching a Python format specifier.<br>
* Matches, i.e.: <code>%2.2f</code>, <code>%(mapping)s</code>,
* <code>%s</code>, etc.</p>
*
* <p>Groups:</p>
*
* <ol>
* <li>mapping key, e.g. <code>mapping</code> (optional)</li>
* <li>conversion flags, e.g. <code>#,+,0</code> etc. (optional)</li>
* <li>minimum width, e.g. <code>*,2</code> (optional)</li>
* <li>precision, e.g. <code>.*,.2</code> (optional)</li>
* <li>length modifier, e.g. <code>h,l,L</code> (optional)</li>
* <li>conversion, e.g. <code>d,i,o,u,x,X,e,E,f,F,g,G,c,r,s,%</code></li>
* </ol>
*/
private static final Pattern TOKEN_PATTERN = Pattern.compile(
"(?<!%)%" + // specifier start
"(?:" + //
"\\(([^\\)]+)\\)" + // 1: mapping key (optional)
")?" + //
"([#+ -]{1,})?" + // 2: conversion flags (optional)
"(" + // 3: minimum width (optional)
"(?:\\*|(?:[0-9][0-9]*?))" + //
")?" + //
"(?:" + //
"\\.((?:\\*|(?:[0-9][0-9]*?)))?" + // 4: precision (optional)
")?" + //
"([hlL])?" + // 5: length modifier (optional)
"(?<!\\s)([diouxXeEfFgGcrs%])" // 6: conversion
);
private final int[] span;
private final String source;
private final String key;
private final String width;
private final String precision;
private final String flags;
private final String conversion;
/**
* <p>Create a new {@link #PercentConversion} instance.</p>
*
* <p>A <tt>PercentConversion</tt> instance is created from one
* particular specifier match result and is fixed after creation.</p>
*
* This is because for some format strings, it is expected that
* multiple <tt>PercentConversions</tt> will be needed to fully convert
* the format string and each <tt>PercentConversion</tt> should represent
* one specifier and one specifier only in the format string.
*
* @param aConverter - the enclosing {@link #PercentToBraceConverter} instance
* @param aMatch - a specific {@link java.util.regex#MatchResult MatchResult} that holds
* information about the matched specifier token.
* @throws IllegalArgumentException
* if <tt>aConverter</tt> or <tt>aMatch</tt> is <tt>null</tt>
*
* @throws IllegalStateException
* if <tt>aMatch</tt> is passed before a successful match could be made
* it is said to have inconsistent state.
*/
public PercentConversion(PercentToBraceConverter aConverter, MatchResult aMatch)
throws IllegalArgumentException, IllegalStateException {
if (null == aConverter) {
throw new IllegalArgumentException("Converter can't be null!");
}
if (null == aMatch) {
throw new IllegalArgumentException("Match can't be null!");
}
source = aMatch.group(0);
span = new int[] {aMatch.start(), aMatch.end()};
final Map<String, String> groups = extractTokenGroups(aMatch);
String spec = groups.get("Key");
if (null == spec) {
if ("%%".equals(source)) {
key = "";
} else {
key = aConverter.nextIndex();
}
} else {
key = spec;
}
spec = groups.get("Width");
if (null != spec && "*".equals(spec)) {
// TODO: {} representation is hard-wired, could generalize this if needed
width = String.format("{%s}", aConverter.nextIndex());
} else {
width = spec;
}
spec = groups.get("Precision");
if (null != spec && "*".equals(spec)) {
precision = String.format("{%s}", aConverter.nextIndex());
} else {
precision = spec;
}
flags = groups.get("Flags");
conversion = groups.get("Conversion");
}
/**
* Two <tt>PercentConversion</tt> instances are equal if
* their <tt>source</tt> and <tt>span</tt> are equal.
*
* @param obj the object to compare to
* @return <tt>true</tt> if both objects are <i>field equal</i>,
* <tt>false</tt> otherwise.
*/
@Override
public boolean equals(Object obj) {
if (null == obj) {
return false;
}
if (this == obj) {
return true;
}
if (!(obj instanceof PercentConversion)) {
return false;
}
PercentConversion other = (PercentConversion) obj;
if (!((span[0] == other.span[0]) &&
(span[1] == other.span[1]))) {
return false;
}
if (!source.equals(other.source)) {
return false;
}
return true;
}
/**
* Returns the combination of this <tt>PercentConversion</tt>'s <tt>span</tt>
* plus the hash code of the <tt>source</tt> which makes the hash code of two
* <tt>PercentConversions</tt> identical if their <tt>source</tt>, as well as
* the span in the surrounding text (the <i>context</i>) the conversion applies
* to, are equal.
*
* @return hash code
*/
@Override
public int hashCode() {
return (span[0] + span[1]) + source.hashCode();
}
@Override
public String toString() {
final String description =
MessageFormat.format("<{0}@0x{1} | source={2} span=[{3}:{4}]>",
this.getClass().getSimpleName(), Integer.toHexString(this.hashCode()),
source, span[0], span[1]);
return description;
}
/**
* Perform conversion to format-style brace syntax on a per-specifier or per-token basis.
* @return the converted string or <tt>null</tt>.
*/
public String toBrace() {
String result = null;
String conversion = this.conversion;
String key = "";
String flags = "";
if ("%".equals(conversion)) {
return source;
} else {
final boolean isNumber = Pattern.compile("\\d+").matcher(this.key).matches();
if (isNumber) {
key = this.key;
} else {
key = String.format("[%s]", this.key);
}
}
if (null != this.flags) {
flags = this.flags;
}
String align = "";
if (null == width) {
align = "";
} else if (flags.indexOf('-') > -1) {
align = "<";
} else if (flags.indexOf('0') > -1 &&
"diouxXbB".indexOf(conversion) > -1) {
align = "=";
} else {
align = ">";
}
String sign = "";
String fill = "";
String alt = "";
if (null != flags && flags.length() > 0) {
if (flags.indexOf('+') > -1) {
sign = "+";
} else if (flags.indexOf(' ') > -1) {
sign = " ";
}
if (flags.indexOf('0') > -1 &&
flags.indexOf('-') == -1 &&
"crs".indexOf(conversion) == -1) {
fill = "0";
}
if (flags.indexOf('#') > -1 &&
"diuxXbB".indexOf(conversion) > -1) {
alt = "#";
}
}
String transform = "";
if ("iu".indexOf(conversion) > -1) {
conversion = "d";
} else if ("rs".indexOf(conversion) > -1) {
// %s is interpreted as calling str() on the operand, so
// we specify !s in the {}-format. If we don't do this, then
// we can't convert the case where %s is used to print e.g. integers
// or floats.
transform = "!" + conversion;
conversion = "";
}
final String prefix = String.format("%s%s", key, transform);
String suffix = String.format("%s%s%s%s", fill, align, sign, alt);
if (null != width) {
suffix += width;
}
if (null != precision) {
suffix += "." + precision;
}
suffix += conversion;
result = prefix;
if (!StringUtils.isEmpty(suffix)) {
result += ":" + suffix;
}
result = String.format("{%s}", result);
return result;
}
/**
* Return the source string the PercentConversion was initialized with.
* @return the string representing the complete specifier token, e.g. <tt>%0.2f</tt>.
*/
@SuppressWarnings("unused")
private String getSource() {
return source;
}
private int[] getSpan() {
return span;
}
/**
* Build a group dictionary from the matched groups. <br>
* Guaranteed to have 6 keys:<br>
* <ol>
* <li><tt>Key</tt>: a key mapping (str) as used for dict interpolation, or a positional index (num as str)</li>
* <li><tt>Flags</tt>: notational flags (e.g. <tt>"#,0,+,-,<i><space></i>)</tt></li>
* <li><tt>Width</tt>: minimum width, e.g. <code>*,2</code> (optional)</li>
* <li><tt>Precision</tt>: precision specifier, e.g. <code>.*,.2</code> (optional)</li>
* <li><tt>Length</tt>: length modifier, e.g. <code>h,l,L</code> (optional)</li>
* <li><tt>Conversion</tt>: conversion specifier <code>d,i,o,u,x,X,e,E,f,F,g,G,c,r,s,%</code></li>
* </ol>
* @param matchResult
* @return the K, V mapped groups.
* @see {@link #TOKEN_PATTERN}
*/
private static Map<String, String> extractTokenGroups(final MatchResult matchResult) {
Assert.isLegal(6 == matchResult.groupCount(),
"E: match result from TOKEN_PATTERN is malformed. Group count must be 6.");
final Map<String, String> result = new HashMap<String,String>(6);
final String key = matchResult.group(1);
final String flags = matchResult.group(2);
final String width = matchResult.group(3);
final String precision = matchResult.group(4);
final String length = matchResult.group(5);
final String conversion = matchResult.group(6);
result.put("Key", key);
result.put("Flags", flags);
result.put("Width", width);
result.put("Precision", precision);
result.put("Length", length);
result.put("Conversion", conversion);
return result;
}
/**
* @return the Pattern to match Python format specifier tokens.
* @see {@link #TOKEN_PATTERN}
* @see {@link #extractTokenGroups}
*/
private static Pattern getTokenPattern() {
return TOKEN_PATTERN;
}
}
}