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.
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.
2 Answers 2
That's a seriously impressive answer from Heslacher. I remember doing something like this a while ago (turns out to be nearly 5 years ago) and thought I solved it completely with regexes. I've slightly modified it to use an object as it originally used a dictionary:
public static class StringExtensions
{
private static Regex _parameterReplacementRegex =
new Regex("(?<!{){(?<name>[a-zA-Z0-9]+)}(?!})",
RegexOptions.ExplicitCapture |
RegexOptions.Compiled);
public static string FormatFrom(this string text, object args)
{
if (text == null)
{
throw new ArgumentNullException("text");
}
if (args == null)
{
return ReplaceDoubleBraces(text);
}
var argsType = args.GetType();
var result = _parameterReplacementRegex.Replace(text, match =>
{
var paramName = match.Groups["name"].Value;
var propertyInfo = argsType.GetProperty(paramName, BindingFlags.Instance | BindingFlags.Public);
if (propertyInfo != null)
{
return propertyInfo.GetValue(args).ToString();
}
return "{" + paramName + "}";
});
return ReplaceDoubleBraces(result);
}
private static string ReplaceDoubleBraces(string result)
{
return Regex.Replace(result, "(\\{|\\}){2}", "1ドル");
}
}
You'll notice my Regex uses a negative lookbehind and a negative lookahead to only capture non 'esacped' curly braces. I then use a MatchEvaluator delegate (called on each match) to replace it with the value from the args object or put the string back in its original format if there is no entry in the args object (return "{" + paramName + "}";
). That could be modified to throw if you'd rather.
I'd also suggest the following additional test case:
[TestMethod()]
public void FormatFromTestArgsNullEscapesCurlyBrace()
{
string expected = "{land}";
string actual;
actual = "{{land}}".FormatFrom(null);
Assert.AreEqual(expected, actual);
}
To ensure it has consistent behaviour with string.Format
.
I'm sure that this answer is less performant but I find it easier to reason about (but I am a Regex nut).
As a final point, I'd rename the method With
or FormatWith
:)
-
\$\begingroup\$ My first version of this method was based on regex replace but I didn't notice that I can actually handle the replace/match event and put a new value already there. I used a rookie replace. This is pretty cool ;-) \$\endgroup\$t3chb0t– t3chb0t2015年11月05日 12:40:03 +00:00Commented Nov 5, 2015 at 12:40
-
\$\begingroup\$
FormatWith
vsFormatFrom
... The first version was indeedFormatWith
;-) but then I decided to turn it around and instead of trying to put all properties into a string I look which properties I want to have and pull them from the object as needed. This way I can use a big object as a source and get only a few properties. I think this might be cheaper. \$\endgroup\$t3chb0t– t3chb0t2015年11月05日 12:46:00 +00:00Commented Nov 5, 2015 at 12:46
Always check the argument which is reffered by
this
in an extension method againstnull
to early throw and return. Sure one could say it doesn't matter, because it will throw anArgumentNullException
but that would be thrown from theRegex.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 returnnull
and anNullReferenceException
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 anIndexOutOfRange
exception. This can be prevented by returning early if the length oftext
is< 3
.if the
Length
ofsubstrings
will be< 3
we can return early by returningtext
.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 < 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();
}
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);
}
-
\$\begingroup\$ I'm always forgetting about the null checks ;-) If there was always a way to speparate the workig code from the defensive one. \$\endgroup\$t3chb0t– t3chb0t2015年11月05日 11:43:02 +00:00Commented Nov 5, 2015 at 11:43
-
\$\begingroup\$ @t3chb0t aspect oriented programming / PostSharp on .NET... \$\endgroup\$Konrad Morawski– Konrad Morawski2015年11月05日 12:29:56 +00:00Commented Nov 5, 2015 at 12:29
IFormattable
interface. Jon Skeet did a great demo of it on you tube. \$\endgroup\$