License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

Thanks for your posting. It did make me decide to investigate the situation.

I still really think this is a situation that begs for recursion. But maybe you were right that substring is not a good idea. So I made this version:

publicclass MString2
{
///<summary>/// Function to compare two strings, where strA may contain wildcard characters '*' and
/// '?'. http://en.wikipedia.org/wiki/Wildcard_character
///</summary>///<paramname="strA">string which may contain wildcards, may be empty, must not be null</param>///<paramname="strB">string to compare to, no wildcard processing, may be empty, must not be null</param>///<paramname="ignoreCase">true = ignore upper/lower case, false = don't ignore case</param>///<returns>true = match, false = non-match</returns>publicstaticbool CompareWWc(string strA, string strB, bool ignoreCase)
{
if (ignoreCase)
return CompareWWc(strA.ToLower(), 0, strB.ToLower(), 0);
elsereturn CompareWWc(strA, 0, strB, 0);
}
///<summary>/// Function to compare two strings, where strA may contain wildcard characters '*' and
/// '?'. http://en.wikipedia.org/wiki/Wildcard_character
///</summary>///<paramname="strA">string which may contain wildcards, may be empty, must not be null</param>///<paramname="strB">string to compare to, no wildcard processing, may be empty, must not be null</param>///<returns>true = match, false = non-match</returns>publicstaticbool CompareWWc(string strA, string strB)
{
// Just call the private recursive version of this functionreturn CompareWWc(strA, 0, strB, 0);
}
///<summary>/// Private recursive function used by the above two public functions.
///</summary>///<paramname="strA">string which may contain wildcards, may be empty, must not be null</param>///<paramname="indexA">index into strA marking start of the string for processing purposes</param>///<paramname="strB">string to compare to, no wildcard processing, may be empty, must not be null</param>///<paramname="indexB">index into strB marking start of the string for processing purposes</param>///<returns>true = match, false = non-match</returns>privatestaticbool CompareWWc(string strA, int indexA, string strB, int indexB)
{
// Top of loop to scan across strA (and strB)for (int i = 0; indexA + i < strA.Length; i++)
{
// Special processing when we hit a '*' in strAif (strA[indexA + i] == '*')
{
// If the '*' is at the end of strA then result = true irrespective of strBif (indexA + i == strA.Length - 1)
returntrue;
// Do recursive calls to try to find a match somewhere to the right in strBfor (int j = indexB + i; j < strB.Length; j++)
if (CompareWWc(strA, indexA + i + 1, strB, j))
returntrue;
returnfalse;
}
// Normal processing for non-'*' characters in strAif (indexB + i >= strB.Length || (strA[indexA + i] != strB[indexB + i] && strA[indexA + i] != '?'))
returnfalse;
}
// We've reached the end of strA and there is no '*' in strAreturn strA.Length - indexA == strB.Length - indexB;
}
}

Then I ran some timing tests, using System.Diagnostics.Stopwatch. I put my test case with 19 calls to the function in a loop and executed it 10,000 times. I did this for my original version, your version, and my new version. I compiled the programs in Release mode.

Assuming I haven't made a mistake somewhere, here are my results for a single function call:

Now to tell you the truth, I find it very difficult to get excited about saving 100 nanoseconds at the expense of having two and a half times as many lines of code. Especially since my expected use of this function in my application will probably never exceed a couple hundred calls per day.

Anyway, thanks for getting me to think things over again and make the tests. Personally, at least in this particular case, I prefer programmer understandability to execution efficiency. I've decided to stick with my original version, since I think my second version is more difficult to understand, and the improved efficiency not worth that disadvantage.

Assuming these timings are valid, your version is three times faster than my original version, and that is pretty significant, at least in a situation were the function may be used millions times a day.

Yes, the recursive function makes it more understandable for sure. In my case I actually call it several thousands of times after certain user actions, so I'm even considering using unsafe code I also thought of a special case where your function will get a performance hit: SearchString = "--ABC-----ABC-----ABC-----lots of text (without 'at') goes here", wildcardString = "*ABC*@". In this case my function (based on Jack's) will search for the '@' character once starting from position 5 (but won't find it, because it's not there). With your function it would search for the '@' character 3 times (once starting from position 5 until the end, once from 13 and once from 21). The longer the text at the end or the more occurances of 'ABC' at the start, the greater the performance hit.

Here's my third version, where I say to hell with minimizing lines of code and try to optimize the speed. No "unsafe" code though, unless you consider "goto" to be unsafe coding.

publicclass MString
{
///<summary>/// Compare two strings, where strA may contain wildcard characters '*' and '?'.
///</summary>///<paramname="strA">string which may contain wildcards, may be empty,
/// must not be null</param>///<paramname="strB">string to compare to, no wildcard processing, may be empty,
/// must not be null</param>///<paramname="ignoreCase">true = ignore upper/lower case, false = observe case</param>///<returns>true = match, false = non-match</returns>publicstaticbool CompareWWc(string strA, string strB, bool ignoreCase)
{
if (ignoreCase)
return CompareWWc(strA.ToLower(), strB.ToLower());
elsereturn CompareWWc(strA, strB);
}
///<summary>/// Compare two strings, where strA may contain wildcard characters '*' and '?'.
////// In the comments, the word 'segment' is used to talk about the portions of strA that
/// fall between two '*' characters, or between the start of the string and the first '*'
/// or between the last '*' and the end of the string.
///</summary>///<paramname="strA">string which may contain wildcards, may be empty,
/// must not be null</param>///<paramname="strB">string to compare to, no wildcard processing, may be empty,
/// must not be null</param>///<returns>true = match, false = non-match</returns>publicstaticbool CompareWWc(string strA, string strB)
{
int starPtr = 0; // Points at the '*' in strA// This part of the code handles the first segment in strA, or the case where strA// does not contain any '*' character at all. The first segment is fairly simple to// handle because it must match from the start of strB - no need to have a sliding // match loop.// Check strB long enough so we don't need to test for hitting its end while scanningif (strB.Length >= strA.Length)
{
// Simple optimized scan of first segment of strA and comparison with strBfor (;; starPtr++)
{
if (starPtr == strA.Length)
return strA.Length == strB.Length; // No '*' in strA and no mismatchif (strA[starPtr] == '*')
goto firstSegmentMatches;
if (strA[starPtr] != strB[starPtr] && strA[starPtr] != '?')
returnfalse; // Mismatch
}
}
else
{
// When strB is shorter than strA a match is not likely. But if strA contains // enough '*' characters it is possible, so we have to give it a try.for (;; starPtr++)
{
if (strA[starPtr] == '*')
goto firstSegmentMatches;
if (starPtr == strB.Length)
returnfalse; // No '*' in strA before end of strB encounteredif (strA[starPtr] != strB[starPtr] && strA[starPtr] != '?')
returnfalse; // Mismatch
}
}
// The rest of the code handles the case where strA does contain one or more '*' // characters, and the first segment does match the start of strB.
firstSegmentMatches:
int indexA; // Start of segment in strAint indexB = starPtr; // Sliding match location in strB// Loop to process the segments in strAwhile (true)
{
// Test if next segment is last and empty
indexA = ++starPtr; // Point past '*'if (indexA == strA.Length)
returntrue; // Last segment empty - matches irrespective of strB content// Scan over the next segment in strAfor (;; starPtr++)
if (starPtr == strA.Length || strA[starPtr] == '*')
break;
// Try to find match for this segment somewhere in strBfor (;; indexB++)
{
if (starPtr - indexA > strB.Length - indexB)
returnfalse; // Mismatch if not enough characters left in strBfor (int i = indexA, j = indexB; i < starPtr; i++, j++)
if (strA[i] != strB[j] && strA[i] != '?')
goto tryStringBAgain;
goto findNextSegment; // Match found for this segment in strB
tryStringBAgain:
continue;
}
// Was that last segment? Return if so, loop if not.
findNextSegment:
indexB += starPtr - indexA; // Point past matching portion of strBif (starPtr == strA.Length)
return indexB == strB.Length; // Return if that was last segment
}
}
}

And here are my timing results (which I'm not totally sure of, I'm not used to timing code):