Refactor ForEach to Join

by KodefuGuru 6. July 2009 19:02

Have you ever noticed the goofy logic that must be used to do simple things like creating a comma-delimited string?

public string CommaDelimit(ICollection<string> strings)
{
    StringBuilder sb = new StringBuilder();
    bool first = true;
    foreach (string s in strings)
    {
        if (first)
            first = false;
        else
            sb.Append(',');

        sb.Append(s);
    }
    return sb.ToString();
}

This method should be hidden away in a static method, and in fact this kind of logic is hidden away in a static method. String.Join() will create a properly delimited string for you. However, it only works with a string array, but as in the example above, oftentimes we’re receiving another type of enumerable.

Luckily, LINQ comes with an extension method to convert IEnumerable<T> to T[]: ToArray().

Let me repeat that since the markup makes it difficult to read. LINQ comes with an extension method to convert a generic IEnumerable of a type to an array of that type: ToArray().

public string CommaDelimit(ICollection<string> strings)
{
    return String.Join(",", strings.ToArray());
}

It’s amazing how much ToArray() can simplify a method. In fact, it’s so simple, I would argue that the method to convert the collection to a comma-delimited string is no longer necessary.

Of course, there are more complex situations. How do you handle a collection of non-primitive objects?

public string Names(ICollection<Customer> customers)
{
    StringBuilder sb = new StringBuilder();
    bool first = true;
    foreach (Customer customer in customers)
    {
        if (first)
            first = false;
        else
            sb.Append(',');

        sb.Append(String.Format("{0} {1}", 
            customer.FirstName, customer.LastName));
    }
    return sb.ToString();
}

In this example, the method takes in an ICollection of Customer and returns a comma-delimited list of names. A simple ToArray() is not enough. We must retrieve the data from the object and format it prior to making it comma-delimited.

public string Names(ICollection<Customer> customers)
{
    var names = from c in customers
                select String.Format("{0} {1}",
                c.FirstName, c.LastName);

    return String.Join(",", names.ToArray());
}

A simple LINQ statement does the trick! Of course, this being C#, there is an alternate way of writing this code.

public string Names(ICollection<Customer> customers)
{
    return String.Join(",", 
        customers.Select(c => String.Format("{0} {1}", 
            c.FirstName, c.LastName)).ToArray());
}

I prefer the LINQ syntax to the lambda expression in this situation due to improved readability. However, both simplify the statements required to create a comma-delimited string based on data from a list of objects. Keep this in your arsenal for the next time you’re confronted with refactoring goofy logic from the 2.0 days of C#.

Update

I missed another way of doing this for complex objects: Aggregate. Thanks to Richard Mason for calling it to my attention.

public string Names(ICollection<Customer> customers)
{
    return customers.Select(c => c.FirstName + " " + c.LastName)
    .Aggregate((a, b) => a + "," + b); 
}

Tags: , ,

Kodefu

Comments

7/6/2009 7:04:09 PM #

trackback

Trackback from DotNetKicks.com

Refactor ForEach to Join

DotNetKicks.com

7/6/2009 7:07:19 PM #

trackback

Trackback from DotNetShoutout

Refactor ForEach to Join

DotNetShoutout

7/7/2009 12:26:38 AM #

Richard Mason

Or you could stay entirely in LINQ and use Aggregate:

customers.Select(c => c.FirstName + " " + c.LastName)
.Aggregate((a, b) => a + "," + b);


Which should also maintain lazy semantics I believe (though I haven't confirmed) and is usable with non-string lists.

Richard Mason Australia

7/7/2009 3:30:30 AM #

pingback

Pingback from blog.cwa.me.uk

Reflective Perspective - Chris Alcock  » The Morning Brew #384

blog.cwa.me.uk

7/7/2009 7:28:09 AM #

Will Sullivan

((IEnumerable<SomeObject>)foo).Select(x=>x.ToString).ToArray() is another option.

Will Sullivan

7/7/2009 1:54:44 PM #

chris

Will: wouldn't that require overriding the .ToString() method on the object in question?

Richard: I didn't consider Aggregate. I'll test it out and update the post if it works out.

chris United States

7/8/2009 9:17:08 PM #

Daniel Pratt

Another vote for Aggregate. I find myself using it quite a bit these days. Notably, there are some overloads of Aggregate that make it very convenient to use StringBuilder. For example:

    return customers.Aggregate(
        new StringBuilder(),
        (sb, c) => sb.AppendFormat("{0} {1},", c.FirstName, c.LastName),
        (sb) => sb.Remove(sb.Length - 1, 1).ToString());

Daniel Pratt United States

7/9/2009 8:49:50 AM #

John Berham

Meh. Join all strings with a comma in front, then return substring from 2nd character.

John Berham United States

7/9/2009 11:35:09 AM #

chris

I'm not a fan of tricks to remove a beginning or trailing comma, and it's one of the reasons I decided to refactor some otheres code. I think I put that kind of goofy logic in a helper method back in the 2.0 days.

chris United States

7/10/2009 6:17:25 PM #

trackback

Trackback from #.think.in

#.think.in infoDose #36 (4th July - 11th July)

#.think.in

Add comment




  Country flag

biuquote
  • Comment
  • Preview
Loading



Powered by BlogEngine.NET 1.6.0.0
Theme by Mads Kristensen

Whois KodefuGuru

Chris Eargle

Chris Eargle
.NET Community Champion

LinkedIn Twitter Technorati Facebook

MVP - Visual C#

 

INETA Community Champions
Friend of RedGate
Telerik .NET Ninja
Community blogs & blog posts

I am a #52er


World Map

RecentComments

Comment RSS

Tag cloud

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2010