20

I want to create a comma separated list in C# with the word "and" as last delimiter.

string.Join(", ", someStringArray)

will result in a string like this

Apple, Banana, Pear

but instead I want it to look like this:

Apple, Banana and Pear

Is there a simple way to achieve it with Linq and without using loops?

bytecode77
  • 14,163
  • 30
  • 110
  • 141
  • Do the items have to appear in the same order they currently are, or can we re-arrange them? – Damien_The_Unbeliever Jan 02 '13 at 14:01
  • They should be in the same order. – bytecode77 Jan 02 '13 at 14:02
  • 4
    `and without using loops` -- as the answers prove, you don't need explicit loops for this. However, loops are hardly evil, and LINQ is really just generating the loops for you. – Jon B Jan 02 '13 at 14:10
  • 1
    If you're asking to do this without loops, you're going to get answers that are not very readable. – svick Jan 02 '13 at 14:12
  • I know that Linq internally generates loops. However, from the standpoint of readability I find Linq better. Once you get into LinqToSql, you can't live without it anymore ;) – bytecode77 Jan 02 '13 at 14:46

3 Answers3

30

You can do a Join on all items except the last one and then manually add the last item:

using System;
using System.Linq;

namespace Stackoverflow
{
    class Program
    {
        static void Main(string[] args)
        {
            DumpResult(new string[] { });
            DumpResult(new string[] { "Apple" });
            DumpResult(new string[] { "Apple", "Banana" });
            DumpResult(new string[] { "Apple", "Banana", "Pear" });
        }

        private static void DumpResult(string[] someStringArray)
        {
            string result = string.Join(", ", someStringArray.Take(someStringArray.Length - 1)) + (someStringArray.Length <= 1 ? "" : " and ") + someStringArray.LastOrDefault();
            Console.WriteLine(result);
        }
    }
}

As you can see, there is a check on the amount of items and decides if it's necessary to add the 'and' part.

Wouter de Kort
  • 39,090
  • 12
  • 84
  • 103
  • 1
    Nice, but what if the list contains fewer than two items? – Mark Byers Jan 02 '13 at 14:04
  • 2
    Then, we would need to use this: string days = string.Join(", ", notSentDays.Take(notSentDays.Count - 1)) + (notSentDays.Count == 1 ? "" : " and ") + notSentDays.Last(); – bytecode77 Jan 02 '13 at 14:05
  • I'd rather use the `string, string[], int, int` overload than the `IEnumerable` overload. – Rawling Jan 02 '13 at 14:09
  • If the array has one item you end up with an empty string. – juharr Jan 02 '13 at 14:16
  • @juharr nope.. I've tested it. With one item it just displays the item. Doesn't add a ',' or 'and'. – Wouter de Kort Jan 02 '13 at 14:17
  • @WouterdeKort My bad. I missed a paren when I read it. Though you might want to change that to `LastOrDefault` to avoid an exception when the collection is empty. – juharr Jan 02 '13 at 14:19
  • Really nice! I implemented it as an extension method like this: `public static string BuildSentenceFromList(this List list, string normalSeperator = ", ", string finalSeperator = " and ") { var result = string.Join(normalSeperator, list.Take(list.Count - 1)) + (list.Count <= 1 ? "" : finalSeperator) + list.LastOrDefault(); return result; }` – Adam Diament May 06 '16 at 14:10
13

One possible solution:

var items = someStringArray; // someStringArray.ToList() if not a ICollection<>
var s = string.Join(", ", items.Take(items.Count() - 1)) +
        (items.Count() > 1 ? " and " : "") + items.LastOrDefault();

Note that this statement can iterate someStringArray multiple times if it doesn't implement ICollection<string> (lists and arrays implement it). If so, create a list with your collection and perform the query on that.

digEmAll
  • 56,430
  • 9
  • 115
  • 140
2

Is there a simple way to achieve it with Linq and without using loops?

Not possible without loop. For loop will work best. LINQ queries will use multiple loops.

    string Combine (IEnumerable<string> list)
    {
        bool start = true;
        var last = string.Empty;
        String str = string.Empty;

        foreach(var item in list)
        {
            if ( !start)
            {
                str = str + " , " + item; 
                last = item;

            }
            else
            {
                str = item;
                start = false;
            } 

        }

        if (!string.IsNullOrWhiteSpace(last))
        {
            str = str.Replace( " , " + last, " and " + last);
        }

        return str;
    }
Tilak
  • 30,108
  • 19
  • 83
  • 131
  • Well, multiple loops could get a performance issue if the list was long enough. Since there are roughly 5 entries, performance does not matter. The reason I use Linq is to avoid spaghetti code. There are about 10 people working on this project, so we can't exchange performance for readability like this. But thanks for your answer. – bytecode77 Jan 02 '13 at 19:11
  • 2
    @Devils, If there are multiple entries Use `StringBuilder` instead of `String`. Otherwise this is most efficient solution than any other `LINQ` query. I always try to do everything (as it is readable) in LINQ, but when performance issues comes, It is easy to write worst code using LINQ> – Tilak Jan 03 '13 at 02:59