diff --git a/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java b/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java index d32c798fab60..7733d68f58aa 100644 --- a/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java +++ b/python/src/com/jetbrains/python/codeInsight/PySubstitutionChunkReference.java @@ -40,11 +40,11 @@ public class PySubstitutionChunkReference extends PsiReferenceBase chunks = PyStringFormatParser.filterSubstitutions( PyStringFormatParser.parseNewStyleFormat(element.getText())); - return getReferencesFromChunks(element, chunks, false); + return getReferencesFromChunks(element, chunks); } private static PySubstitutionChunkReference[] getReferencesFromPercentString(@NotNull final PyStringLiteralExpression element) { final List chunks = PyStringFormatParser.filterSubstitutions(PyStringFormatParser.parsePercentFormat(element.getText())); - return getReferencesFromChunks(element, chunks, true); + return getReferencesFromChunks(element, chunks); } @NotNull public static PySubstitutionChunkReference[] getReferencesFromChunks(@NotNull final PyStringLiteralExpression element, - @NotNull final List chunks, - boolean isPercent) { + @NotNull final List chunks) { + final PySubstitutionChunkReference[] result = new PySubstitutionChunkReference[chunks.size()]; for (int i = 0; i < chunks.size(); i++) { final PyStringFormatParser.SubstitutionChunk chunk = chunks.get(i); - result[i] = new PySubstitutionChunkReference(element, chunk, i, isPercent); + result[i] = new PySubstitutionChunkReference(element, chunk, i); } return result; } diff --git a/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java b/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java index 929315a73e19..b048331c5290 100644 --- a/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java +++ b/python/src/com/jetbrains/python/inspections/PyStringFormatInspection.java @@ -66,7 +66,7 @@ public class PyStringFormatInspection extends PyInspection { public static class Visitor extends PyInspectionVisitor { private static class Inspection { - private static final ImmutableMap FORMAT_CONVERSIONS = ImmutableMap.builder() + private static final ImmutableMap PERCENT_FORMAT_CONVERSIONS = ImmutableMap.builder() .put('d', "int or long or float") .put('i', "int or long or float") .put('o', "int or long or float") @@ -364,7 +364,7 @@ public class PyStringFormatInspection extends PyInspection { } } - private void inspectFormat(@NotNull final PyStringLiteralExpression formatExpression) { + private void inspectPercentFormat(@NotNull final PyStringLiteralExpression formatExpression) { final String value = formatExpression.getStringValue(); final List chunks = filterSubstitutions(parsePercentFormat(value)); @@ -374,40 +374,42 @@ public class PyStringFormatInspection extends PyInspection { // if use mapping keys final boolean mapping = chunks.size() > 0 && chunks.get(0).getMappingKey() != null; for (int i = 0; i < chunks.size(); ++i) { - PyStringFormatParser.SubstitutionChunk chunk = chunks.get(i); - - // Mapping key - String mappingKey = Integer.toString(i + 1); - if (mapping) { - if (chunk.getMappingKey() == null || chunk.isUnclosedMapping()) { - registerProblem(formatExpression, PyBundle.message("INSP.too.few.keys")); - break; + + PyStringFormatParser.PercentSubstitutionChunk chunk = as(chunks.get(i), PyStringFormatParser.PercentSubstitutionChunk.class); + if (chunk != null) { + // Mapping key + String mappingKey = Integer.toString(i + 1); + if (mapping) { + if (chunk.getMappingKey() == null || chunk.isUnclosedMapping()) { + registerProblem(formatExpression, PyBundle.message("INSP.too.few.keys")); + break; + } + mappingKey = chunk.getMappingKey(); + myUsedMappingKeys.put(mappingKey, false); } - mappingKey = chunk.getMappingKey(); - myUsedMappingKeys.put(mappingKey, false); - } - // Minimum field width - inspectWidth(formatExpression, chunk.getWidth()); + // Minimum field width + inspectWidth(formatExpression, chunk.getWidth()); - // Precision - inspectWidth(formatExpression, chunk.getPrecision()); + // Precision + inspectWidth(formatExpression, chunk.getPrecision()); - // Format specifier - final char conversionType = chunk.getConversionType(); - if (conversionType == 'b') { - final LanguageLevel languageLevel = LanguageLevel.forElement(formatExpression); - if (languageLevel.isOlderThan(LanguageLevel.PYTHON35) || !isBytesLiteral(formatExpression, myTypeEvalContext)) { - registerProblem(formatExpression, "Unsupported format character 'b'"); - return; + // Format specifier + final char conversionType = chunk.getConversionType(); + if (conversionType == 'b') { + final LanguageLevel languageLevel = LanguageLevel.forElement(formatExpression); + if (languageLevel.isOlderThan(LanguageLevel.PYTHON35) || !isBytesLiteral(formatExpression, myTypeEvalContext)) { + registerProblem(formatExpression, "Unsupported format character 'b'"); + return; + } } + if (PERCENT_FORMAT_CONVERSIONS.containsKey(conversionType)) { + myFormatSpec.put(mappingKey, PERCENT_FORMAT_CONVERSIONS.get(conversionType)); + continue; + } + registerProblem(formatExpression, PyBundle.message("INSP.no.format.specifier.char"), new PyAddSpecifierToFormatQuickFix()); + return; } - if (FORMAT_CONVERSIONS.containsKey(conversionType)) { - myFormatSpec.put(mappingKey, FORMAT_CONVERSIONS.get(conversionType)); - continue; - } - registerProblem(formatExpression, PyBundle.message("INSP.no.format.specifier.char"), new PyAddSpecifierToFormatQuickFix()); - return; } } @@ -472,7 +474,7 @@ public class PyStringFormatInspection extends PyInspection { if (node.getLeftExpression() instanceof PyStringLiteralExpression && node.isOperator("%")) { final Inspection inspection = new Inspection(this, myTypeEvalContext); final PyStringLiteralExpression literalExpression = (PyStringLiteralExpression)node.getLeftExpression(); - inspection.inspectFormat(literalExpression); + inspection.inspectPercentFormat(literalExpression); if (inspection.isProblem()) { return; } diff --git a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java index 33e4530586ed..583d1eeee91e 100644 --- a/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java +++ b/python/src/com/jetbrains/python/inspections/PyStringFormatParser.java @@ -69,83 +69,19 @@ public class PyStringFormatParser { public static class SubstitutionChunk extends FormatStringChunk { @Nullable private String myMappingKey; - @Nullable private String myConversionFlags; @Nullable private String myWidth; @Nullable private String myPrecision; @Nullable private Integer myPosition; @Nullable private Integer myAutoPosition; - private char myLengthModifier; - private char myConversionType; - private boolean myUnclosedMapping; - public SubstitutionChunk(int startIndex) { - super(startIndex, startIndex); + public SubstitutionChunk(int startIndex, int endIndex) { + super(startIndex, endIndex); } protected void setEndIndex(int endIndex) { myEndIndex = endIndex; } - public char getConversionType() { - return myConversionType; - } - - private void setConversionType(char conversionType) { - myConversionType = conversionType; - } - - @Nullable - public String getMappingKey() { - return myMappingKey; - } - - protected void setMappingKey(@Nullable String mappingKey) { - myMappingKey = mappingKey; - } - - @Nullable - public String getConversionFlags() { - return myConversionFlags; - } - - private void setConversionFlags(@Nullable String conversionFlags) { - myConversionFlags = conversionFlags; - } - - @Nullable - public String getWidth() { - return myWidth; - } - - private void setWidth(@Nullable String width) { - myWidth = width; - } - - @Nullable - public String getPrecision() { - return myPrecision; - } - - private void setPrecision(@Nullable String precision) { - myPrecision = precision; - } - - public char getLengthModifier() { - return myLengthModifier; - } - - private void setLengthModifier(char lengthModifier) { - myLengthModifier = lengthModifier; - } - - public boolean isUnclosedMapping() { - return myUnclosedMapping; - } - - private void setUnclosedMapping(boolean unclosedMapping) { - myUnclosedMapping = unclosedMapping; - } - @Nullable public Integer getPosition() { return myPosition; @@ -171,6 +107,169 @@ public class PyStringFormatParser { protected void setAutoPosition(@Nullable Integer autoPosition) { myAutoPosition = autoPosition; } + + @Nullable + public String getPrecision() { + return myPrecision; + } + + public void setPrecision(@Nullable String precision) { + myPrecision = precision; + } + + @Nullable + public String getWidth() { + return myWidth; + } + + public void setWidth(@Nullable String width) { + myWidth = width; + } + + @Nullable + public String getMappingKey() { + return myMappingKey; + } + + protected void setMappingKey(@Nullable String mappingKey) { + myMappingKey = mappingKey; + } + } + + public static class PercentSubstitutionChunk extends SubstitutionChunk { + @Nullable private String myConversionFlags; + private char myLengthModifier; + private char myConversionType; + private boolean myUnclosedMapping; + + public PercentSubstitutionChunk(int startIndex) { + super(startIndex, startIndex); + } + + public char getConversionType() { + return myConversionType; + } + + private void setConversionType(char conversionType) { + myConversionType = conversionType; + } + + @Nullable + public String getConversionFlags() { + return myConversionFlags; + } + + private void setConversionFlags(@Nullable String conversionFlags) { + myConversionFlags = conversionFlags; + } + + public char getLengthModifier() { + return myLengthModifier; + } + + private void setLengthModifier(char lengthModifier) { + myLengthModifier = lengthModifier; + } + + public boolean isUnclosedMapping() { + return myUnclosedMapping; + } + + private void setUnclosedMapping(boolean unclosedMapping) { + myUnclosedMapping = unclosedMapping; + } + } + public static class NewStyleSubstitutionChunk extends SubstitutionChunk { + @Nullable private String myMappingKey; + @Nullable private String myConversion; + @Nullable private String myWidth; + @Nullable private String myPrecision; + @Nullable private Integer myPosition; + private char myConversionType; + private boolean signOption; + private boolean zeroPadding; + private boolean alternateForm; + private boolean thousandsSeparator; + + public NewStyleSubstitutionChunk(int startIndex) { + super(startIndex, startIndex); + } + + @Nullable + public String getMappingKey() { + return myMappingKey; + } + + public void setMappingKey(@Nullable String mappingKey) { + myMappingKey = mappingKey; + } + + @Nullable + public String getConversion() { + return myConversion; + } + + public void setConversion(@Nullable String conversion) { + myConversion = conversion; + } + + @Nullable + public String getWidth() { + return myWidth; + } + + public void setWidth(@Nullable String width) { + myWidth = width; + } + + @Nullable + public String getPrecision() { + return myPrecision; + } + + public void setPrecision(@Nullable String precision) { + myPrecision = precision; + } + + public boolean hasSignOption() { + return signOption; + } + + public void setSignOption(boolean signOption) { + this.signOption = signOption; + } + + public char getConversionType() { + return myConversionType; + } + + public void setConversionType(char conversionType) { + myConversionType = conversionType; + } + + public boolean useAlternateForm() { + return alternateForm; + } + + public void setAlternateForm(boolean alternateForm) { + this.alternateForm = alternateForm; + } + + public boolean hasZeroPadding() { + return zeroPadding; + } + + public void setZeroPadding(boolean zeroPadding) { + this.zeroPadding = zeroPadding; + } + + public boolean hasThousandsSeparator() { + return thousandsSeparator; + } + + public void setThousandsSeparator(boolean thousandsSeparator) { + this.thousandsSeparator = thousandsSeparator; + } } @NotNull private final String myLiteral; @@ -178,11 +277,21 @@ public class PyStringFormatParser { private int myPos; private int mySubstitutionsCount = 0; + // % strings private static final String CONVERSION_FLAGS = "#0- +"; private static final String DIGITS = "0123456789"; private static final String LENGTH_MODIFIERS = "hlL"; private static final String VALID_CONVERSION_TYPES = "diouxXeEfFgGcrsb"; + // new style strings + private static final String ALIGN_SYMBOLS = "<>=^"; + private static final String SIGN_SYMBOLS = "+- "; + private static final String NEW_STYLE_CONVERSION_TYPES = "bcdeEfFgGnosxX%"; + private static final String CONVERSIONS = "rsa"; + private static final char ALTERNATE_FORM_SYMBOL = '#'; + private static final char ZERO_PADDING_SYMBOL = '0'; + + private PyStringFormatParser(@NotNull String literal) { myLiteral = literal; } @@ -194,42 +303,7 @@ public class PyStringFormatParser { @NotNull public static List parseNewStyleFormat(@NotNull String s) { - final List results = new ArrayList<>(); - final Matcher matcher = NEW_STYLE_FORMAT_TOKENS.matcher(s); - int autoPositionedFieldsCount = 0; - while (matcher.find()) { - final String group = matcher.group(); - final int start = matcher.start(); - final int end = matcher.end(); - if ("{{".equals(group) || "}}".equals(group)) { - results.add(new ConstantChunk(start, end)); - } - else if (group.startsWith("{") && group.endsWith("}")) { - final SubstitutionChunk chunk = new SubstitutionChunk(start); - chunk.setEndIndex(end); - final int nameStart = start + 1; - final int nameEnd = StringUtil.indexOfAny(s, "!:.[}", nameStart, end); - if (nameEnd > 0 && nameStart < nameEnd) { - final String name = s.substring(nameStart, nameEnd); - try { - final int number = Integer.parseInt(name); - chunk.setPosition(number); - } catch (NumberFormatException e) { - chunk.setMappingKey(name); - } - } - else { - chunk.setAutoPosition(autoPositionedFieldsCount); - autoPositionedFieldsCount++; - } - // TODO: Parse substitution details - results.add(chunk); - } - else { - results.add(new ConstantChunk(start, end)); - } - } - return results; + return new PyStringFormatParser(s).parseNewStyle(); } @NotNull @@ -253,9 +327,107 @@ public class PyStringFormatParser { return myResult; } + private List parseNewStyle() { + final List results = new ArrayList(); + final Matcher matcher = NEW_STYLE_FORMAT_TOKENS.matcher(myLiteral); + int autoPositionedFieldsCount = 0; + while (matcher.find()) { + final String group = matcher.group(); + myPos = matcher.start(); + final int end = matcher.end(); + if ("{{".equals(group) || "}}".equals(group)) { + results.add(new ConstantChunk(myPos, end)); + } + else if (group.startsWith("{") && group.endsWith("}")) { + autoPositionedFieldsCount = parseNewStyleSubstitution(results, end, autoPositionedFieldsCount); + } + else { + results.add(new ConstantChunk(myPos, end)); + } + } + return results; + } + + private int parseNewStyleSubstitution(List results, int end, int autoPositionedFieldsCount) { + final NewStyleSubstitutionChunk chunk = new NewStyleSubstitutionChunk(myPos); + chunk.setEndIndex(end); + + // skip "{" + myPos++; + // name + final int nameEnd = StringUtil.indexOfAny(myLiteral, "!:.[}", myPos, end); + if (nameEnd > 0 && myPos < nameEnd) { + final String name = myLiteral.substring(myPos, nameEnd); + try { + final int number = Integer.parseInt(name); + chunk.setPosition(number); + } catch (NumberFormatException e) { + chunk.setMappingKey(name); + } + } + else { + chunk.setAutoPosition(autoPositionedFieldsCount); + autoPositionedFieldsCount++; + } + + // conversion + myPos = Math.max(myPos, StringUtil.indexOf(myLiteral, '!', myPos, end) + 1); + final int conversionEnd = StringUtil.indexOfAny(myLiteral, ":}", myPos, end); + if (conversionEnd - myPos == 1) { + final String conversion = myLiteral.substring(myPos, conversionEnd); + if (StringUtil.containsAnyChar(conversion, CONVERSIONS)) { + chunk.setConversion(conversion); + myPos = conversionEnd + 1; + } + } + + // parse format spec + // [[fill]align][sign][#][0][width][,][.precision][type] + if (isAt(':')) { + myPos++; + + //skip align options + myPos = Math.max(myPos, StringUtil.indexOfAny(myLiteral, ALIGN_SYMBOLS, myPos, end) + 1); + + if (isAtSet(SIGN_SYMBOLS)) { + chunk.setSignOption(true); + myPos++; + } + + if (isAt(ALTERNATE_FORM_SYMBOL)) { + chunk.setAlternateForm(true); + myPos++; + } + + if (isAt(ZERO_PADDING_SYMBOL)) { + chunk.setZeroPadding(true); + myPos++; + } + + chunk.setWidth(parseWhileCharacterInSet(DIGITS)); + + if (isAt(',')) { + myPos++; + chunk.setThousandsSeparator(true); + } + + if (isAt('.')) { + myPos++; + chunk.setPrecision(parseWhileCharacterInSet(DIGITS)); + } + + if (isAtSet(NEW_STYLE_CONVERSION_TYPES)) { + chunk.setConversionType(myLiteral.charAt(myPos)); + } + } + + results.add(chunk); + return autoPositionedFieldsCount; + } + private void parseSubstitution() { assert myLiteral.charAt(myPos) == '%'; - SubstitutionChunk chunk = new SubstitutionChunk(myPos); + PercentSubstitutionChunk chunk = new PercentSubstitutionChunk(myPos); myResult.add(chunk); myPos++; if (isAt('(')) { diff --git a/python/src/com/jetbrains/python/inspections/quickfix/PyAddSpecifierToFormatQuickFix.java b/python/src/com/jetbrains/python/inspections/quickfix/PyAddSpecifierToFormatQuickFix.java index 1b62a1ef23e4..04e3ef17ae02 100644 --- a/python/src/com/jetbrains/python/inspections/quickfix/PyAddSpecifierToFormatQuickFix.java +++ b/python/src/com/jetbrains/python/inspections/quickfix/PyAddSpecifierToFormatQuickFix.java @@ -79,20 +79,22 @@ public class PyAddSpecifierToFormatQuickFix implements LocalQuickFix { int shift = 2; for (int i = 0; i < chunks.size(); i++) { - final PyStringFormatParser.SubstitutionChunk chunk = chunks.get(i); - if (elements.length <= i) return; - final PyType type = context.getType(elements[i]); - final char conversionType = chunk.getConversionType(); - if (conversionType == '\u0000') { - final int insertOffset = offset + chunk.getStartIndex() + shift; - if (insertOffset > leftExpression.getTextRange().getEndOffset()) return; - if (PyTypeChecker.match(strType, type, context)) { - document.insertString(insertOffset, "s"); - shift += 1; - } - if (PyTypeChecker.match(intType, type, context) || PyTypeChecker.match(floatType, type, context)) { - document.insertString(insertOffset, "d"); - shift += 1; + final PyStringFormatParser.PercentSubstitutionChunk chunk = PyUtil.as(chunks.get(i), PyStringFormatParser.PercentSubstitutionChunk.class); + if (chunk != null) { + if (elements.length <= i) return; + final PyType type = context.getType(elements[i]); + final char conversionType = chunk.getConversionType(); + if (conversionType == '\u0000') { + final int insertOffset = offset + chunk.getStartIndex() + shift; + if (insertOffset > leftExpression.getTextRange().getEndOffset()) return; + if (PyTypeChecker.match(strType, type, context)) { + document.insertString(insertOffset, "s"); + shift += 1; + } + if (PyTypeChecker.match(intType, type, context) || PyTypeChecker.match(floatType, type, context)) { + document.insertString(insertOffset, "d"); + shift += 1; + } } } } diff --git a/python/testSrc/com/jetbrains/python/PyStringFormatParserTest.java b/python/testSrc/com/jetbrains/python/PyStringFormatParserTest.java index f12e69bee207..28bc5085da7a 100644 --- a/python/testSrc/com/jetbrains/python/PyStringFormatParserTest.java +++ b/python/testSrc/com/jetbrains/python/PyStringFormatParserTest.java @@ -53,7 +53,7 @@ public class PyStringFormatParserTest extends TestCase { public void testFormat() { List chunks = parsePercentFormat("%s"); assertEquals(1, chunks.size()); - SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0); + PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0); assertEquals(0, chunk.getStartIndex()); assertEquals(2, chunk.getEndIndex()); assertEquals('s', chunk.getConversionType()); @@ -68,7 +68,7 @@ public class PyStringFormatParserTest extends TestCase { public void testMappingKey() { List chunks = parsePercentFormat("%(language)s"); assertEquals(1, chunks.size()); - SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0); + PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0); assertEquals("language", chunk.getMappingKey()); assertEquals('s', chunk.getConversionType()); } @@ -76,7 +76,7 @@ public class PyStringFormatParserTest extends TestCase { public void testConversionFlags() { List chunks = parsePercentFormat("%#0d"); assertEquals(1, chunks.size()); - SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0); + PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0); assertEquals("#0", chunk.getConversionFlags()); } @@ -97,21 +97,21 @@ public class PyStringFormatParserTest extends TestCase { public void testLengthModifier() { List chunks = parsePercentFormat("%ld"); assertEquals(1, chunks.size()); - SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0); + PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0); assertEquals('l', chunk.getLengthModifier()); } public void testDoubleAsterisk() { List chunks = parsePercentFormat("%**d"); assertEquals(2, chunks.size()); - SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0); + PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0); assertEquals(2, chunk.getEndIndex()); assertEquals('\0', chunk.getConversionType()); } public void testUnclosedMapping() { List chunks = parsePercentFormat("%(name1s"); - SubstitutionChunk chunk = (SubstitutionChunk)chunks.get(0); + PercentSubstitutionChunk chunk = (PercentSubstitutionChunk)chunks.get(0); assertEquals("name1s", chunk.getMappingKey()); assertTrue(chunk.isUnclosedMapping()); } @@ -174,6 +174,70 @@ public class PyStringFormatParserTest extends TestCase { assertEquals(TextRange.create(7, 9), chunks.get(2).getTextRange()); } + public void testNewStyleSign() { + final List chunks = parseNewStyleFormat("{:+}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange()); + assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasSignOption()); + } + + public void testNewStyleAlternateForm() { + final List chunks = parseNewStyleFormat("{:#}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange()); + assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).useAlternateForm()); + } + + public void testNewStyleZeroPadded() { + final List chunks = parseNewStyleFormat("{:0}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange()); + assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasZeroPadding()); + } + + public void testNewStyleWidth() { + final List chunks = parseNewStyleFormat("{:10}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 5), chunks.get(0).getTextRange()); + assertEquals("10", ((NewStyleSubstitutionChunk)chunks.get(0)).getWidth()); + } + + public void testNewStyleZeroPaddingWidth() { + final List chunks = parseNewStyleFormat("{:010}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 6), chunks.get(0).getTextRange()); + assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasZeroPadding()); + assertEquals("10", ((NewStyleSubstitutionChunk)chunks.get(0)).getWidth()); + } + + public void testNewStyleThousandSeparator() { + final List chunks = parseNewStyleFormat("{:,}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange()); + assertTrue(((NewStyleSubstitutionChunk)chunks.get(0)).hasThousandsSeparator()); + } + + public void testNewStylePrecision() { + final List chunks = parseNewStyleFormat("{:.2}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 5), chunks.get(0).getTextRange()); + assertEquals("2", ((NewStyleSubstitutionChunk)chunks.get(0)).getPrecision()); + } + + public void testNewStyleConversionType() { + final List chunks = parseNewStyleFormat("{:d}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange()); + assertEquals('d', ((NewStyleSubstitutionChunk)chunks.get(0)).getConversionType()); + } + + public void testNewStyleConversion() { + final List chunks = parseNewStyleFormat("{!s}"); + assertEquals(1, chunks.size()); + assertEquals(TextRange.create(0, 4), chunks.get(0).getTextRange()); + assertEquals("s", ((NewStyleSubstitutionChunk)chunks.get(0)).getConversion()); + } + public void testNewStyleNestedFields() { final Field field = doParseAndGetFirstField("u'{foo:{bar} {baz}}'"); assertEquals("foo", field.getFirstName());