tags:

views:

4395

answers:

9

In many places in our code we have collections of objects, from which we need to create a comma-separated list. The type of collection varies: it may be a DataTable from which we need a certain column, or a List<Customer>, etc.

Now we loop through the collection and use string concatenation, for example:

string text = "";
string separator = "";
foreach (DataRow row in table.Rows)
{
    text += separator + row["title"];
    separator = ", ";
}

Is there a better pattern for this? Ideally I would like an approach we could reuse by just sending in a function to get the right field/property/column from each object.

+1  A: 

As an aside: The first modification I would make is to use the StringBuilder Class instead of just a String - it'll save resources for you.

Galwegian
+5  A: 
static string ToCsv<T>(IEnumerable<T> things, Func<T, string> toStringMethod)
{
    StringBuilder sb = new StringBuilder();

    foreach (T thing in things)
        sb.Append(toStringMethod(thing)).Append(',');

    return sb.ToString(0, sb.Length - 1); //remove trailing ,
}

Use like this:

DataTable dt = ...; //datatable with some data
Console.WriteLine(ToCsv(dt.Rows, row => row["ColName"]));

or:

List<Customer> customers = ...; //assume Customer has a Name property
Console.WriteLine(ToCsv(customers, c => c.Name));

I don't have a compiler to hand but in theory it should work. And as everyone knows, in theory, practice and theory are the same. In practice, they're not.

Matt Howells
This is what I was looking for, thank you!Theory is not the same as practice here, you need two overloads for the method (one for generics and one for non-generics), like in Hosam Aly's answer. Your answer is a lot easier to read, though.
Helen Toomik
+1  A: 

You could write a function that transforms a IEnumerable into a comma separated string

public string Concat(IEnumerable<string> stringList)
{
    StringBuilder textBuilder;
    string separator = String.Empty;
    foreach(string item in stringList)
    {
        textBuilder.Append(item);
        textBuilder.Append(separator);
        separator = ", ";
    }
}

You can then use Linq to query your collection/dataset/etc to provide the stringList.

Mendelt
+7  A: 
// using System.Collections;
// using System.Collections.Generic;

public delegate string Indexer<T>(T obj);

public static string concatenate<T>(IEnumerable<T> collection, Indexer<T> indexer, char separator)
{
    StringBuilder sb = new StringBuilder();
    foreach (T t in collection) sb.Append(indexer(t)).Append(separator);
    return sb.Remove(sb.Length - 1, 1).ToString();
}

// version for non-generic collections
public static string concatenate<T>(IEnumerable collection, Indexer<T> indexer, char separator)
{
    StringBuilder sb = new StringBuilder();
    foreach (object t in collection) sb.Append(indexer((T)t)).Append(separator);
    return sb.Remove(sb.Length - 1, 1).ToString();
}

// example 1: simple int list
string getAllInts(IEnumerable<int> listOfInts)
{
    return concatenate<int>(listOfInts, Convert.ToString, ',');
}

// example 2: DataTable.Rows
string getTitle(DataRow row) { return row["title"].ToString(); }
string getAllTitles(DataTable table)
{
    return concatenate<DataRow>(table.Rows, getTitle, '\n');
}
Hosam Aly
This does exactly what I wanted to achieve, thank you! Although I find lambda expressions more readable (like in Matt's solution).
Helen Toomik
I agree that lambda expressions are much more readable, but I have only used .NET 2.0 so far. I hope to learn them soon.
Hosam Aly
You might want to change the separator to be a string instead of a char, but then don't forget to change the "sb.Remove" call to be "sb.Remove(sb.Length - separator.Length, separator.Length)"
Hosam Aly
A: 

Have a look at String.Join(). You need to translate your input to an array of strings.

For example, with the input in a List<String>:

    String JoinWithComma(List<String> mystrings)
    {
        return String.Join(", ", mystrings.ToArray());
    }
gimel
Ha! I replied at the same time, seeing no-one mentioned the 'correct' way :)
leppie
But the input isn't a list of strings. Its a collection of objects or a datatable, etc. Creating a list of strings, then an array of strings, then joining is not particularly efficient or elegant.
Matt Howells
@Matt: test it, not particularly inefficient either! Definitely more elegant than you suggestion.
leppie
This ignores all the complexity of the actual situation I described in my original question and is not particularly useful.
Helen Toomik
+16  A: 
string.Join(", ", Array.Convert(somelist.ToArray(), i => i.ToString()))
leppie
nice and clean. What if someList is declared as IList? It lacks the ToArray() method.
Cristi Diaconescu
This code makes me happy. It feels nice and clean.
Brian
@brian: thanks, you made my day a little better :)
leppie
A: 

Check this one: [URL][1]
string strTest = "1,2,4,6";
string[] Nums = strTest.Split(',');
Console.Write(Nums.Aggregate((first, second) => first + "," + second));
//OUTPUT:
//1,2,4,6

Aggregate returns the same data type as the input values, i.e. as far as I can see I cannot aggregate a List<int> into a string.
Helen Toomik
A: 

I love Matt Howells answer in this post:

I had to make it into an extension:

public static string ToCsv<T>(this IEnumerable<T> things, Func<T, string> toStringMethod)

Usage: [ I am getting all the emails and turning them into a csv string for emails ]:

var list = Session.Find("from User u where u.IsActive = true").Cast<User>();

return list.ToCsv(i => i.Email);
BigBlondeViking
A: 

In .NET 4 you can just do string.Join(", ", table.Rows.Select(r => r["title"]))

Hightechrider