views:

57

answers:

3

I need a case-insensitive Replace method for the StringBuilder class. The code should work with the existing StringBuilder. An extension method implementation would be nice.

Following is how I plan to use the method:

    [TestMethod]
    public void StringBuilder_Replace_TTD() {

        StringBuilder oRequestText = new StringBuilder(File.ReadAllText("Customer.xml"));

        Customer oCustomer = new Customer(null);

        foreach (FieldIndex iField in Enum.GetValues(typeof(FieldIndex))) {

            oRequestText.Replace("{" iField.ToString() + "}", oCustomer[iField]);

        }

        Debug.WriteLine(oRequestText.ToString());
    }
+1  A: 

From: http://blogs.msdn.com/b/btine/archive/2005/03/22/400667.aspx

string Replace( string expr, string find, string repl, bool bIgnoreCase )  
{
// Get input string length
       int exprLen = expr.Length;
       int findLen = find.Length;

       // Check inputs    
       if( 0 == exprLen || 0 == findLen || findLen > exprLen )    
              return expr;

       // Use the original method if the case is required    
       if( !bIgnoreCase )
              return expr.Replace( find, repl );

       StringBuilder sbRet = new StringBuilder( exprLen );

       int pos = 0;              
       while( pos + findLen <= exprLen )    
       {    
              if( 0 == string.Compare( expr, pos, find, 0, findLen, bIgnoreCase ) )    
              {    
                     // Add the replaced string    
                     sbRet.Append( repl );    
                     pos += findLen;    
                     continue;    
              }

              // Advance one character    
              sbRet.Append( expr, pos++, 1 );    
       }

       // Append remaining characters    
       sbRet.Append( expr, pos, exprLen-pos );

       // Return string    
       return sbRet.ToString();    
}
NinjaCat
Shame there are no test cases... I feel hesitant to upvote a piece of code so complicated without any indication that it works. Also, this seems to be O(n^2). Wouldn't it be better to use `IndexOf` with `StringComparison.CurrentCultureIgnoreCase` for example?
Mark Byers
Uses a string and returns a string. I want a method that works with a StringBuilder, during the replacement without creating new strings or new string-builders.
AMissico
@Mark - it's from MSDN, so that means it _has_ to be correct, right? :)@AMissico - OK - now I see your point... let me try a few things.
NinjaCat
@AMissico: "Uses a string and returns a string" Yep - and that's exactly how it should be. The StringBuilder is an implementation detail and shouldn't be part of the interface to the method.
Mark Byers
+1 I tested this a bit and it seems to at least work correctly, if not fast.
Mark Byers
@Mark Byers: Sorry, I do not know what you are referring to. The answer is using a String and I want a replace that works with a StringBuilder so I can use it as an Extension to the StringBuilder class.
AMissico
@Mark Byers: No, definitely not fast. See *Fastest C# Case Insenstive String Replace* at http://www.codeproject.com/KB/string/fastestcscaseinsstringrep.aspx
AMissico
@Amissico: That ReplaceEx method seems reasonably fast but I don't particularly like the use of Upper and Lower to simulate case insensitive matching.
Mark Byers
+2  A: 

StringBuilder doesn't support using an IComparer when search/replacing text (in fact, there is no search support at all). You could try rolling a character-by-character version, but that will be complicated and may still perform poorly.

Based on your use case, I would suggest using a string rather than StringBuilder, and using string.IndexOf() to locate the positions in the input string where you are going to do replacement - which support case insensitive search. Once you've located all of the replacement regions, create a StringBuilder and then copy each region - replacing found text with the desired replacement values.

EDIT: Presumably you are looking to use replacement with a StringBuilder to avoid allocating additional strings and incurring the performance hit for doing so. However, replacing text within the buffer of a StringBuilder could actually be more expensive - particularly if the replacement strings are of different length than the source string they are replacing. Each replacement requires that characters be shifted forward or backwards depending on whether the replacing text is shorter or longer. Performing memory block moves like this will be expensive.

LBushkin
I have method for string, but I want for StringBuilder because I will be making 40+ replacements. I would rather not create 40+ strings.
AMissico
@AMissico: I'm not proposing that you do that. I'm suggesting that you *start* with a regular string in order to take advantage of the case-insensitive search methods. Once you've located the *indexes and lengths* of all of the substrings to be replaced, you would selectively copy from the original string into a `StringBuilder` and perform replacement for each substring that was found. It's also more efficient than replacement within a StringBuilder because it won't require *moving any blocks of text*, which would happen if replacements are a different length than the text they are replacing.
LBushkin
@LBushkin: I believe I understand what you are suggesting. Yet, I cannot see how to avoid creating the 40+ string to find the *indexes and lengths* for each of the enum members. Performance is not my main concern. For this specific case, I am more interest in clean code and production ready code.
AMissico
@AMissico: If you aren't concerned about performance, then my advice to you is to keep things simple. Don't worry about the intermediate replacement strings - use `string.Replace()`. Any solution that involves incremental search and composition (as when using a StringBuilder) will be more complicated and harder to maintain than the naive version.
LBushkin
@LBushkin: Yes, that is what I am doing now, but I am using `Microsoft.VisualBasic.Strings.Replace` instead of `String.Replace`. The function is extremely fast, supports and case-insensitive. I guess I was just thinking out loud.
AMissico
A: 

In your example you want to do (in pseudo code):

StringBuilder oRequestText = ...;
For all fields do replace on oRequestText;
Debug.WriteLine(oRequestText.ToString());

Since you're not actually using the StringBuilder after this there is no functional difference with

StringBuilder oRequestText = ...;
string strRequest = oRequestText.ToString();
For all fields do replace on strRequest;
Debug.WriteLine(strRequest);

The normal replace functions for strings should easily support what you want to do.

I'm assuming in your real situation you do want to use the StringBuilder again. But it's probably still easiest to do a .ToString(), replace on the string, and then reload the StringBuilder with the string.

Stefan