9

In C# there is a string interpolation support like this:

$"Constant with {Value}"

which will format this string using in-scope variable Value.

But the following won't compile in current C# syntax.

Say, I have a static Dictionary<string, string> of templates:

templates = new Dictionary<string, string>
{
    { "Key1", $"{Value1}" },
    { "Key2", $"Constant with {Value2}" }
}

And then on every run of this method I want to fill in the placeholders:

public IDictionary<string, string> FillTemplate(IDictionary<string, string> placeholderValues)
{
    return templates.ToDictionary(
        t => t.Key,
        t => string.FormatByNames(t.Value, placeholderValues));
}

Is it achievable without implementing Regex parsing of those placeholders and then a replace callback on that Regex? What are the most performant options that can suit this method as being a hot path?

For example, it is easily achievable in Python:

>>> templates = { "Key1": "{Value1}", "Key2": "Constant with {Value2}" }
>>> values = { "Value1": "1", "Value2": "example 2" }
>>> result = dict(((k, v.format(**values)) for k, v in templates.items()))
>>> result
{'Key2': 'Constant with example 2', 'Key1': '1'}
>>> values2 = { "Value1": "another", "Value2": "different" }
>>> result2 = dict(((k, v.format(**values2)) for k, v in templates.items()))
>>> result2
{'Key2': 'Constant with different', 'Key1': 'another'}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
Sergey Ionov
  • 105
  • 5
  • Internally, Python's `format` must be using a Regular Expression, what do you have against using one? – NetMage Apr 27 '18 at 22:24
  • My initial implementation is using Regex and is slow, showing high contention on Regex matching. I surely could leave it like that, but this method is the hot path. – Sergey Ionov Apr 27 '18 at 22:45

3 Answers3

5

No, it's not possible, instead you should use String.Format.

With String format your string template would look like string template = "The temperature is {0}°C." and then to insert the value you could just:

decimal temp = 20.4m;
string s = String.Format(template, temp);

As shown in the Microsoft examples.

Andrew D. Bond
  • 902
  • 1
  • 11
  • 11
Leonardo Menezes
  • 496
  • 5
  • 11
  • 1
    string interpolation uses string.format essentially .. AFAIK – Rahul Apr 27 '18 at 22:17
  • From your example, it moves towards positional format. Notice that the template dictionary had `{Value1}` and `{Value2}` placeholders in different values, so they both look like `{0}`, but should apply different values from placeholderValues dictionary. I guess I could have `Tuple("{0}", new[] { "Value1" })` and then use it to prepare a formatting array. Is there a cleaner way? – Sergey Ionov Apr 27 '18 at 22:25
  • I don’t think there’s a cleaner way, you must declare both the template and the object array somehow. – Leonardo Menezes Apr 27 '18 at 22:35
3

Using an extension method that does a substitution based on regular expressions, I get a good speed up over using multiple Replace calls for each value.

Here is my extension method for expanding brace surrounded variables:

public static class ExpandExt {
    static Regex varPattern = new Regex(@"{(?<var>\w+)}", RegexOptions.Compiled);
    public static string Expand(this string src, Dictionary<string, string> vals) => varPattern.Replace(src, m => vals.TryGetValue(m.Groups[1].Value, out var v) ? v : m.Value);
}

And here is the sample code using it:

var ans = templates.ToDictionary(kv => kv.Key, kv => kv.Value.Expand(values));

Over 10,000 repeating expansions with values at 18 entries and typically only one replacement, I get 3x faster than multiple String.Replace calls.

NetMage
  • 26,163
  • 3
  • 34
  • 55
  • Use `RegexOptions.Compiled`. You'll probably get a speed up as well – pinkfloydx33 Apr 28 '18 at 00:43
  • 1
    @pinkfloydx33 Changes from about 3x faster to about 3.5x faster. – NetMage Apr 30 '18 at 15:56
  • My Regex was a bit different (`\\{([^}]*)\\}`), and it didn't have the Compiled flag. So Regex is actually better than string Replace. I'll mark this accepted. – Sergey Ionov May 04 '18 at 16:43
  • 1
    This is a great way to solve it, however, the code sample was not complete to try it out instantly - so I am providing a .Net fiddle **[here](https://dotnetfiddle.net/oJEpbz)** for those, who are interested. – Matt Jun 04 '18 at 15:21
1

I think your best alternative to Regular Expressions is doing Replace on each possible Value key, but which is faster would depend on how many values you have and how common replacements for those values are.

var templates = new Dictionary<string, string> {
    { "Key1", "{Value1}" },
    { "Key2", "Constant with {Value2}" }
};

var values = new Dictionary<string, string> { { "Value1", "1" }, { "Value2", "example 2" } };

var ans = templates.ToDictionary(kv => kv.Key, kv => values.Aggregate(kv.Value, (s, v) => s.Replace($"{{{v.Key}}}", v.Value)));

Note that a foreach instead of Aggregate would be marginally faster, again depending on how many entries are in values. Also, pre-building new key and values Arrays with key already surrounded by braces can bring about a 4x speedup, but we are talking about milliseconds in your example.

var subkeys = values.Select(kv => $"{{{kv.Key}}}").ToArray();
var subvals = values.Select(kv => kv.Value).ToArray();

var ans4 = templates.ToDictionary(kv => kv.Key, kv => {
    var final = kv.Value;
    for (int j1 = 0; j1 < subkeys.Length; ++j1)
        final = final.Replace(subkeys[j1], subvals[j1]);
    return final;
});

Of course, if your values array changes with each method call, using two Lists with braces around the keys would be a better storage structure as translating every time will eat up the time savings.

NetMage
  • 26,163
  • 3
  • 34
  • 55
  • `values` dictionary is expected to have 15-20 keys. Executed your Aggregate example on a unit test - got around ten times speed up and no contentions in profiling (comparing to Regex). Looking into this direction. – Sergey Ionov Apr 27 '18 at 22:42
  • @SergeyIonov Running a Regex on an expanded example using a `values` with 18 keys without braces versus `Aggregate` and it was 3X faster. I'll post it as another answer. – NetMage Apr 27 '18 at 22:59