August 31, 2008

StartsWithAnyOf extension method

Here comes another great string extension for improved readability. You all know how to check if a string starts with another string (yep, it's the StartsWith method I'm talking about).

But what if you wan't to check if a string starts with any of a series of strings? Well, you'd have to do something like this.

string name = "Mr. Markus Olsson"
var l = new List<string> { "Dr", "Mr", "Ms" };
bool found;
foreach(string s in l) {
    if(name.StartsWith(l)) {
        found = true;
        break;
    }
}

Or, you could use lambdas for a much more elegant solution

string name = "Mr. Markus Olsson"
var l = new List<string> { "Dr", "Mr", "Ms" };
bool found = l.Exists(prefix => name.StartsWith(prefix));

That's pretty cool, right? The .Exists method on the list object takes a Predicate as a parameter and executes that predicate with each element as it's first argument until it finds a match. Coolio indeed but we can do better readability wise.

Enter StartsWithAnyOf extension methods

/// <summary>
/// Checks to see if the string starts with any of the supplied strings
/// </summary>
/// <param name="s">The string to check for a start value</param>
/// <param name="strings">One or more strings</param>
/// <returns>True the strings starts with any of the supplied strings, false otherwise</returns>
public static bool StartsWithAnyOf(this string s, params string[] strings)
{
    if (s == null)
        throw new ArgumentNullException("s");

    if (strings == null)
        throw new ArgumentNullException("strings");

    if (strings.Length == 0)
        throw new ArgumentOutOfRangeException("strings", "You must supply one or more strings");

    return Array.Exists(strings, (prefix => s.StartsWith(prefix)); 
}

/// <summary>
/// Checks to see if the string starts with any of the supplied strings
/// </summary>
/// <param name="s">The string to check for a start value</param>
/// <param name="strings">One or more strings</param>
/// <returns>True the strings starts with any of the supplied strings, false otherwise</returns>
public static bool StartsWithAnyOf(this string s, List<string> strings)
{
    if (s == null)
        throw new ArgumentNullException("s");

    if (strings == null)
        throw new ArgumentNullException("strings");

    if (strings.Count == 0)
        throw new ArgumentOutOfRangeException("strings", "You must supply one or more strings");

    return strings.Exists(x => s.StartsWith(x));
}

This allows us to rewrite our code to

string name = "Mr. Markus Olsson"
bool found = name.StartsWithAnyOf("Dr.", "Mr.", "Ms.");

Readability in a nutshell. Variations of these extension methods includes an override that takes a StringComparison in order to allow for case insensitive lookup. The EndsWithAnyOf method is of course also a must.

Update: as mattias pointed out there's a bit of code duplication here but that's intentional, read why
Update 2: I changed my mind again after discussing it with mattias and for the sake of readability and code-duplication I've decided to wrap the first method into a call of the second.
Update 3: James Curran pointed out that the Array class have an Exist method and that's of course what you want for the string array. Thanks James!

Licensing information

kick it on DotNetKicks.com

5 comments:

  1. Hejhej, now im not even a csharp newbie, but my eyes sees some code duplication :) could the first method that takes an array of strings just use the second metohd?

    Pseudo-c-sharp:

    public static bool StartsWithAnyOf(this string s, params string[] strings)
    {
    return s.StartWithAnyOf(strings.ToList())
    }

    ReplyDelete
  2. Spot on! Actually the original post didn't contain the duplication and used the second method as you suggest but I changed it at the last minute due to the fact that when you use the "params string[]" method you probably aren't going to be sending in more than a few strings, like this:

    s.StartsWithAnyOf("foo", "bar");

    So I just thought that the overhead of creating a new list and then creating an enumerator for looping through the strings in the list was a bit too much but I really do agree with you and I have as a matter of fact implemented it using the "code dup" way in our core library at work.

    Nice catch though, didn't think anyone would notice ;)

    ReplyDelete
  3. Actually, the way you really want it is:

    public static bool StartsWithAnyOf(this string s, params string[] strings)
    {
    // parameter checking
    return Array.Exists(strings, (prefix => s.StartsWith(prefix));
    }

    ReplyDelete
  4. Right you are James! I actually didn't know that method existed. It makes perfect sense that it would though. I've updated the post.

    Thanks!

    ReplyDelete
  5. To be sure, what you really want is
    l.Exists(p => name.StartsWith(p));

    Littering your code with extension methods like StartsWithAnyOf will probably prove to be detrimental to readability.
    Faced with (new) code, seeing
    mylocalvar.StartsWithAnyOf(list);

    I'd have to step into that extension method to make sure it does what it says (even if it's my own 6-months old code).

    If I instead see:
    l.Exists(p => name.StartsWith(p));
    I'll know exactly what it does. Thats readability!

    ReplyDelete