30

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.

Helen Toomik
  • 2,096
  • 1
  • 18
  • 17

11 Answers11

99
string.Join(", ", Array.ConvertAll(somelist.ToArray(), i => i.ToString()))
leppie
  • 115,091
  • 17
  • 196
  • 297
  • 1
    nice and clean. What if someList is declared as IList? It lacks the ToArray() method. – Cristian Diaconescu Dec 05 '08 at 16:50
  • 4
    @CristiDiaconescu: string.Join(", ", somelist.Select(t => t.ToString()).ToArray()) – cederlof Nov 03 '11 at 10:11
  • I get `System.Array does not contain a definition for 'Convert'` – Josh Stodola Jul 31 '12 at 19:47
  • @JoshStodola: Say what? You trying to compile with the JVM? ;p It has been there since 1.0. – leppie Jul 31 '12 at 19:52
  • 1
    @leppie Wrong. It is [not in 1.1](http://msdn.microsoft.com/en-US/library/system.array_methods(v=vs.71)), it's [not in 2.0](http://msdn.microsoft.com/en-US/library/system.array_methods(v=vs.80)), and it's [not in 3.5](http://msdn.microsoft.com/en-US/library/system.array_methods(v=vs.85)). I think you meant `ConvertAll` – Josh Stodola Aug 01 '12 at 23:12
  • @JoshStodola: Good point ;p Intellisense makes you lazy I guess. – leppie Aug 02 '12 at 00:43
  • What if the somelist.ToArray() contains a string that already has a "," in it? Does it not break this? How do you fix it? – Hunter Dec 12 '12 at 19:29
  • @Hunter: That is irrelevant. – leppie Dec 13 '12 at 05:00
  • Another option for .Net 3.5 and up when you have a Generic List of StringBuilder for example and you want to use Lambada expression: `code`string.Join(",", (from s in queries select s.ToString()).ToArray());`code` – Michael Jun 17 '13 at 09:17
12
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
  • 40,310
  • 20
  • 83
  • 102
  • 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 Dec 01 '08 at 12:37
  • This is the best answer, especially if you follow through with BigBlondViking's improvement in making it an extension method. Much cleaner and works for complex objects. – smdrager Mar 29 '11 at 18:39
11

I found string.Join and lambda Select<Func<>> helps to write minimum code.

List<string> fruits = new List<string>();
fruits.Add("Mango");
fruits.Add("Banana");
fruits.Add("Papaya");

string commaSepFruits = string.Join(",", fruits.Select(f => "'" + f + "'"));
Console.WriteLine(commaSepFruits);

List<int> ids = new List<int>();
ids.Add(1001);
ids.Add(1002);
ids.Add(1003);

string commaSepIds = string.Join(",", ids);
Console.WriteLine(commaSepIds);

List<Customer> customers = new List<Customer>();
customers.Add(new Customer { Id = 10001, Name = "John" });
customers.Add(new Customer { Id = 10002, Name = "Robert" });
customers.Add(new Customer { Id = 10002, Name = "Ryan" });

string commaSepCustIds = string.Join(", ", customers.Select(cust => cust.Id));
string commaSepCustNames = string.Join(", ", customers.Select(cust => "'" + cust.Name + "'"));

Console.WriteLine(commaSepCustIds);
Console.WriteLine(commaSepCustNames);

Console.ReadLine();
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Umang Patel
  • 111
  • 1
  • 2
10
// using System.Collections;
// using System.Collections.Generic;
// using System.Linq

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');
}

// example 3: DataTable.Rows without Indexer function
string getAllTitles(DataTable table)
{
    return concatenate<DataRow>(table.Rows, r => r["title"].ToString(), '\n');
}
Kami
  • 19,134
  • 4
  • 51
  • 63
Hosam Aly
  • 41,555
  • 36
  • 141
  • 182
  • 1
    This does exactly what I wanted to achieve, thank you! Although I find lambda expressions more readable (like in Matt's solution). – Helen Toomik Dec 01 '08 at 12:38
  • 1
    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 Dec 01 '08 at 13:13
  • 1
    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 Dec 01 '08 at 13:15
7

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

Ian Mercer
  • 38,490
  • 8
  • 97
  • 133
6

You could write a function that transforms a IEnumerable<string> into a comma-separated string:

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

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

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Mendelt
  • 36,795
  • 6
  • 74
  • 97
  • While Join is probably the way that I would normally go, I always like to see this idiom of using an empty separator in the first iteration and the desired separator in subsequent. It looks cleaner than always adding the separator and then having to strip it off afterwards. – wageoghe Dec 15 '10 at 16:16
  • Just noticed I was adding separators and items in the wrong order... silly me! Fixed it :-) – Mendelt Dec 26 '10 at 12:29
2

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);
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
BigBlondeViking
  • 3,853
  • 1
  • 32
  • 28
  • This is exactly what I did. You should also use return `sb.ToString().TrimEnd(',');` to remove the trailing comma. It is clearer in function and if there are no elements in the enumerable you pass it, his function would throw a cannot-be-less-than-zero-length error since 0 - 1 = -1. – smdrager Mar 29 '11 at 18:35
2

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
  • 41,475
  • 16
  • 112
  • 158
2

For collections you can use this method as well, for example:

string.Join(", ", contactsCollection.Select(i => i.FirstName));

You can select any property that you want to separate.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Sapan Ghafuri
  • 545
  • 3
  • 13
1
string strTest = "1,2,4,6";
string[] Nums = strTest.Split(',');
Console.Write(Nums.Aggregate<string>((first, second) => first + "," + second));
//OUTPUT:
//1,2,4,6
sra
  • 23,820
  • 7
  • 55
  • 89
0

Here's my favorite answer adapted to the question, and corrected Convert to ConvertAll:

string text = string.Join(", ", Array.ConvertAll(table.Rows.ToArray(), i => i["title"]));
toddmo
  • 20,682
  • 14
  • 97
  • 107