开发者

A better way to "comma and and-ize" an IEnumerable in C# [duplicate]

This question already has answers here: Closed 12 years ago.

Possible Duplicate:

LINQ list to sentence format (insert commas & “and”)

Imagine these inputs and results:

[] -> ""

["Hello World!"] -> "Hello World!"

["Apples", "bananas"] -> "Apples, and bananas" (put your grammar books away)

["Lions", "Tigers", "Bears"] -> "Lions, Tigers, and Bears" (oh my!)

Now, imagine that the inputs are all of IEnumerable<string>. What is a good (where good may encompass "small and tidy", "easy to understand", "uses the full ability of LINQ", or other as long as it's justified) to write a function in C# to 开发者_开发知识库do this? I would really like to avoid "imperative loop" approaches.

My current approach looks like:

string Commaize (IEnumerable<string> list) {
    if (list.Count() > 1) {
        list = list.Take(list.Count() - 2).Concat(
            new[] { list.Reverse().Take(2).Reverse()
                        .Aggregate((a, b) => a + " and " + b) });
    }
    return String.Join(", ", list.ToArray());
}

But it just doesn't feel very "good". It's for .NET3.5 so the ToArray() bit is required here. If list is null the result is UB.


Unlike other answers (except the one posted by CodeInChaos), this implementation only enumerates the input sequence once. This can be important if the cost of enumerating it is high (e.g. DB query, web service call...)

string Commaize (IEnumerable<string> list)
{
    string previous = null;
    StringBuilder sb = new StringBuilder();
    foreach(string s in list)
    {
        if (previous != null)
            sb.AppendFormat("{0}, ", previous);
        previous = s;
    }
    if (previous != null)
    {
        if (sb.Length > 0)
            sb.AppendFormat("and {0}", previous);
        else
            sb.Append(previous);
    }
    return sb.ToString();
}


string Commaize (IEnumerable<string> sequence)
{
    IList<string> list=sequence as IList<string>;
    if(list==null)
      list=sequence.ToList();
    if(list.Count==0)
      return "";
    else if(list.Count==1)
      return list.First();
    else
      return String.Join(", ", list.Take(list.Count-1).ToArray()) + " and " + list.Last();
}

The overhead of this is the allocation of a few additional arrays(one ToList() and one ToArray() call, which probably both use allocation of exponentially growing arrays, so the number of allocated arrays is larger than two).


string Commaize (IEnumerable<string> list) {
    var last = list.LastOrDefault();
    return (last != null) ?
        list.Aggregate((acc,x) => acc + ", " + (x == last ? "and " : "") + x) :
        string.Empty;
}

For a list of 10,000 strings this ran in .5 seconds (compare to Thomas' that runs in about .005 seconds). Not the fastest, but I do like the readability.

EDIT:

string Commaize (IEnumerable<string> list) {
    var enumer = list.GetEnumerator();
    if (enumer.MoveNext()) {
        var c = enumer.Current;
        return (enumer.MoveNext()) ?
        list.Aggregate((acc,x) => acc + ", " + (!enumer.MoveNext() ? "and " : "") + x) :
            c;
    }
    return string.Empty;
}

This version doesn't have the equality problem of the first,.....but at the cost of readability, which was really the only thing the first function had going for it.


Here's my implementation (using .NET 4.0) of it found in Eric Lippert's challenge:

static string CommaQuibbling<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) + "}";
}

Add the Oxford comma if you want it and remove the extra braces if you don't.


From the linked question, an extension method:

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

Edit: Note: I think we can safely assume that English sentences aren't going to cause any performance issues here with the repeated use of Count() and/or that future compilers might be able to optimize this for us. But yes, if you want to optimize it for enumerable collections that don't implement Count efficiently, you can move Count() out of the statement.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜