Skip to content

Commit

Permalink
String format compatibility (axuno#173)
Browse files Browse the repository at this point in the history
* Separated SmartFormat features from `string.Format`compatibility
* Moved `ParserSettings.UseStringFormatCompatibility` to `Settings.UseStringFormatCompatibility` because this does no more apply to the parser only.
* `string.Format` compatibility:
   * SmartFormat acts as a drop-in replacement, and on top allows for named placeholders besides indexed placeholders. Example (note the colon is not escaped):
   * ```csharp
            var now = DateTime.Now;
            var smartFmt = "It is now {Date:yyyy/MM/dd HH:mm:ss}";
            var stringFmt = $"It is now {now.Date:yyyy/MM/dd HH:mm:ss}";
            var formatter = Smart.CreateDefaultSmartFormat();
            formatter.Settings.UseStringFormatCompatibility = true;
            Assert.That(formatter.Format(smartFmt, now), Is.EqualTo(stringFmt));
      ```
   
   * Custom formatters of SmartFormat are not parsed and thus cannot be used
   * Curly braces are escaped the `string.Format` way with `{{` and `}}`
* SmartFormat added feature:
  * As long as special characters (`(){}:\`) are escaped, any character is allowed anywhere. Now this applies also for the colon. Example (note the escaped colon):
   * ```Csharp
            var now = DateTime.Now;
            var smartFmt = @"It is now {Date:yyyy/MM/dd HH\:mm\:ss}";
            var stringFmt = $"It is now {now.Date:yyyy/MM/dd HH:mm:ss}";
            var formatter = Smart.CreateDefaultSmartFormat();
            formatter.Settings.UseStringFormatCompatibility = false;
            Assert.That(formatter.Format(smartFmt, now), Is.EqualTo(stringFmt));
      ```
  * Tests are modified occordingly
* Parser does not process `string[] formatterExtensionNames` any more
  * CTOR does not take `formatterExtensionNames` as argument
  * `Parser.ParseFormat` does not check for a valid formatter name (it's implemented in the formatter anyway)
  • Loading branch information
axunonb committed Mar 9, 2022
1 parent b829e79 commit 71e6793
Show file tree
Hide file tree
Showing 15 changed files with 187 additions and 144 deletions.
2 changes: 1 addition & 1 deletion src/SmartFormat.Performance/SimpleSpanParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void Setup()
[Benchmark]
public void ParseSmartFormat()
{
var result = _sfParser.ParseFormat(_inputFormatString, new[] {"default"});
var result = _sfParser.ParseFormat(_inputFormatString);
}

/// <summary>
Expand Down
5 changes: 2 additions & 3 deletions src/SmartFormat.Performance/SourcePerformanceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,10 @@ public SourcePerformanceTests()
);

// Cache the parsing result, so we don't include parsing performance
var format = _jsonFormatter.Parser.ParseFormat(_format, _jsonFormatter.FormatterExtensions[0].Names);
var format = _jsonFormatter.Parser.ParseFormat(_format);
_formatCache = new FormatCache(format);

var formatForLiteral = _jsonFormatter.Parser.ParseFormat(_formatForLiteral,
_jsonFormatter.FormatterExtensions[0].Names);
var formatForLiteral = _jsonFormatter.Parser.ParseFormat(_formatForLiteral);
_formatCacheLiteral = new FormatCache(formatForLiteral);

}
Expand Down
4 changes: 2 additions & 2 deletions src/SmartFormat.Tests/Core/FormatCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public void Format_WithCache()
var data = new {Name = "Joe", City = "Melbourne"};
var formatter = GetSimpleFormatter();
var formatString = "{Name}, {City}";
var format = formatter.Parser.ParseFormat(formatString, formatter.FormatterExtensions[0].Names);
var format = formatter.Parser.ParseFormat(formatString);
var cache = new FormatCache(format);
Assert.That(formatter.FormatWithCache(ref cache, formatString, data), Is.EqualTo($"{data.Name}, {data.City}"));
}
Expand All @@ -49,7 +49,7 @@ public void Format_WithCache_Into()
var data = new {Name = "Joe", City = "Melbourne"};
var formatter = GetSimpleFormatter();
var formatString = "{Name}, {City}";
var format = formatter.Parser.ParseFormat(formatString, formatter.FormatterExtensions[0].Names);
var format = formatter.Parser.ParseFormat(formatString);
var cache = new FormatCache(format);
var output = new StringOutput();
formatter.FormatWithCacheInto(ref cache, output, formatString, data);
Expand Down
21 changes: 14 additions & 7 deletions src/SmartFormat.Tests/Core/FormatterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,24 @@ public void Nested_Placeholders_Braces()
var data = new {Person = new {FirstName = "John", LastName = "Long"}, Address = new {City = "London"}};
var formatter = Smart.CreateDefaultSmartFormat();

// This is necessary to avoid undesired trailing blanks:
// }}} are now considered as 3 different closing braces
formatter.Parser.UseAlternativeEscapeChar('\\');

// This allows a nested template to access outer scopes.
// Here, {City} will come from Address, but {FirstName} will come from Person:
var result = formatter.Format("{Person:{Address:City: {City}, Name: {FirstName}}}", data);
var result = formatter.Format("{Person:{Address:City\\: {City}, Name\\: {FirstName}}}", data);

Assert.That(result, Is.EqualTo("City: London, Name: John"));
}

[TestCase("({.Joe.})", ":{Joe}:")]
[TestCase("Kate", ":{(.Not:Joe.)}:")]
public void Any_Character_Anywhere_If_Escaped(string name, string expected)
{
var smart = Smart.CreateDefaultSmartFormat();
var arg = new {Name = name};
// {} and () must and can only be escaped inside options
var format = @":\{{Name:choose(\(\{.Joe.\}\)):Joe|(.Not\:Joe.)}\}:";
Assert.That(smart.Format(format, arg), Is.EqualTo(expected));
}

[TestCase(1)]
[TestCase(2)]
public void Nested_PlaceHolders_Conditional(int numOfPeople)
Expand Down Expand Up @@ -153,7 +160,7 @@ public void LeadingBackslashMustNotEscapeBraces()
{
var smart = Smart.CreateDefaultSmartFormat();
smart.Settings.Parser.ConvertCharacterStringLiterals = false;
smart.Settings.Parser.UseStringFormatCompatibility = true;
smart.Settings.UseStringFormatCompatibility = true;

var expected = "\\Hello";
var actual = smart.Format("\\{Test}", new { Test = "Hello" });
Expand Down Expand Up @@ -187,7 +194,7 @@ public void SmartFormatter_FormatDetails()
formatter.Settings.Formatter.ErrorAction = FormatErrorAction.OutputErrorInResult;
formatter.Settings.Parser.ErrorAction = ParseErrorAction.OutputErrorInResult;
formatter.Parser.AddAlphanumericSelectors(); // required for this test
var formatParsed = formatter.Parser.ParseFormat(format, new []{string.Empty});
var formatParsed = formatter.Parser.ParseFormat(format);
var formatDetails = new FormatDetails(formatter, formatParsed, args, null, null, output);

Assert.AreEqual(args, formatDetails.OriginalArgs);
Expand Down
48 changes: 23 additions & 25 deletions src/SmartFormat.Tests/Core/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public void TestParser()
"{a}",
" aaa {bbb_bbb.CCC} ddd ",
};
var results = formats.Select(f => new { format = f, parsed = parser.ParseFormat(f, new[] { Guid.NewGuid().ToString("N") }) }).ToArray();
var results = formats.Select(f => new { format = f, parsed = parser.ParseFormat(f) }).ToArray();

// Verify that the reconstructed formats
// match the original ones:
Expand Down Expand Up @@ -106,7 +106,7 @@ public void Parser_Ignores_Exceptions()
};
foreach (var format in invalidFormats)
{
_ = parser.ParseFormat(format, new[] { Guid.NewGuid().ToString("N") });
_ = parser.ParseFormat(format);
}
}

Expand All @@ -121,7 +121,7 @@ public void Parser_Error_Action_Ignore()

var parser = GetRegularParser();
parser.Settings.Parser.ErrorAction = ParseErrorAction.Ignore;
var parsed = parser.ParseFormat(invalidTemplate, new[] { Guid.NewGuid().ToString("N") });
var parsed = parser.ParseFormat(invalidTemplate);

Assert.That(parsed.Items.Count, Is.EqualTo(4), "Number of parsed items");
Assert.That(parsed.Items[0].RawText, Is.EqualTo("Hello, I'm "), "Literal text");
Expand All @@ -141,7 +141,7 @@ public void Parser_Error_Action_MaintainTokens(string invalidTemplate, bool last
{
var parser = GetRegularParser();
parser.Settings.Parser.ErrorAction = ParseErrorAction.MaintainTokens;
var parsed = parser.ParseFormat(invalidTemplate, new[] { Guid.NewGuid().ToString("N") });
var parsed = parser.ParseFormat(invalidTemplate);

Assert.That(parsed.Items.Count, Is.EqualTo(4), "Number of parsed items");
Assert.That(parsed.Items[0].RawText, Is.EqualTo("Hello, I'm "));
Expand All @@ -167,7 +167,7 @@ public void Parser_Error_Action_OutputErrorInResult()

var parser = GetRegularParser();
parser.Settings.Parser.ErrorAction = ParseErrorAction.OutputErrorInResult;
var parsed = parser.ParseFormat(invalidTemplate, new[] { Guid.NewGuid().ToString("N") });
var parsed = parser.ParseFormat(invalidTemplate);

Assert.That(parsed.Items.Count, Is.EqualTo(1));
Assert.That(parsed.Items[0].RawText, Does.StartWith("The format string has 3 issues"));
Expand Down Expand Up @@ -241,7 +241,7 @@ function interpolationSearch(sortedArray, seekIndex) {
";
var parser = GetRegularParser();
parser.Settings.Parser.ErrorAction = ParseErrorAction.MaintainTokens;
var parsed = parser.ParseFormat(js, new[] { Guid.NewGuid().ToString("N") });
var parsed = parser.ParseFormat(js);

// No characters should get lost compared to the format string,
// no matter if a Placeholder can be identified or not
Expand Down Expand Up @@ -290,7 +290,7 @@ .comment img {
";
var parser = GetRegularParser();
parser.Settings.Parser.ErrorAction = ParseErrorAction.MaintainTokens;
var parsed = parser.ParseFormat(css, new[] { Guid.NewGuid().ToString("N") });
var parsed = parser.ParseFormat(css);

// No characters should get lost compared to the format string,
// no matter if a Placeholder can be identified or not
Expand All @@ -313,8 +313,6 @@ .comment img {
private static Parser GetRegularParser()
{
var parser = new SmartFormatter() { Settings = { Parser = {ErrorAction = ParseErrorAction.ThrowError }}}.Parser;
parser.AddAlphanumericSelectors();
parser.AddOperators(".,");
return parser;
}

Expand All @@ -324,7 +322,7 @@ public void Test_Format_Substring()
var parser = GetRegularParser();
var formatString = " a|aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg|g ";

var format = parser.ParseFormat(formatString, new[] { Guid.NewGuid().ToString("N") });
var format = parser.ParseFormat(formatString);

// Extract the substrings of literal text:
Assert.That(format.Substring( 1, 3).ToString(), Is.EqualTo("a|a"));
Expand Down Expand Up @@ -364,7 +362,7 @@ public void Test_Format_Set_Alignment_Property()
var parser = GetRegularParser();
var formatString = "{0}";

var format = parser.ParseFormat(formatString, new[] { Guid.NewGuid().ToString("N") });
var format = parser.ParseFormat(formatString);
var placeholder = (Placeholder) format.Items[0];
Assert.AreEqual(formatString, placeholder.ToString());
placeholder.Alignment = 10;
Expand All @@ -377,7 +375,7 @@ public void Test_Format_With_Alignment()
var parser = GetRegularParser();
var formatString = "{0,-10}";

var format = parser.ParseFormat(formatString, new[] { Guid.NewGuid().ToString("N") });
var format = parser.ParseFormat(formatString);
var placeholder = (Placeholder) format.Items[0];
Assert.That(placeholder.ToString(), Is.EqualTo(formatString));
Assert.That(placeholder.Selectors.Count, Is.EqualTo(2));
Expand All @@ -391,7 +389,7 @@ public void Test_Formatter_Name_And_Options()
var parser = GetRegularParser();
var formatString = "{0}";

var format = parser.ParseFormat(formatString, new[] { Guid.NewGuid().ToString("N") });
var format = parser.ParseFormat(formatString);
var placeholder = (Placeholder)format.Items[0];
placeholder.FormatterName = "test";
Assert.AreEqual($"{{0:{placeholder.FormatterName}}}", placeholder.ToString());
Expand All @@ -404,7 +402,7 @@ public void Test_Format_IndexOf()
{
var parser = GetRegularParser();
var format = " a|aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg|g ";
var Format = parser.ParseFormat(format, new[] { Guid.NewGuid().ToString("N") });
var Format = parser.ParseFormat(format);

Assert.That(Format.IndexOf('|'), Is.EqualTo(2));
Assert.That(Format.IndexOf('|', 3), Is.EqualTo(43));
Expand All @@ -427,7 +425,7 @@ public void Test_Format_Split()
{
var parser = GetRegularParser();
var format = " a|aa {bbb: ccc dd|d {:|||} {eee} ff|f } gg|g ";
var parsedFormat = parser.ParseFormat(format, new[] { Guid.NewGuid().ToString("N") });
var parsedFormat = parser.ParseFormat(format);
var splits = parsedFormat.Split('|');

Assert.That(splits.Count, Is.EqualTo(3));
Expand All @@ -449,7 +447,7 @@ public void Test_Format_Split()

private Format Parse(string format, string[] formatterExentionNames )
{
return GetRegularParser().ParseFormat(format, formatterExentionNames);
return GetRegularParser().ParseFormat(format);
}

[TestCase("{0:name:}", "name", "", "")]
Expand Down Expand Up @@ -478,7 +476,7 @@ public void Name_of_unregistered_NamedFormatter_will_not_be_parsed()
{
// find formatter formattername, which does not exist in the (empty) list of formatter extensions
var placeholderWithNonExistingName = (Placeholder)Parse("{0:formattername:}", new string[] {} ).Items[0];
Assert.AreEqual("formattername:", placeholderWithNonExistingName.Format?.ToString()); // name is only treaded as a literal
Assert.AreEqual("formattername", placeholderWithNonExistingName.FormatterName); // name is only treaded as a literal
}

// Incomplete:
Expand Down Expand Up @@ -528,7 +526,7 @@ private void AssertNoNamedFormatter(string format, string expectedLiteralText)
parser.UseAlternativeEscapeChar('\\');
parser.Settings.ConvertCharacterStringLiterals = false;

var placeholder = (Placeholder) parser.ParseFormat(format, new[] { Guid.NewGuid().ToString("N") }).Items[0];
var placeholder = (Placeholder) parser.ParseFormat(format).Items[0];
Assert.IsEmpty(placeholder.FormatterName);
Assert.IsEmpty(placeholder.FormatterOptions);
var literalText = placeholder.Format?.GetLiteralText();
Expand All @@ -553,7 +551,7 @@ public void Alternative_Escaping_In_Literal()
{
var parser = GetRegularParser();
parser.UseAlternativeEscapeChar('\\');
Assert.That(parser.ParseFormat("\\{\\}", Array.Empty<string>()).ToString(), Is.EqualTo("{}"));
Assert.That(parser.ParseFormat("\\{\\}").ToString(), Is.EqualTo("{}"));
}

[Test]
Expand All @@ -562,7 +560,7 @@ public void Nested_format_with_alternative_escaping()
var parser = GetRegularParser();
// necessary because of the consecutive }}}, which would otherwise be escaped as }} and lead to "missing brace" exception:
parser.UseAlternativeEscapeChar('\\');
var placeholders = parser.ParseFormat("{c1:{c2:{c3}}}", new[] {Guid.NewGuid().ToString("N")});
var placeholders = parser.ParseFormat("{c1:{c2:{c3}}}");

var c1 = (Placeholder) placeholders.Items[0];
var c2 = (Placeholder) c1.Format?.Items[0]!;
Expand All @@ -585,7 +583,7 @@ public void Parse_Options()
// The literal may also contain escaped characters
var literal = "one|two|th\\} \\{ree|other";

var format = parser.ParseFormat($"{{{selector}:{formatterName}({options}):{literal}}}", new[] {formatterName});
var format = parser.ParseFormat($"{{{selector}:{formatterName}({options}):{literal}}}");
var placeholder = (Placeholder) format.Items[0];

Assert.That(format.Items.Count, Is.EqualTo(1));
Expand All @@ -606,7 +604,7 @@ public void Parse_Options()
public void Parse_Unicode(string formatString, string unicodeLiteral, int itemIndex, bool isLegal)
{
var parser = GetRegularParser();
var result = parser.ParseFormat(formatString, new[] {"d"});
var result = parser.ParseFormat(formatString);

var literal = result.Items[itemIndex];
Assert.That(literal, Is.TypeOf(typeof(LiteralText)));
Expand All @@ -625,7 +623,7 @@ public void Selector_With_Custom_Selector_Character(string formatString, char cu
{
var parser = GetRegularParser();
parser.Settings.Parser.AddCustomSelectorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});
var result = parser.ParseFormat(formatString);

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Expand All @@ -639,7 +637,7 @@ public void Selectors_With_Custom_Operator_Character(string formatString, char c
{
var parser = GetRegularParser();
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});
var result = parser.ParseFormat(formatString);

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Expand All @@ -658,7 +656,7 @@ public void Selector_With_Contiguous_Operator_Characters(string formatString, ch
var parser = GetRegularParser();
// adding '.' is ignored, as it's the standard operator
parser.Settings.Parser.AddCustomOperatorChars(new[]{customChar});
var result = parser.ParseFormat(formatString, new[] {"d"});
var result = parser.ParseFormat(formatString);

var placeholder = result.Items[0] as Placeholder;
Assert.That(placeholder, Is.Not.Null);
Expand Down
Loading

0 comments on commit 71e6793

Please sign in to comment.