Эрик Липперт бросает вызов "запятой", лучший ответ?

Я хотел довести этот вызов до сведения сообщества stackoverflow. Оригинальная проблема и ответы здесь. Кстати, если вы раньше не следовали этому, вы должны попытаться прочитать блог Эрика, это чистая мудрость.

Резюме:

Напишите функцию, которая принимает ненулевой IEnumerable и возвращает строку со следующими характеристиками:

  • Если последовательность пуста, результирующая строка "{}".
  • Если последовательность представляет собой отдельный элемент "ABC" , тогда результирующая строка будет "{ABC}".
  • Если последовательность представляет собой последовательность двух элементов "ABC" , "DEF", тогда результирующая строка "{ABC и DEF}".
  • Если последовательность имеет более двух элементов, например, "ABC" , "DEF", "G", "H", тогда результирующая строка "{ABC, DEF, G и H}". (Примечание: нет Оксфордской запятой!)

Как вы можете видеть даже наш собственный Джон Скит (да, хорошо известно, что он может быть в двух местах одновременно) опубликовал решение но его (IMHO) не самый изящный, хотя, вероятно, вы не можете победить его производительность.

Как вы думаете? Там довольно хорошие варианты. Мне очень нравится одно из решений, которое включает в себя методы выбора и совокупности (от Fernando Nicolet). Linq очень эффективен и посвящает некоторое время таким сложным задачам, чтобы вы многому научились. Я немного перекрутил его, чтобы он был более реалистичным и понятным (используя Count и избегая Reverse):

public static string CommaQuibbling(IEnumerable<string> items)
{
    int last = items.Count() - 1;
    Func<int, string> getSeparator = (i) => i == 0 ? string.Empty : (i == last ? " and " : ", ");
    string answer = string.Empty;

    return "{" + items.Select((s, i) => new { Index = i, Value = s })
                      .Aggregate(answer, (s, a) => s + getSeparator(a.Index) + a.Value) + "}";
}

Ответ 1

Как насчет этого подхода? Чисто кумулятивный - нет обратного отслеживания и только повторяет один раз. Для сырой производительности я не уверен, что вы будете лучше работать с LINQ и т.д., Независимо от того, насколько "симпатичным" может быть ответ LINQ.

using System;
using System.Collections.Generic;
using System.Text;

static class Program
{
    public static string CommaQuibbling(IEnumerable<string> items)
    {
        StringBuilder sb = new StringBuilder('{');
        using (var iter = items.GetEnumerator())
        {
            if (iter.MoveNext())
            { // first item can be appended directly
                sb.Append(iter.Current);
                if (iter.MoveNext())
                { // more than one; only add each
                  // term when we know there is another
                    string lastItem = iter.Current;
                    while (iter.MoveNext())
                    { // middle term; use ", "
                        sb.Append(", ").Append(lastItem);
                        lastItem = iter.Current;
                    }
                    // add the final term; since we are on at least the
                    // second term, always use " and "
                    sb.Append(" and ").Append(lastItem);
                }
            }
        }
        return sb.Append('}').ToString();
    }
    static void Main()
    {
        Console.WriteLine(CommaQuibbling(new string[] { }));
        Console.WriteLine(CommaQuibbling(new string[] { "ABC" }));
        Console.WriteLine(CommaQuibbling(new string[] { "ABC", "DEF" }));
        Console.WriteLine(CommaQuibbling(new string[] {
             "ABC", "DEF", "G", "H" }));
    }
}

Ответ 2

Неэффективен, но я думаю, ясно.

public static string CommaQuibbling(IEnumerable<string> items)
{
    List<String> list = new List<String>(items);
    if (list.Count == 0) { return "{}"; }
    if (list.Count == 1) { return "{" + list[0] + "}"; }

    String[] initial = list.GetRange(0, list.Count - 1).ToArray();
    return "{" + String.Join(", ", initial) + " and " + list[list.Count - 1] + "}";
}

Если бы я поддерживал код, я предпочел бы это более умные версии.

Ответ 3

Если бы я делал много с потоками, для которых требовалась первая/последняя информация, у меня было бы расширение:

[Flags]
public enum StreamPosition
{
   First = 1, Last = 2
}

public static IEnumerable<R> MapWithPositions<T, R> (this IEnumerable<T> stream, 
    Func<StreamPosition, T, R> map)
{
    using (var enumerator = stream.GetEnumerator ())
    {
        if (!enumerator.MoveNext ()) yield break ;

        var cur   = enumerator.Current   ;
        var flags = StreamPosition.First ;
        while (true)
        {
            if (!enumerator.MoveNext ()) flags |= StreamPosition.Last ;
            yield return map (flags, cur) ;
            if ((flags & StreamPosition.Last) != 0) yield break ;
            cur   = enumerator.Current ;
            flags = 0 ;
        }
    }
}

Тогда самым простым (а не самым быстрым, что потребуется несколько более удобных методов расширения) будет:

public static string Quibble (IEnumerable<string> strings)
{
    return "{" + String.Join ("", strings.MapWithPositions ((pos, item) => (
       (pos &  StreamPosition.First) != 0      ? "" : 
        pos == StreamPosition.Last   ? " and " : ", ") + item)) + "}" ;
}

Ответ 4

Здесь в качестве Python один вкладыш


>>> f=lambda s:"{%s}"%", ".join(s)[::-1].replace(',','dna ',1)[::-1]
>>> f([])
'{}'
>>> f(["ABC"])
'{ABC}'
>>> f(["ABC","DEF"])
'{ABC and DEF}'
>>> f(["ABC","DEF","G","H"])
'{ABC, DEF, G and H}'

Эта версия может быть проще понять


>>> f=lambda s:"{%s}"%" and ".join(s).replace(' and',',',len(s)-2)
>>> f([])
'{}'
>>> f(["ABC"])
'{ABC}'
>>> f(["ABC","DEF"])
'{ABC and DEF}'
>>> f(["ABC","DEF","G","H"])
'{ABC, DEF, G and H}'

Ответ 5

Здесь простое решение F #, которое выполняет только одну итерацию вперед:

let CommaQuibble items =
    let sb = System.Text.StringBuilder("{")
    // pp is 2 previous, p is previous
    let pp,p = items |> Seq.fold (fun (pp:string option,p) s -> 
        if pp <> None then
            sb.Append(pp.Value).Append(", ") |> ignore
        (p, Some(s))) (None,None)
    if pp <> None then
        sb.Append(pp.Value).Append(" and ") |> ignore
    if p <> None then
        sb.Append(p.Value) |> ignore
    sb.Append("}").ToString()

(EDIT: Оказывается, это очень похоже на Skeet's.)

Код проверки:

let Test l =
    printfn "%s" (CommaQuibble l)

Test []
Test ["ABC"]        
Test ["ABC";"DEF"]        
Test ["ABC";"DEF";"G"]        
Test ["ABC";"DEF";"G";"H"]        
Test ["ABC";null;"G";"H"]        

Ответ 6

Отказ от ответственности. Я использовал это как предлог для игры с новыми технологиями, поэтому мои решения не соответствуют оригинальным требованиям Eric для ясности и удобства обслуживания.

Наивное решение перечислителя (Я признаю, что вариант foreach этого является превосходным, поскольку он не требует ручного возиться с перечислителем.)

public static string NaiveConcatenate(IEnumerable<string> sequence)
{
    StringBuilder sb = new StringBuilder();
    sb.Append('{');

    IEnumerator<string> enumerator = sequence.GetEnumerator();

    if (enumerator.MoveNext())
    {
        string a = enumerator.Current;
        if (!enumerator.MoveNext())
        {
            sb.Append(a);
        }
        else
        {
            string b = enumerator.Current;
            while (enumerator.MoveNext())
            {
                sb.Append(a);
                sb.Append(", ");
                a = b;
                b = enumerator.Current;
            }
            sb.AppendFormat("{0} and {1}", a, b);
        }
    }

    sb.Append('}');
    return sb.ToString();
}

Решение с использованием LINQ

public static string ConcatenateWithLinq(IEnumerable<string> sequence)
{
    return (from item in sequence select item)
        .Aggregate(
        new {sb = new StringBuilder("{"), a = (string) null, b = (string) null},
        (s, x) =>
            {
                if (s.a != null)
                {
                    s.sb.Append(s.a);
                    s.sb.Append(", ");
                }
                return new {s.sb, a = s.b, b = x};
            },
        (s) =>
            {
                if (s.b != null)
                    if (s.a != null)
                        s.sb.AppendFormat("{0} and {1}", s.a, s.b);
                    else
                        s.sb.Append(s.b);
                s.sb.Append("}");
                return s.sb.ToString();
            });
}

Решение с TPL

В этом решении используется очередь производителей-потребителей для подачи входной последовательности на процессор, сохраняя по меньшей мере два элемента, буферизованных в очереди. Как только производитель достигнет конца входной последовательности, последние два элемента могут быть обработаны с особой обработкой.

В ретроспективе нет причин для того, чтобы потребитель работал асинхронно, что избавило бы от потребности в параллельной очереди, но, как я сказал ранее, я просто использовал это как повод для игры с новыми технологиями: -)

public static string ConcatenateWithTpl(IEnumerable<string> sequence)
{
    var queue = new ConcurrentQueue<string>();
    bool stop = false;

    var consumer = Future.Create(
        () =>
            {
                var sb = new StringBuilder("{");
                while (!stop || queue.Count > 2)
                {
                    string s;
                    if (queue.Count > 2 && queue.TryDequeue(out s))
                        sb.AppendFormat("{0}, ", s);
                }
                return sb;
            });

    // Producer
    foreach (var item in sequence)
        queue.Enqueue(item);

    stop = true;
    StringBuilder result = consumer.Value;

    string a;
    string b;

    if (queue.TryDequeue(out a))
        if (queue.TryDequeue(out b))
            result.AppendFormat("{0} and {1}", a, b);
        else
            result.Append(a);

    result.Append("}");
    return result.ToString();
}

Уточненные тесты для краткости.

Ответ 7

Я поклонник серийной запятой: я ем, стреляю и уезжаю.

Я постоянно нуждаюсь в решении этой проблемы и решил ее на трех языках (хотя и не на С#). Я бы адаптировал следующее решение (в Lua, не обертывает ответ в фигурные скобки), написав метод concat, который работает на любой IEnumerable:

function commafy(t, andword)
  andword = andword or 'and'
  local n = #t -- number of elements in the numeration
  if n == 1 then
    return t[1]
  elseif n == 2 then
    return concat { t[1], ' ', andword, ' ', t[2] }
  else
    local last = t[n]
    t[n] = andword .. ' ' .. t[n]
    local answer = concat(t, ', ')
    t[n] = last
    return answer
  end
end

Ответ 8

Это не блестяще читаемо, но оно масштабируется до десятков миллионов строк. Я развиваюсь на старой рабочей станции Pentium 4, и она составляет 1 000 000 строк средней длины 8 примерно в 350 мс.

public static string CreateLippertString(IEnumerable<string> strings)
{
    char[] combinedString;
    char[] commaSeparator = new char[] { ',', ' ' };
    char[] andSeparator = new char[] { ' ', 'A', 'N', 'D', ' ' };

    int totalLength = 2;  //'{' and '}'
    int numEntries = 0;
    int currentEntry = 0;
    int currentPosition = 0;
    int secondToLast;
    int last;
    int commaLength= commaSeparator.Length;
    int andLength = andSeparator.Length;
    int cbComma = commaLength * sizeof(char);
    int cbAnd = andLength * sizeof(char);

    //calculate the sum of the lengths of the strings
    foreach (string s in strings)
    {
        totalLength += s.Length;
        ++numEntries;
    }

    //add to the total length the length of the constant characters
    if (numEntries >= 2)
        totalLength += 5;  // " AND "

    if (numEntries > 2)
        totalLength += (2 * (numEntries - 2)); // ", " between items

    //setup some meta-variables to help later
    secondToLast = numEntries - 2;
    last = numEntries - 1;

    //allocate the memory for the combined string
    combinedString = new char[totalLength];
    //set the first character to {
    combinedString[0] = '{';
    currentPosition = 1;

    if (numEntries > 0)
    {
        //now copy each string into its place
        foreach (string s in strings)
        {
            Buffer.BlockCopy(s.ToCharArray(), 0, combinedString, currentPosition * sizeof(char), s.Length * sizeof(char));
            currentPosition += s.Length;

            if (currentEntry == secondToLast)
            {
                Buffer.BlockCopy(andSeparator, 0, combinedString, currentPosition * sizeof(char), cbAnd);
                currentPosition += andLength;
            }
            else if (currentEntry == last)
            {
                combinedString[currentPosition] = '}'; //set the last character to '}'
                break;  //don't bother making that last call to the enumerator
            }
            else if (currentEntry < secondToLast)
            {
                Buffer.BlockCopy(commaSeparator, 0, combinedString, currentPosition * sizeof(char), cbComma);
                currentPosition += commaLength;
            }

            ++currentEntry;
        }
    }
    else
    {
        //set the last character to '}'
        combinedString[1] = '}';
    }

    return new string(combinedString);
}

Ответ 9

Другой вариант - разделение логики пунктуации и итерации ради ясности кода. И все же думать о перфомансе.

Работает по запросу с чистыми IEnumerable/string/и строками в списке не может быть null.

public static string Concat(IEnumerable<string> strings)
{
    return "{" + strings.reduce("", (acc, prev, cur, next) => 
               acc.Append(punctuation(prev, cur, next)).Append(cur)) + "}";
}
private static string punctuation(string prev, string cur, string next)
{
    if (null == prev || null == cur)
        return "";
    if (null == next)
        return " and ";
    return ", ";
}

private static string reduce(this IEnumerable<string> strings, 
    string acc, Func<StringBuilder, string, string, string, StringBuilder> func)
{
    if (null == strings) return "";

    var accumulatorBuilder = new StringBuilder(acc);
    string cur = null;
    string prev = null;
    foreach (var next in strings)
    {
        func(accumulatorBuilder, prev, cur, next);
        prev = cur;
        cur = next;
    }
    func(accumulatorBuilder, prev, cur, null);

    return accumulatorBuilder.ToString();
}

F #, безусловно, выглядит намного лучше:

let rec reduce list =
    match list with
    | []          -> ""
    | head::curr::[]  -> head + " and " + curr
    | head::curr::tail  -> head + ", " + curr :: tail |> reduce
    | head::[] -> head

let concat list = "{" + (list |> reduce )  + "}"

Ответ 10

Поздняя запись:

public static string CommaQuibbling(IEnumerable<string> items)
{
    string[] parts = items.ToArray();
    StringBuilder result = new StringBuilder('{');
    for (int i = 0; i < parts.Length; i++)
    {
        if (i > 0)
            result.Append(i == parts.Length - 1 ? " and " : ", ");
        result.Append(parts[i]);
    }
    return result.Append('}').ToString();
}

Ответ 11

public static string CommaQuibbling(IEnumerable<string> items)
{
  int count = items.Count();
  string answer = string.Empty;
  return "{" + 
      (count==0)  ?  ""  :  
         (  items[0] + 
             (count == 1 ? "" :  
                 items.Range(1,count-1).
                     Aggregate(answer, (s,a)=> s += ", " + a) +
                 items.Range(count-1,1).
                     Aggregate(answer, (s,a)=> s += " AND " + a) ))+ "}";
}

Он реализуется как

if count == 0 , then return empty,
if count == 1 , then return only element,
if count > 1 , then take two ranges, 
   first 2nd element to 2nd last element
   last element

Ответ 12

Как пропустить сложный код агрегации и просто очистить строку после ее сборки?

public static string CommaQuibbling(IEnumerable<string> items)    
{
    var aggregate = items.Aggregate<string, StringBuilder>(
        new StringBuilder(), 
        (b,s) => b.AppendFormat(", {0}", s));
    var trimmed = Regex.Replace(aggregate.ToString(), "^, ", string.Empty);
    return string.Format(
               "{{{0}}}", 
               Regex.Replace(trimmed, 
                   ", (?<last>[^,]*)$", @" and ${last}"));
}

ОБНОВЛЕНО: это не будет работать с строками с запятыми, как указано в комментариях. Я пробовал некоторые другие варианты, но без определенных правил о том, что могут содержать строки, у меня будут реальные проблемы, соответствующие любому возможному последнему элементу с регулярным выражением, что делает этот хороший урок для меня по их ограничениям.

Ответ 13

Здесь мой, но я понимаю это в значительной степени, как у Марка, некоторые незначительные различия в порядке вещей, и я также добавил модульные тесты.

using System;
using NUnit.Framework;
using NUnit.Framework.Extensions;
using System.Collections.Generic;
using System.Text;
using NUnit.Framework.SyntaxHelpers;

namespace StringChallengeProject
{
    [TestFixture]
    public class StringChallenge
    {
        [RowTest]
        [Row(new String[] { }, "{}")]
        [Row(new[] { "ABC" }, "{ABC}")]
        [Row(new[] { "ABC", "DEF" }, "{ABC and DEF}")]
        [Row(new[] { "ABC", "DEF", "G", "H" }, "{ABC, DEF, G and H}")]
        public void Test(String[] input, String expectedOutput)
        {
            Assert.That(FormatString(input), Is.EqualTo(expectedOutput));
        }

        //codesnippet:93458590-3182-11de-8c30-0800200c9a66
        public static String FormatString(IEnumerable<String> input)
        {
            if (input == null)
                return "{}";

            using (var iterator = input.GetEnumerator())
            {
                // Guard-clause for empty source
                if (!iterator.MoveNext())
                    return "{}";

                // Take care of first value
                var output = new StringBuilder();
                output.Append('{').Append(iterator.Current);

                // Grab next
                if (iterator.MoveNext())
                {
                    // Grab the next value, but don't process it
                    // we don't know whether to use comma or "and"
                    // until we've grabbed the next after it as well
                    String nextValue = iterator.Current;
                    while (iterator.MoveNext())
                    {
                        output.Append(", ");
                        output.Append(nextValue);

                        nextValue = iterator.Current;
                    }

                    output.Append(" and ");
                    output.Append(nextValue);
                }


                output.Append('}');
                return output.ToString();
            }
        }
    }
}

Ответ 14

Мне очень понравился Джон, но это потому, что мне очень нравится, как я подошел к этой проблеме. Вместо специального кодирования в двух переменных я реализовал их внутри очереди FIFO.

Это странно, потому что я только предположил, что будет 15 сообщений, которые все сделали точно так же, но похоже, что мы были единственными, кто сделал это именно так. О, глядя на эти ответы, ответ Марка Гравелла довольно близок к тому методу, который мы использовали, но он использует две "петли", а не придерживаясь значений.

Но все эти ответы с LINQ и регулярным выражением и объединение массивов просто кажутся сумасшедшими!: -)

Ответ 15

Я не думаю, что использование старого старого массива - это ограничение. Вот моя версия с использованием массива и метода расширения:

public static string CommaQuibbling(IEnumerable<string> list)
{
    string[] array = list.ToArray();

    if (array.Length == 0) return string.Empty.PutCurlyBraces();
    if (array.Length == 1) return array[0].PutCurlyBraces();

    string allExceptLast = string.Join(", ", array, 0, array.Length - 1);
    string theLast = array[array.Length - 1];

    return string.Format("{0} and {1}", allExceptLast, theLast)
                 .PutCurlyBraces();
}

public static string PutCurlyBraces(this string str)
{
    return "{" + str + "}";
}

Я использую массив из-за метода string.Join и потому, что если возможность доступа к последнему элементу через индекс. Метод расширения здесь из-за DRY.

Я думаю, что нарушения производительности исходят от вызовов list.ToArray() и string.Join, но все в одном я надеюсь, что часть кода приятна для чтения и поддержки.

Ответ 16

Я думаю, что Linq предоставляет довольно читаемый код. Эта версия обрабатывает миллион "ABC" за 0,89 секунды:

using System.Collections.Generic;
using System.Linq;

namespace CommaQuibbling
{
    internal class Translator
    {
        public string Translate(IEnumerable<string> items)
        {
            return "{" + Join(items) + "}";
        }

        private static string Join(IEnumerable<string> items)
        {
            var leadingItems = LeadingItemsFrom(items);
            var lastItem = LastItemFrom(items);

            return JoinLeading(leadingItems) + lastItem;
        }

        private static IEnumerable<string> LeadingItemsFrom(IEnumerable<string> items)
        {
            return items.Reverse().Skip(1).Reverse();
        }

        private static string LastItemFrom(IEnumerable<string> items)
        {
            return items.LastOrDefault();
        }

        private static string JoinLeading(IEnumerable<string> items)
        {
            if (items.Any() == false) return "";

            return string.Join(", ", items.ToArray()) + " and ";
        }
    }
}

Ответ 17

Вы можете использовать foreach, без LINQ, делегатов, закрытий, списков или массивов и по-прежнему иметь понятный код. Используйте bool и строку, например:

public static string CommaQuibbling(IEnumerable items)
{
    StringBuilder sb = new StringBuilder("{");
    bool empty = true;
    string prev = null;
    foreach (string s in items)
    {
        if (prev!=null)
        {
            if (!empty) sb.Append(", ");
            else empty = false;
            sb.Append(prev);
        }
        prev = s;
    }
    if (prev!=null)
    {
        if (!empty) sb.Append(" and ");
        sb.Append(prev);
    }
    return sb.Append('}').ToString();
}

Ответ 18

public static string CommaQuibbling(IEnumerable<string> items)
{
   var itemArray = items.ToArray();

   var commaSeparated = String.Join(", ", itemArray, 0, Math.Max(itemArray.Length - 1, 0));
   if (commaSeparated.Length > 0) commaSeparated += " and ";

   return "{" + commaSeparated + itemArray.LastOrDefault() + "}";
}

Ответ 19

Здесь мое представление. Несколько изменили подпись, чтобы сделать ее более общей. Использование функций .NET 4 (String.Join() с помощью IEnumerable<T>), в противном случае работает с .NET 3.5. Цель заключалась в использовании LINQ с существенно упрощенной логикой.

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

Ответ 20

Есть несколько ответов, отличных от С#, и исходное сообщение задавало ответы на любом языке, поэтому я подумал, что я покажу еще один способ сделать это, что ни один из программистов на С#, кажется, не затронул: DSL!

(defun quibble-comma (words)
  (format nil "~{~#[~;~a~;~a and ~a~:;[email protected]{~a~#[~; and ~:;, ~]~}~]~}" words))

Проницательность заметит, что Common Lisp не имеет встроенного IEnumerable<T>, и, следовательно, FORMAT здесь будет работать только в правильном списке. Но если вы сделали IEnumerable, вы, конечно же, могли бы расширить FORMAT, чтобы работать над этим. (Имеет ли это Clojure?)

Кроме того, любой, кто читает этот, у кого есть вкус (включая Lisp программистов!), вероятно, будет оскорблен литеральным "~{~#[~;~a~;~a and ~a~:;[email protected]{~a~#[~; and ~:;, ~]~}~]~}" там. Я не буду утверждать, что FORMAT реализует хороший DSL, но я считаю, что чрезвычайно полезно иметь несколько мощных DSL для объединения строк. Regex - мощный DSL для разрыва строк, а string.Format - это DSL (тип) для объединения строк, но он глупо слабый.

Я думаю, что все это все время пишут. Почему, черт возьми, там нет встроенного универсального со вкусом DSL? Я думаю, что самое близкое, что у нас есть, это "Perl", может быть.

Ответ 21

Просто для удовольствия, используя новый метод расширения Zip из С# 4.0:

private static string CommaQuibbling(IEnumerable<string> list)
{
    IEnumerable<string> separators = GetSeparators(list.Count());
    var finalList = list.Zip(separators, (w, s) => w + s);
    return string.Concat("{", string.Join(string.Empty, finalList), "}");
}

private static IEnumerable<string> GetSeparators(int itemCount)
{
    while (itemCount-- > 2)
        yield return ", ";

    if (itemCount == 1)
        yield return " and ";

    yield return string.Empty;
}

Ответ 22

return String.Concat(
    "{",
    input.Length > 2 ?
        String.Concat(
            String.Join(", ", input.Take(input.Length - 1)),
            " and ",
            input.Last()) :
    String.Join(" and ", input),
    "}");

Ответ 23

Я пробовал использовать foreach. Пожалуйста, дайте мне знать ваши мнения.

private static string CommaQuibble(IEnumerable<string> input)
{
    var val = string.Concat(input.Process(
        p => p,
        p => string.Format(" and {0}", p),
        p => string.Format(", {0}", p)));
    return string.Format("{{{0}}}", val);
}

public static IEnumerable<T> Process<T>(this IEnumerable<T> input, 
    Func<T, T> firstItemFunc, 
    Func<T, T> lastItemFunc, 
    Func<T, T> otherItemFunc)
{
    //break on empty sequence
    if (!input.Any()) yield break;

    //return first elem
    var first = input.First();
    yield return firstItemFunc(first);

    //break if there was only one elem
    var rest = input.Skip(1);
    if (!rest.Any()) yield break;

    //start looping the rest of the elements
    T prevItem = first;
    bool isFirstIteration = true;
    foreach (var item in rest)
    {
        if (isFirstIteration) isFirstIteration = false;
        else
        {
            yield return otherItemFunc(prevItem);
        }
        prevItem = item;
    }

    //last element
    yield return lastItemFunc(prevItem);
}

Ответ 24

Вот несколько решений и тестовый код, написанный на Perl на основе ответов http://blogs.perl.org/users/brian_d_foy/2013/10/comma-quibbling-in-perl.html.

#!/usr/bin/perl

use 5.14.0;
use warnings;
use strict;
use Test::More qw{no_plan};

sub comma_quibbling1 {
   my (@words) = @_;
   return "" unless @words;
   return $words[0] if @words == 1;
   return join(", ", @words[0 .. $#words - 1]) . " and $words[-1]";
}

sub comma_quibbling2 {
   return "" unless @_;
   my $last = pop @_;
   return $last unless @_;
   return join(", ", @_) . " and $last";
}

is comma_quibbling1(qw{}),                   "",                         "1-0";
is comma_quibbling1(qw{one}),                "one",                      "1-1";
is comma_quibbling1(qw{one two}),            "one and two",              "1-2";
is comma_quibbling1(qw{one two three}),      "one, two and three",       "1-3";
is comma_quibbling1(qw{one two three four}), "one, two, three and four", "1-4";

is comma_quibbling2(qw{}),                   "",                         "2-0";
is comma_quibbling2(qw{one}),                "one",                      "2-1";
is comma_quibbling2(qw{one two}),            "one and two",              "2-2";
is comma_quibbling2(qw{one two three}),      "one, two and three",       "2-3";
is comma_quibbling2(qw{one two three four}), "one, two, three and four", "2-4";