views:

221

answers:

16

I have a linq query that does something simple like:

var k = people.Select(x=>new{x.ID, x.Name});

I then want a function or linq lambda, or something that will output the names in sentence format using commas and "ands".

{1, John}
{2, Mark}
{3, George}

to

"1:John, 2:Mark and 3:George"

I'm fine with hardcoding the ID + ":" + Name part, but it could be a ToString() depending on the type of the linq query result. I'm just wondering if there is a neat way to do this with linq or String.Format().

+4  A: 

Why Linq?

StringBuilder sb = new StringBuilder();

for(i=0;i<k.Count();k++)
{
   sb.Append(String.Format("{0}:{1}", k[i].ID, k[i].Name);
   if(i + 2 < k.Count())
      sb.Append(", ");
   else if(i + 1 < k.Count())
      sb.Append(" and ");
}

Really, all Linq will let you do is hide the loop.

Also, make sure you do or do not want the "Oxford Comma"; this algorithm will not insert one, but a small change will (append the comma and space after every element except the last, and also append "and " after the next-to-last).

KeithS
I support this. The comparable LINQ is complicated for no reason. However, wouldn't it be better to do i<k.Count() - 1, and then add the last bit?
Rubys
It would probably be preferable to store the result of `k.Count()` in a local variable.
Timwi
@Rubys, Timwi: tom-ay-to, tom-ah-to. You'd also have to ToList() or ToArray() the results of the OP's query to index it, making the cardinality accessible through a member property Count or Length. As for efficiency, the else if is only evaluated if the first half is false, so it doesn't save much to take the last part out UNLESS the OP wants the Oxford comma.
KeithS
@KeithS: If tom-ay-to is O(n) and tom-ah-to is O(n²), I much prefer tom-ah-to.
Timwi
@Timwi: You prefer O(n²)? Odd.
Mark
@Timwi: Disregarding your mistype, I mentioned that you'd have to ToList() the query results to index them; Therefore, the algorithm could use Count (which has constant-time evaluation) instead of Count() (which does have linear). I used Count() in my algorithm only because, not knowing whether a ToList() or ToArray() would be used, it would work for both. Besides, as Count() does not change and is not used outside the loop's scope, I wouldn't be surprised if the compiler optimized the Count() calls with a variable anyway.
KeithS
Count() does use the Count property of IList or the Length property of Array. k currently isn't either of those, but it's not hard to make it so.
David B
@KeithS: I agree with what you said except the last part. The C# compiler does not optimize *any* common subexpressions whatsoever. The .NET JIT compiler sometimes does, but definitely not method calls.
Timwi
A: 

There are ways to optimize this since it isn't very efficient, but something like this may work:

var k = people.Select(x => new {x.ID, x.Name}).ToList();

var last = k.Last();
k.Aggregate(new StringBuilder(), (sentence, item) => { 
    if (sentence.Length > 0)
    {
        if (item == last)
            sentence.Append(" and ");
        else
            sentence.Append(", ");
    }

    sentence.Append(item.ID).Append(":").Append(item.Name);
    return sentence;
});
Matt
Input: `[ a, b, b, b ]` Output: `a and b and b and b`
Timwi
A: 

Improving(hopefully) on KeithS's answer:

string nextBit = "";
var sb = new StringBuilder();
foreach(Person person in list)
{
    sb.Append(nextBit);
    sb.Append(", ");
    nextBit = String.Format("{0}:{1}", person.ID, person.Name);
}
sb.Remove(sb.Length - 3, 2);
sb.Append(" and ");
sb.Append(nextBit);
Rubys
Ouch. Outputs `, 1:John, 2:Mar and 3:George`
Timwi
+2  A: 

Just for fun, here’s something that really uses functional LINQ — no loop and no StringBuilder. Of course, it’s pretty slow.

var list = new[] { new { ID = 1, Name = "John" },
                   new { ID = 2, Name = "Mark" },
                   new { ID = 3, Name = "George" } };

var resultAggr = list
    .Select(item => item.ID + ":" + item.Name)
    .Aggregate(new { Sofar = "", Next = (string) null },
               (agg, next) => new { Sofar = agg.Next == null ? "" :
                                            agg.Sofar == "" ? agg.Next :
                                            agg.Sofar + ", " + agg.Next,
                                    Next = next });
var result = resultAggr.Sofar == "" ? resultAggr.Next :
             resultAggr.Sofar + " and " + resultAggr.Next;

// Prints 1:John, 2:Mark and 3:George
Console.WriteLine(result);
Timwi
It looks like that `ToList()` isn't required. Nonetheless, it doesn't change the code.
Ahmad Mageed
@Ahmad: Thanks, removed :)
Timwi
A: 

This is not pretty but will do the job using LINQ

string s = string.Join(",", k.TakeWhile(X => X != k.Last()).Select(X => X.Id + ":" + X.Name).ToArray()).TrimEnd(",".ToCharArray()) + " And " + k.Last().Id + ":" + k.Last().Name;
Vivek
This won't work if your last element shows up multiple times in the list.
Gabe
The question does not specify that it should be distinct.
Vivek
A: 

Y'all are making it too complicated:

var list = k.Select(x => x.ID + ":" + x.Name).ToList();
var str = list.LastOrDefault();
str = (list.Count >= 2 ? list[list.Count - 2] + " and " : null) + str;
str = string.Join(", ", list.Take(list.Count - 2).Concat(new[]{str}));
StriplingWarrior
A: 

StringBuilder Approach

Here's an Aggregate with a StringBuilder. There's some position determinations that are made to clean up the string and insert the "and" but it's all done at the StringBuilder level.

var people = new[]
{
    new { Id = 1, Name = "John" },
    new { Id = 2, Name = "Mark" },
    new { Id = 3, Name = "George" }
};

var sb = people.Aggregate(new StringBuilder(),
             (s, p) => s.AppendFormat("{0}:{1}, ", p.Id, p.Name));
sb.Remove(sb.Length - 2, 2); // remove the trailing comma and space

var last = people.Last();
// index to last comma (-2 accounts for ":" and space prior to last name)
int indexComma = sb.Length - last.Id.ToString().Length - last.Name.Length - 2;

sb.Remove(indexComma - 1, 1); // remove last comma between last 2 names
sb.Insert(indexComma, "and ");

// 1:John, 2:Mark and 3:George
Console.WriteLine(sb.ToString());

A String.Join approach could have been used instead but the "and" insertion and comma removal would generate ~2 new strings.


Regex Approach

Here's another approach using regex that is quite understandable (nothing too cryptic).

var people = new[]
{
    new { Id = 1, Name = "John" },
    new { Id = 2, Name = "Mark" },
    new { Id = 3, Name = "George" }
};
var joined = String.Join(", ", people.Select(p => p.Id + ":" + p.Name).ToArray());
Regex rx = new Regex(", ", RegexOptions.RightToLeft);
string result = rx.Replace(joined, " and ", 1); // make 1 replacement only
Console.WriteLine(result);

The pattern is simply ", ". The magic lies in the RegexOptions.RightToLeft which makes the match occur from the right and thereby makes the replacement occur at the last comma occurrence. There is no static Regex method that accepts the number of replacements with the RegexOptions, hence the instance usage.

Ahmad Mageed
+1  A: 

Much like the rest, this isn't better than using a string builder, but you can go (ignoring the ID, you can add it in):

IEnumerable<string> names = new[] { "Tom", "Dick", "Harry", "Abe", "Bill" };
int count = names.Count();
string s = String.Join(", ", names.Take(count - 2)
                 .Concat(new [] {String.Join(" and ", names.Skip(count - 2))}));

This approach pretty much abuses Skip and Take's ability to take negative numbers, and String.Join's willingness to take a single parameter, so it works for one, two or more strings.

Kobi
A: 

This can be the way you can achieve your goal

var list = new[] { new { ID = 1, Name = "John" }, 
                   new { ID = 2, Name = "Mark" }, 
                   new { ID = 3, Name = "George" }
                 }.ToList();

int i = 0;

string str = string.Empty;

var k = list.Select(x => x.ID.ToString() + ":" + x.Name + ", ").ToList();

k.ForEach(a => { if (i < k.Count() - 1) { str = str +  a; } else { str = str.Substring(0, str.Length -2) + " and " + a.Replace("," , ""); } i++; });
Sany
Close. This returns `, 1:John, 2:Mark and 3:George`. You need to check if it's the first element and not append the comma in that case.
Ahmad Mageed
Yes, you are right, I have updated the changes
Sany
A: 

How about this?

var k = people.Select(x=>new{x.ID, x.Name});
var stringified = people
                  .Select(x => string.Format("{0} : {1}", x.ID, x.Name))
                  .ToList();
return string.Join(", ", stringified.Take(stringified.Count-1).ToArray())
       + " and " + stringified.Last();
nikie
What about the edge case of an empty list?
Hightechrider
+3  A: 
public string ToPrettyCommas<T>(
  List<T> source,
  Func<T, string> stringSelector
)
{
  int count = source.Count;

  Func<string, int> prefixSelector = x => 
    x == 0 ? "" :
    x == count - 1 ? " and " :
    ", ";

  StringBuilder sb = new StringBuilder();

  for(int i = 0; i < count; i++)
  {
    sb.Append(prefixSelector(i));
    sb.Append(stringSelector(source[i]));
  }

  string result = sb.ToString();
  return result;
}

Called with:

string result = ToPrettyCommas(people, p => p.ID.ToString() + ":" + p.Name);
David B
Ooh, I like your solution. It both carries elegance of functional languages and some pragmatic StringBuilder approach. Kudos.
gaearon
+1  A: 

Using the Select operation that gives you an index, this can be written as a ONE LINE extension method:

public static string ToAndList<T>(this IEnumerable<T> list, Func<T, string> formatter)
{
   return string.Join(" ", list.Select((x, i) => formatter(x) + (i < list.Count() - 2 ? ", " : (i < list.Count() - 1 ? " and" : ""))));
}

e.g.

var list = new[] { new { ID = 1, Name = "John" },
                   new { ID = 2, Name = "Mark" },
                   new { ID = 3, Name = "George" } }.ToList();

Console.WriteLine(list.ToAndList(x => (x.ID + ": " + x.Name)));
Hightechrider
A: 

Hi! I have refined my previous answer and I believe this is the most elegant solution yet.
However it would only work on reference types that don't repeat in the collection (or else we'd have to use different means for finding out if item is first/last).

Enjoy!

var firstGuy = guys.First();
var lastGuy = guys.Last();

var getSeparator = (Func<Guy, string>)
    (guy => {
        if (guy == firstGuy) return "";
        if (guy == lastGuy) return " and ";
        return ", ";
    });

var formatGuy = (Func<Guy, string>)
    (g => string.Format("{0}:{1}", g.Id, g.Name));

// 1:John, 2:Mark and 3:George
var summary = guys.Aggregate("",
    (sum, guy) => sum + getSeparator(guy) + formatGuy(guy));
gaearon
A: 

Here's a method that doesn't use LINQ, but is probably as efficient as you can get:

public static string Join<T>(this IEnumerable<T> list,
                             string joiner,
                             string lastJoiner = null)
{
    StringBuilder sb = new StringBuilder();
    string sep = null, lastItem = null;
    foreach (T item in list)
    {
        if (lastItem != null)
        {
            sb.Append(sep);
            sb.Append(lastItem);
            sep = joiner;
        }
        lastItem = item.ToString();
    }
    if (lastItem != null)
    {
        if (sep != null)
            sb.Append(lastJoiner ?? joiner);
        sb.Append(lastItem);
    }
    return sb.ToString();
}

Console.WriteLine(people.Select(x => x.ID + ":" + x.Name).Join(", ", " and "));

Since it never creates a list, looks at an element twice, or appends extra stuff to the StringBuilder, I don't think you can get more efficient. It also works for 0, 1, and 2 elements in the list (as well as more, obviously).

Gabe
A: 

Here's one using a slightly modified version of my answer to Eric Lippert's Challenge which is IMHO the most concise with easy to follow logic (if you're familiar with LINQ).

static string CommaQuibblingMod<T>(IEnumerable<T> items)
{
    int count = items.Count();
    var quibbled = items.Select((Item, index) => new { Item, Group = (count - index - 2) > 0})
                        .GroupBy(item => item.Group, item => item.Item)
                        .Select(g => g.Key
                            ? String.Join(", ", g)
                            : String.Join(" and ", g));
    return String.Join(", ", quibbled);  //removed braces
}

//usage
var items = k.Select(item => String.Format("{0}:{1}", item.ID, item.Name));
string formatted = CommaQuibblingMod(items);
Jeff M
A: 
static public void Linq1()
{
    var k = new[] { new[] { "1", "John" }, new[] { "2", "Mark" }, new[] { "3", "George" } };

    Func<string[], string> showPerson = p => p[0] + ": " + p[1];

    var res = k.Skip(1).Aggregate(new StringBuilder(showPerson(k.First())),
        (acc, next) => acc.Append(next == k.Last() ? " and " : ", ").Append(showPerson(next)));

    Console.WriteLine(res);
}

could be optimized by moving k.Last() computation to before the loop

Grozz