HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Named string interpolation

Submitted by: @import:stackexchange-codereview··
0
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.

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 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.