Задача Эрика Липперта "запятая", лучший ответ? - PullRequest
23 голосов
/ 25 апреля 2009

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

Резюме:

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

  1. Если последовательность пуста, в результате получается строка "{}".
  2. Если последовательность представляет собой один элемент «ABC», тогда полученная строка будет «{ABC}».
  3. Если последовательность представляет собой последовательность из двух элементов «ABC», «DEF», то полученная строка будет «{ABC and DEF}».
  4. Если последовательность содержит более двух элементов, скажем, «ABC», «DEF», «G», «H», то полученная строка будет «{ABC, DEF, G и H}». (Примечание: запятая не оксфордская!)

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

Что ты думаешь? Там есть довольно хорошие варианты. Мне действительно нравится одно из решений, которое включает методы выбора и агрегирования (от Фернандо Николе). 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) + "}";
}

Ответы [ 25 ]

1 голос
/ 25 апреля 2009
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
1 голос
/ 25 апреля 2009

Как насчет пропуска сложного кода агрегации и просто очистки строки после ее построения?

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

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

1 голос
/ 03 октября 2010

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

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

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

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

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

1 голос
/ 25 апреля 2009

Я думаю, что 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 ";
        }
    }
}
1 голос
/ 25 апреля 2009

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

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 и из-за возможности доступа к последнему элементу через индекс. Метод расширения здесь из-за СУХОГО.

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

1 голос
/ 25 апреля 2009

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

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

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

1 голос
/ 25 апреля 2009

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

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();
            }
        }
    }
}
1 голос
/ 03 октября 2010

Вот мое представление. Немного изменила подпись, чтобы сделать ее более общей. Использование функций .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) + "}";
}
1 голос
/ 28 января 2011
return String.Concat(
    "{",
    input.Length > 2 ?
        String.Concat(
            String.Join(", ", input.Take(input.Length - 1)),
            " and ",
            input.Last()) :
    String.Join(" and ", input),
    "}");
1 голос
/ 26 января 2011

Просто для удовольствия, используя новый метод расширения Zip из C # 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;
}
...