Dominic Sauder
Dominic Sauder

Reputation: 57

What is the best practice to resolve placeholders in a plain text?

I need to resolve a huge load of placeholders (about 250) in a plain text.

A placeholder is defined as %ThisIsAPlaceholder%, an example would be %EmailSender%.

Now it's gets a bit creepy: the code should handle case insensitive placeholders too. So, %EmailSender%, %EMAILSENDER% and %emailsender% are the same placeholder. I think that's where it gets complicated.

My first approach was the something like:

public string ResolvePlaceholders(string text)
{
    var placeholders = new IEnumerable<string>
    {
        "%EmailSender%",
        "%ErrorMessage%",
        "%ActiveUser%"
    };

    var resolvedText = text;

    foreach(var placeholder in placeholders)
    {
        if(!replacedText.Contains(placeholder))
            continue;

        var value = GetValueByPlaceholder(placeholder);

        resolvedText = resolvedText.Replace(placeholder, value);
    }


    return resolvedText;
}

But.. as you may notice, i can't handle case insesitive placeholders. Also i check for every placeholder (if it is used in the text). When using > 200 placholders in a text with about 10'000 words i think this solution is not very fast.

How can this be solved in a better way? A solution that supports case insensitive placeholders would be appreciated.

Upvotes: 1

Views: 1576

Answers (2)

Mike Zboray
Mike Zboray

Reputation: 40838

A really basic but efficient replacement scheme for your case would be something like this:

private readonly static Regex regex = new Regex("%(?<name>.+?)%");

private static string Replace(string input, ISet<string> replacements)
{
    string result = regex.Replace(input, m => {

        string name = m.Groups["name"].Value;
        string value;
        if (replacements.Contains(name))
        {
            return GetValueByPlaceholder(name);
        }
        else   
        {
            return m.Captures[0].Value;
        }
    });

    return result;
}

public static void Main(string[] args)
{
    var replacements = new HashSet<string>(StringComparer.CurrentCultureIgnoreCase)
    {
        "EmailSender", "ErrorMessage", "ActiveUser"
    };

    string text = "Hello %ACTIVEUSER%, There is a message from %emailsender%. %errorMessage%";
    string result = Replace(text, replacements);

    Console.WriteLine(result);
}

It will use a regular expression to go through the input text once. Note that we are getting case-insensitive comparisons via the equality comparer passed to the HashSet that we constructed in Main. Any unrecognized items will be ignored. For more general cases, the Replace method could take a dictionary:

private static string Replace(string input, IDictionary<string, string> replacements)
{
    string result = regex.Replace(input, m => {

        string name = m.Groups["name"].Value;
        string value;
        if (replacements.TryGetValue(name, out value))
        {
            return value;
        }
        else   
        {
            return m.Captures[0].Value;
        }
    });

    return result;
}

A typical recommendation when matching using quantifiers on input from an untrusted source (e.g. users over the internet) is to specify a match timeout for the regular expression. You would have to catch the RegexMatchTimeoutException that is thrown and do something in that case.

Upvotes: 3

IdahoSixString
IdahoSixString

Reputation: 643

Regex solution

private static string ReplaceCaseInsensitive(string input, string search, string replacement)
{
    string result = Regex.Replace(
        input,
        Regex.Escape(search), 
        replacement.Replace("$","$$"), 
        RegexOptions.IgnoreCase
    );
    return result;
}

Non regex solution

public static string Replace(this string str, string old, string @new, StringComparison comparison)
{
    @new = @new ?? "";
    if (string.IsNullOrEmpty(str) || string.IsNullOrEmpty(old) || old.Equals(@new, comparison))
    return str;
    int foundAt;
    while ((foundAt = str.IndexOf(old, 0, StringComparison.CurrentCultureIgnoreCase)) != -1)
    str = str.Remove(foundAt, old.Length).Insert(foundAt, @new);
    return str;
}

Seems like a duplicate question / answer String.Replace ignoring case

Upvotes: 0

Related Questions