1

I'd like to take a HashSet<String> and elegantly convert it to a string. I can iterate like so:

HashSet<String> words = new HashSet<string>() { "alpha", "beta", "delta" };

string joined = "";
foreach (var w in words) 
  joined += w + ",";

if(joined.Length > 0)
  joined = joined.SubString(0,joined.Length-1); // remove final comma

Is there a LinQ way to do this elegantly and efficiently?

The only way I can think of doing this is by converting it to an array first:

HashSet<String> words = new HashSet<string>() { "alpha", "beta", "delta" };
string joined = String.Join(",",words.ToArray());

But, then I'm doing a double conversion. Would there be some handy-dandy LinQ expression that's efficient and clear?

ANSWER 1 (from marr's idea)

public static string JoinItems(this IEnumerable<string> items, string joiner) {
    StringBuilder sb = new StringBuilder("");

    foreach (var i in items) 
        sb.AppendFormat("{0}{1}",i,joiner);

    if(sb.Length>0) 
        return sb.Remove(sb.Length - joiner.Length, joiner.Length).ToString();
    else
       return sb.ToString();
}

ANSWER 2 using an Enumerator (from Martin's solution)

public static string JoinItems<T>(this IEnumerable<T> items, string delim) {
    var sb = new StringBuilder();
    var i = items.GetEnumerator();
    if (i.MoveNext()) {
        sb.Append(i.Current);
        while (i.MoveNext()) {
            sb.Append(delim);
            sb.Append(i.Current);
        }
    }
    return sb.ToString();
}
Armstrongest
  • 14,284
  • 13
  • 61
  • 102
  • 4
    Perhaps you are unaware of the overload of string.Join that takes an enumerable? (Which includes HashSet) – Kirk Woll Sep 16 '10 at 19:33
  • 2
    @Kirk - Only 4.0 has that overload. 3.5 does not. http://msdn.microsoft.com/en-us/library/system.string.join(v=VS.90).aspx – Martin Sep 16 '10 at 19:39

3 Answers3

3

I took your method and modified it to not need to remove the last comma. I also changed the AppendFormat to just Append because it avoids all the work of parsing the format each time.

public static string JoinItems(this IEnumerable<string> items, string joiner)
{
    StringBuilder sb = new StringBuilder(); 
    string delim = "";

    foreach (var i in items)
    {
        sb.Append(delim);
        sb.Append(i);
        delim = joiner;
    }

    return sb.ToString(); 
} 
Gabe
  • 79,868
  • 10
  • 131
  • 226
  • This'll do it nicely, my library version has the signature Join(this string separator, IEnumerable items) because it mimics the python syntax. To each their own. – marr75 Sep 17 '10 at 12:57
  • Very nice! That's a great technique. – Armstrongest Sep 17 '10 at 15:49
  • Very nice. Note there's also a debate about whether StringBuilder or string.Join is faster for large sets ... even though SB is fast, it may need to grow/copy the buffer. http://stackoverflow.com/questions/585860/string-join-vs-stringbuilder-which-is-faster – Slaggg Sep 17 '10 at 21:28
  • Slaggg: You're right, `String.Join(",",words.ToArray())` might be faster for large strings, but not for short strings (and the OP already knew about it). Note that the .Net 4 `Join(IEnumerable)` uses the StringBuilder also. – Gabe Sep 17 '10 at 21:51
1

I don't see the double conversion in your String.Join() line. I see one conversion ToArray(), which is not terrible, and then it executes String.Join(), which performs well.

There is a String.Join() in .Net 4 that takes an IEnumerable that will work without the conversion. If you're using an older framework version, you can write your own extension method for string that takes a separator as the "this" parameter then joins an IEnumerable. Be sure and use stringbuilder for performance.

marr75
  • 5,583
  • 1
  • 22
  • 39
  • I'm not sure I'd phrase it as "double conversion", but it *is* "two pass". – Kirk Woll Sep 16 '10 at 19:48
  • The question is, though... is there a LinQ version that is one pass... or is it two-pass behind the scenes anyhow? Note: I realize that ToArray(), of course, is a LinQ extension method... – Armstrongest Sep 16 '10 at 19:54
  • It's not two pass with the IEnumerable overload, the only aspect of the string array that String.Join uses is enumerating over it. So like I said, write an extension method that takes an enumerable, just don't do it the way you've shown, use a stringbuilder. – marr75 Sep 16 '10 at 20:07
  • Sorry, I'm thick. I was looking at the second example I made and wondering how to implement a stringbuilder, However, I see it now... you mean take the original and use a StringBuilder. Gotcha! – Armstrongest Sep 16 '10 at 22:17
1

This will do the trick without extra copies or checks on each iteration:

String JoinItems<T>(IEnumerable<T> items) {
  var stringBuilder = new StringBuilder();
  var i = items.GetEnumerator();
  if (i.MoveNext()) {
    stringBuilder.Append(i.Current);
    while (i.MoveNext()) {
      stringBuilder.Append(", ");
      stringBuilder.Append(i.Current);
    }
  }
  return stringBuilder.ToString();
}
Martin Liversage
  • 96,855
  • 20
  • 193
  • 238
  • Thanks. I'd probably just modify it so the `", "` delimiter isn't hard-coded. (adding it as a parameter). Also, wouldn't you need to call `ToString` on `i.Current`? `stringBuilder.Append(i.Current.ToString());`. Maybe I'm wrong, but you're using generics, so I thought that it would be necessary. – Armstrongest Sep 17 '10 at 15:50
  • @Atømix: You don't have to call `ToString()` on `i.Current`. It will use the overload `StringBuilder.Append(Object)` which then will call `ToString()` on the object. – Martin Liversage Sep 17 '10 at 16:16