patterncsharpMinor
Named string interpolation
Viewed 0 times
interpolationstringnamed
Problem
On machines where I don't have C# 6 I use this named string interpolation method. I tried to make it as pretty a possible as far as good coding practices are concerned but I just can't get rid of the repeating code for braces validation and index incrementation. Somehow I don't like it.
Result:
Lorem abc 2.1 {sit} met.
public static string FormatFrom(this string text, object args, bool ignoreCase = true)
{
var substrings = Regex.Split(text, "({{?)([A-Za-z_][A-Za-z0-9_]+)(}}?)");
var argsType = args.GetType();
var result = new StringBuilder(text.Length);
const int leftBraceOffset = 0;
const int propertyNameOffset = 1;
const int rightBraceOffset = 2;
for (int i = 0; i < substrings.Length; i++)
{
var leftBraceIndex = i + leftBraceOffset;
var propertyNameIndex = i + propertyNameOffset;
var rightBraceIndex = i + rightBraceOffset;
var isPropertyName = substrings[leftBraceIndex] == "{" && substrings[rightBraceIndex] == "}";
if (isPropertyName)
{
var propertyName = substrings[propertyNameIndex];
var property = argsType.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
result.Append(property.GetValue(args));
i += 2;
continue;
}
var isEscapedPropertyName = substrings[leftBraceIndex] == "{{" && substrings[rightBraceIndex] == "}}";
if (isEscapedPropertyName)
{
result.Append("{").Append(substrings[propertyNameIndex]).Append("}");
i += 2;
continue;
}
result.Append(substrings[i]);
}
return result.ToString();
}var text = "Lorem {ipsum} {dolor} {{sit}} met.";
var obj = new { ipsum = "abc", dolor = 2.1 };
var text2 = text.FormatFrom(obj);Result:
Lorem abc 2.1 {sit} met.
Solution
-
Always check the argument which is reffered by
-
You don't check for
-
The optional argument
-
if by accident the property of the anonymous object isn't spelled exactly like in the string, the call to
-
if the passed in text only contains
-
the regex pattern does not allow single letter variables being passed. So a text like
Implementing the mentioned points will lead to
which will pass all of these tests
Always check the argument which is reffered by
this in an extension method against null to early throw and return. Sure one could say it doesn't matter, because it will throw an ArgumentNullException but that would be thrown from the Regex.Split() method. -
You don't check for
args == null either. -
The optional argument
ignoreCase isn't used anywhere in that method so it can safely removed. -
if by accident the property of the anonymous object isn't spelled exactly like in the string, the call to
argsType.GetProperty() will return null and an NullReferenceException is thrown. Maybe it would be better for such a case to just assume it isn't a property. I will come back to this later. -
if the passed in text only contains
{ the code will throw an IndexOutOfRange exception. This can be prevented by returning early if the length of text is
-
if the Length of substrings will be -
the regex pattern does not allow single letter variables being passed. So a text like
{i} won't be matched. Implementing the mentioned points will lead to
public static string FormatFrom(this string text, object args)
{
if (text == null) { throw new ArgumentNullException("text"); }
if (text.Length s != string.Empty).ToArray();
if (substrings.Length < 3) { return text; }
var argsType = args.GetType();
var result = new StringBuilder(text.Length);
const int propertyNameOffset = 1;
const int rightBraceOffset = 2;
var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
for (int i = 0; i < substrings.Length; i++)
{
var possibleLeftBraces = substrings[i];
var possibleRightBraces = substrings[i + rightBraceOffset];
var propertyName = substrings[i + propertyNameOffset];
var isPropertyName = possibleLeftBraces == "{" && possibleRightBraces == "}";
if (isPropertyName)
{
var property = argsType.GetProperty(propertyName, bindingFlags);
if (property == null)
{
result.Append("{").Append(propertyName).Append("}");
}
else
{
result.Append(property.GetValue(args, null));
}
i += 2;
continue;
}
var isEscapedPropertyName = possibleLeftBraces == "{{" && possibleRightBraces == "}}";
if (isEscapedPropertyName)
{
result.Append("{").Append(propertyName).Append("}");
i += 2;
continue;
}
result.Append(substrings[i]);
}
return result.ToString();
}which will pass all of these tests
[TestMethod()]
public void FormatFromTestStringEmptyShouldPass()
{
string expected = string.Empty;
string actual = string.Empty.FormatFrom(null);
Assert.AreEqual(expected, actual);
}
[TestMethod(),ExpectedException(typeof(ArgumentNullException))]
public void FormatFromTestStrinNullShouldPass()
{
string actual = ((string)null).FormatFrom(null);
Assert.Inconclusive("Shouldn't happen !");
}
[TestMethod()]
public void FormatFromTestArgsNullShouldPass()
{
string expected = "lala";
string actual = "lala".FormatFrom(null);
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void FormatFromTestParamsButArgsNullShouldPass()
{
string expected = "{land}";
string actual = "{land}".FormatFrom(null);
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void FormatFromTestArgsNotNullShouldPass()
{
string expected = "germany";
string actual = "{land}".FormatFrom(new { land = "germany" });
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void FormatFromTestArgsNotNullButWrongShouldPass()
{
string expected = "{land}"; // TODO: Passenden Wert initialisieren
string actual;
actual = "{land}".FormatFrom(new { lan = "germany" });
Assert.AreEqual(expected, actual);
}Code Snippets
public static string FormatFrom(this string text, object args)
{
if (text == null) { throw new ArgumentNullException("text"); }
if (text.Length < 3 || string.IsNullOrWhiteSpace(text) || args==null) { return text; }
var substrings = Regex.Split(text, "({{?)([A-Za-z_][A-Za-z0-9_]+)(}}?)")
.Where(s => s != string.Empty).ToArray();
if (substrings.Length < 3) { return text; }
var argsType = args.GetType();
var result = new StringBuilder(text.Length);
const int propertyNameOffset = 1;
const int rightBraceOffset = 2;
var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
for (int i = 0; i < substrings.Length; i++)
{
var possibleLeftBraces = substrings[i];
var possibleRightBraces = substrings[i + rightBraceOffset];
var propertyName = substrings[i + propertyNameOffset];
var isPropertyName = possibleLeftBraces == "{" && possibleRightBraces == "}";
if (isPropertyName)
{
var property = argsType.GetProperty(propertyName, bindingFlags);
if (property == null)
{
result.Append("{").Append(propertyName).Append("}");
}
else
{
result.Append(property.GetValue(args, null));
}
i += 2;
continue;
}
var isEscapedPropertyName = possibleLeftBraces == "{{" && possibleRightBraces == "}}";
if (isEscapedPropertyName)
{
result.Append("{").Append(propertyName).Append("}");
i += 2;
continue;
}
result.Append(substrings[i]);
}
return result.ToString();
}[TestMethod()]
public void FormatFromTestStringEmptyShouldPass()
{
string expected = string.Empty;
string actual = string.Empty.FormatFrom(null);
Assert.AreEqual(expected, actual);
}
[TestMethod(),ExpectedException(typeof(ArgumentNullException))]
public void FormatFromTestStrinNullShouldPass()
{
string actual = ((string)null).FormatFrom(null);
Assert.Inconclusive("Shouldn't happen !");
}
[TestMethod()]
public void FormatFromTestArgsNullShouldPass()
{
string expected = "lala";
string actual = "lala".FormatFrom(null);
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void FormatFromTestParamsButArgsNullShouldPass()
{
string expected = "{land}";
string actual = "{land}".FormatFrom(null);
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void FormatFromTestArgsNotNullShouldPass()
{
string expected = "germany";
string actual = "{land}".FormatFrom(new { land = "germany" });
Assert.AreEqual(expected, actual);
}
[TestMethod()]
public void FormatFromTestArgsNotNullButWrongShouldPass()
{
string expected = "{land}"; // TODO: Passenden Wert initialisieren
string actual;
actual = "{land}".FormatFrom(new { lan = "germany" });
Assert.AreEqual(expected, actual);
}Context
StackExchange Code Review Q#109858, answer score: 7
Revisions (0)
No revisions yet.