Reputation: 120450
Say I have a text template with a number of fields that need to be populated:
var template = "hello {$name}. you are {$age} years old. you live in {$location}"
and an IDictionary<string,string>
of values to substitute:
key | value
===================
name | spender
age | 38
location| UK
The naive way of populating the template might be something like:
var output = template;
foreach(var kvp in templValues)
{
output = output.Replace(string.format("{{${0}}}", kvp.Key), kvp.Value);
}
However, this seems painfully inefficient. Is there a better way?
Upvotes: 9
Views: 257
Reputation: 52117
If you are worried about performance, manually parsing the template in a single pass is probably the fastest you can go:
static string DictFormat(string template, IDictionary<string, string> dict) {
const string left_delimiter = "{$";
int left_delimiter_len = left_delimiter.Length;
const string right_delimiter = "}";
int right_delimiter_len = right_delimiter.Length;
var sb = new StringBuilder();
int end = 0;
while (true) {
int start = template.IndexOf(left_delimiter, end);
if (start >= 0) {
sb.Append(template.Substring(end, start - end));
start += left_delimiter_len;
end = template.IndexOf(right_delimiter, start);
if (end >= 0) {
string key = template.Substring(start, end - start);
string value;
if (dict.TryGetValue(key, out value)) {
sb.Append(value);
end += right_delimiter_len;
}
else
throw new ArgumentException(string.Format("Key not found: {0}", key), "template");
}
else
throw new ArgumentException(string.Format("Key starting at {0} not properly closed.", start), "template");
}
else {
sb.Append(template.Substring(end));
return sb.ToString();
}
}
}
Use it like this:
const string template = "hello {$name}. you are {$age} years old. you live in {$location}";
var dict = new Dictionary<string, string> { { "name", "spender" }, { "age", "38" }, { "location", "UK" } };
string result = DictFormat(template, dict);
Upvotes: 0
Reputation: 3972
You could use a Regex.Replace()
, like this:
var output = new Regex(@"\{\$([^}]+)\}").Replace(
template,
m => templValues.ContainsKey(m.Captures[1].Value)
? templValues[m.Captures[1].Value]
: m.Value);
AFAIK this will also prevent unexpected results if your dictionary is built like this, because this might produce "hello UK. you are 38 years old. you live in UK"
as well as "hello {$location}. you are 38 years old. you live in UK"
, since dictionarys don't sort their keys:
key | value
===================
name | {$location}
age | 38
location| UK
When the first behavior is actually desired, you can just run the regex multiple times.
Edit: If the template parsing is actually in a time critical part of the code, don't do the template parsing there. you should consider using the manual parsing method Sean recommended.
Upvotes: 4
Reputation: 63338
Use a regular expression that matches a field specifier:
var fieldRegex = new Regex(@"{\$([^}]+?)}", RegexOptions.Compiled);
Regex explanation:
{
$
(which has to be escaped)( )
containing:
}
characters+
?
(capturing lazily)}
Match this regex against the template, using a custom evaluator that substitutes in the relevant field value:
var template = "hello {$name}. you are {$age} years old. you live in {$location}";
var fieldValues = new Dictionary<string, string>
{
{ "name", "spender" },
{ "age", "38" },
{ "location", "UK" },
};
var output = fieldRegex.Replace(
template,
match => fieldValues[match.Groups[1].Value]);
You can break out this lambda into a method that checks if the field actually exists, if you want.
Upvotes: 1
Reputation: 62472
There's nothing wrong with your approach, it depends on the context it's being used. For example, in a tight mission-critical loop it's not the most efficient approach, but for occasional use, or in a gui it's probably ok.
A more efficient solution would be to parse the string. For example. search for the first {
and then for the next }
. The text between them is the key to lookup, which you can then substitute. You then start with the searching from the character after the }
. The advantage of this approach is that if the value you insert has an embedded token it won't be replaced. The disadvantage is that it's more difficult to handle the edge cases when parsing.
Upvotes: 4
Reputation: 809
At the risk of sounding silly, you could just write a function to return the string you want:
public string CreateString(string name, string age, string location)
{
return "hello " + name + ". you are " + age + " years old. you live in " + location;
}
Since you can only store one set of values in the dictionary, the value of using a template in this way seems diminished.
Upvotes: -2