40

I have a string on which I need to do some replacements. I have a Dictionary<string, string> where I have search-replace pairs defined. I have created following extension methods to perform this operation:

public static string Replace(this string str, Dictionary<string, string> dict)
{
    StringBuilder sb = new StringBuilder(str);

    return sb.Replace(dict).ToString();
}

public static StringBuild Replace(this StringBuilder sb, 
    Dictionary<string, string> dict)
{
    foreach (KeyValuePair<string, string> replacement in dict)
    {
        sb.Replace(replacement.Key, replacement.Value);
    }

    return sb;
}

Is there a better way of doing that?

RaYell
  • 69,610
  • 20
  • 126
  • 152

8 Answers8

56

If the data is tokenized (i.e. "Dear $name$, as of $date$ your balance is $amount$"), then a Regex can be useful:

static readonly Regex re = new Regex(@"\$(\w+)\$", RegexOptions.Compiled);
static void Main() {
    string input = @"Dear $name$, as of $date$ your balance is $amount$";

    var args = new Dictionary<string, string>(
        StringComparer.OrdinalIgnoreCase) {
            {"name", "Mr Smith"},
            {"date", "05 Aug 2009"},
            {"amount", "GBP200"}
        };
    string output = re.Replace(input, match => args[match.Groups[1].Value]);
}

However, without something like this, I expect that your Replace loop is probably about as much as you can do, without going to extreme lengths. If it isn't tokenized, perhaps profile it; is the Replace actually a problem?

Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • Great answer. I think your proposal will actually be better then iterating over the whole dictionary as the regex will only replace tokens that were found. It won't check for every one that can possibly be inside. So if I have a big dictionary and small number of tokens in the input string that actually can give my app a boost. – RaYell Aug 05 '09 at 08:28
  • Very useful. I refactored it as an extension method for Regex, which I can't show in a comment so will add as a somewhat redundant extra answer below. – Francis Norton Mar 20 '13 at 16:20
  • 2
    This will throw an exception if the key is not found. – Axel Jan 15 '17 at 05:54
  • 1
    Do we have any solution if the key is not found? – Phoenix_uy May 27 '19 at 20:38
  • 4
    @Phoenix_uy `match => args.TryGetValue(match.Groups[1].Value, out var val) ? val : "*whatever*"` – Marc Gravell May 28 '19 at 07:49
46

Do this with Linq:

var newstr = dict.Aggregate(str, (current, value) => 
     current.Replace(value.Key, value.Value));

dict is your search-replace pairs defined Dictionary object.

str is your string which you need to do some replacements with.

Mike G
  • 4,232
  • 9
  • 40
  • 66
Allen Wang
  • 978
  • 8
  • 12
  • My *hunch*, based on Linq in general, is that this is slightly slower than the foreach. It's also not *that* much shorter ... but I personally love the brevity and find it readable, I just implemented this solution. Thanks. – HaileyStorm Dec 04 '22 at 17:01
  • No tokenize in my case. Love this one. Thanks Allen. Still usefull 12 years later :D – Oliver Voutat Apr 05 '23 at 10:08
10

Seems reasonable to me, except for one thing: it's order-sensitive. For instance, take an input string of "$x $y" and a replacement dictionary of:

"$x" => "$y"
"$y" => "foo"

The results of the replacement are either "foo foo" or "$y foo" depending on which replacement is performed first.

You could control the ordering using a List<KeyValuePair<string, string>> instead. The alternative is to walk through the string making sure you don't consume the replacements in further replace operations. That's likely to be a lot harder though.

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
4

Here's a lightly re-factored version of @Marc's great answer, to make the functionality available as an extension method to Regex:

static void Main() 
{
    string input = @"Dear $name$, as of $date$ your balance is $amount$";
    var args = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    args.Add("name", "Mr Smith");
    args.Add("date", "05 Aug 2009");
    args.Add("amount", "GBP200");

    Regex re = new Regex(@"\$(\w+)\$", RegexOptions.Compiled);
    string output = re.replaceTokens(input, args);

    // spot the LinqPad user // output.Dump();
}

public static class ReplaceTokensUsingDictionary
{
    public static string replaceTokens(this Regex re, string input, IDictionary<string, string> args)
    {
        return re.Replace(input, match => args[match.Groups[1].Value]);
    }
}
Francis Norton
  • 674
  • 1
  • 8
  • 16
2

when using Marc Gravell's RegEx solution, first check if a token is available using i.e. ContainsKey, this to prevent KeyNotFoundException errors :

string output = re.Replace(zpl, match => { return args.ContainsKey(match.Groups[1].Value) ? arg[match.Groups[1].Value] : match.Value; });

when using the following slightly modified sample code (1st parameter has different name):

    var args = new Dictionary<string, string>(
        StringComparer.OrdinalIgnoreCase) 
        {
            {"nameWRONG", "Mr Smith"},
            {"date", "05 Aug 2009"},
            {"AMOUNT", "GBP200"}
        };

this produces the following:

"Dear $name$, as of 05 Aug 2009 your balance is GBP200"

DannyB
  • 172
  • 2
  • 6
2

The correct tool for this particular task is Mustache, a simple standard logicless templating language.

There are many implementations. For this example, I'm going to use stubble:

var stubble = new StubbleBuilder().Build();
var dataHash = new Dictionary<string, Object>()
{
    {"Foo","My Foo Example"},
    {"Bar",5}
};

var output = stubble.Render(
   "Hey, watch me replace this: {{Foo}} ... with example text.  Also {{bar}} is 5"
   , dataHash
);
Pxtl
  • 880
  • 8
  • 18
0

Here you are:

public static class StringExm
{
    public static String ReplaceAll(this String str, KeyValuePair<String, String>[] map)
    {
        if (String.IsNullOrEmpty(str))
            return str;

        StringBuilder result = new StringBuilder(str.Length);
        StringBuilder word = new StringBuilder(str.Length);
        Int32[] indices = new Int32[map.Length];

        for (Int32 characterIndex = 0; characterIndex < str.Length; characterIndex++)
        {
            Char c = str[characterIndex];
            word.Append(c);

            for (var i = 0; i < map.Length; i++)
            {
                String old = map[i].Key;
                if (word.Length - 1 != indices[i])
                    continue;

                if (old.Length == word.Length && old[word.Length - 1] == c)
                {
                    indices[i] = -old.Length;
                    continue;
                }

                if (old.Length > word.Length && old[word.Length - 1] == c)
                {
                    indices[i]++;
                    continue;
                }

                indices[i] = 0;
            }

            Int32 length = 0, index = -1;
            Boolean exists = false;
            for (int i = 0; i < indices.Length; i++)
            {
                if (indices[i] > 0)
                {
                    exists = true;
                    break;
                }

                if (-indices[i] > length)
                {
                    length = -indices[i];
                    index = i;
                }
            }

            if (exists)
                continue;

            if (index >= 0)
            {
                String value = map[index].Value;
                word.Remove(0, length);
                result.Append(value);

                if (word.Length > 0)
                {
                    characterIndex -= word.Length;
                    word.Length = 0;
                }
            }

            result.Append(word);
            word.Length = 0;
            for (int i = 0; i < indices.Length; i++)
                indices[i] = 0;
        }

        if (word.Length > 0)
            result.Append(word);

        return result.ToString();
    }
}
Albeoris
  • 109
  • 1
  • 7
  • 2
    You could add some explanation or commentary to your answer, so that readers don't have to closely inspect the code to understand what you are proposing. Especially because this is quite a long snippet, much longer than the other proposed solutions. – Fabio says Reinstate Monica Apr 21 '16 at 23:59
  • 1
    It does what the author wanted to. Unlike an other, it does not use regular expressions, and passes through the line only once, which is important when you need to produce a few tens of replacements. – Albeoris Apr 23 '16 at 07:10
  • This looks like it ignores word boundaries and seems to be very error prone. On the other hand, no explanation is present, so I may be wrong. – Uwe Keim May 02 '16 at 11:50
  • Yeah, there is no check of the word boundaries. – Albeoris May 08 '16 at 00:16
0

Why not just check whether such key exists?

  • if exists, then remove the pair, otherwise, skip this step;

  • add the same key, but now with newly desired value.

      // say, you have the following collection
      var fields = new Dictionary<string, string>();
      fields.Add("key1", "value1");
      fields.Add("key2", "value2");
      fields.Add("key3", "value3");
    
      // now, you want to add a pair "key2"/"value4"
      // or replace current value of "key2" with "value4"
      if (fields.ContainsKey("key2"))
      {
          fields.Remove("key2");
      }
      fields.Add("key2", "value4");
    
Mike O.
  • 61
  • 6