LINQ список к формату предложения (вставьте запятые и «и»)

У меня есть запрос linq, который делает что-то простое, как:

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

Затем я хочу функцию или linq лямбда, или что-то, что будет выводить имена в формате предложения, используя запятые и «и».

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

в

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

Я в порядке с жестким кодированиемID + ":" + Name часть, но это может быть ToString () в зависимости от типа результата запроса linq. Мне просто интересно, есть ли удобный способ сделать это с помощью linq или String.Format ().

 Foole30 сент. 2010 г., 07:06
Подобный вопрос:stackoverflow.com/questions/788535

Ответы на вопрос(17)

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

                   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);
 Timwi30 сент. 2010 г., 00:29
@ Ахмад: Спасибо, удалено :)
 Ahmad Mageed29 сент. 2010 г., 20:14
Это выглядит такToList() не требуется Тем не менее, это не меняет код.

Вы все усложняете

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

Как насчет этого?

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();
 Ian Mercer30 сент. 2010 г., 19:38
Как насчет крайнего случая пустого списка?
public string ToPrettyCommas<T>(
  List<T> source,
  Func<T, string> stringSelector
)
{
  int count = source.Count;

  Func<int, string> 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;
}

Вызывается с:

string result = ToPrettyCommas(people, p => p.ID.ToString() + ":" + p.Name);
 Dan Abramov29 сент. 2010 г., 22:05
Ох, мне нравится ваше решение. Это одновременно несет в себе элегантность функциональных языков и некоторый прагматичный подход StringBuilder. Престижность.
    public static string ToListingCommaFormat(this List<string> stringList)
    {
        switch(stringList.Count)
        {
            case 0:
                return "";
            case 1:
                return stringList[0];
            case 2:
                return stringList[0] + " and " + stringList[1];
            default:
                return String.Join(", ", stringList.GetRange(0, stringList.Count-1)) 
                    + ", and " + stringList[stringList.Count - 1];
        }
    }

чем «эффективный» метод Join, опубликованный Gabe. Для одного и двух элементов это во много раз быстрее, а для 5-6 строк - примерно на 10% быстрее. Здесь нет зависимости от LINQ. String.Join быстрее, чем StringBuilder для небольших массивов, которые типичны для читабельного текста. В грамматике это называетсясписок запятыхи последняя запятая всегда должна быть включена, чтобы избежать двусмысленности. Вот результирующий код:

people.Select(x=> x.ID.ToString() + ":" + x.Name).ToList().ToListingCommaFormat();

который не использует LINQ, но, вероятно, настолько эффективен, насколько это возможно:

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

Поскольку он никогда не создает список, не просматривает элемент дважды и не добавляет дополнительные элементы в StringBuilder, я не думаю, что вы можете добиться большей эффективности. Это также работает для 0, 1 и 2 элементов в списке (и, конечно, больше).

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

Наслаждайтесь!

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

StringBuilder подход

ВотAggregate сStringBuilder, Есть некоторые определения положения, которые сделаны, чтобы очистить строку и вставить «и», но все это делается наStringBuilder уровень.

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 Вместо этого можно было бы использовать подход, но вставка "и" и удаление запятой привели бы к ~ 2 новым строкам.

Подход Regex

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

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

Шаблон просто", ", Магия заключается вRegexOptions.RightToLeft что делает совпадение справа и, таким образом, замену происходит при последнем появлении запятой. Там нет статическогоRegex метод, который принимает количество замен сRegexOptionsотсюда и пример использования.

использующий слегка модифицированную версию моегоответ вЗадача Эрика Липперта ИМХО, это наиболее лаконичная и понятная логика (если вы знакомы с 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);

которым вы можете достичь своей цели

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++; });
 Sany30 сент. 2010 г., 06:56
Да, вы правы, я обновил изменения
 Ahmad Mageed29 сент. 2010 г., 20:52
Близко. Это возвращает, 1:John, 2:Mark and 3:George, Вы должны проверить, является ли это первым элементом, и не добавлять запятую в этом случае.

так как это не очень эффективно, но что-то вроде этого может работать:

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;
});
 Timwi29 сент. 2010 г., 19:18
Входные данные:[ a, b, b, b ] Выход:a and b and b and b

которая дает вам индекс, это можно записать как метод расширения ONE LINE:

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

например

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

это не лучше, чем использование строителя строк, но вы можете пойти (игнорируя идентификатор, вы можете добавить его):

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

Этот подход в значительной степени злоупотребляетSkip а такжеTakeспособность принимать отрицательные числа, иString.JoinГотовность принять один параметр, поэтому он работает для одной, двух или более строк.

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

Улучшение (надеюсь) ответа KeithS:

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);
 Timwi29 сент. 2010 г., 19:28
Уч. Выходы, 1:John, 2:Mar and 3:George

но сделаю работу, используя 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;
 Viv30 сент. 2010 г., 15:14
Вопрос не указывает, что он должен быть четким.
 Gabe30 сент. 2010 г., 07:43
Это не будет работать, если ваш последний элемент появится в списке несколько раз.

Почему Линк?

StringBuilder sb = new StringBuilder();

for(int i=0;i<k.Count();i++)
{
   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 ");
}

Действительно, все, что Linq позволит вам сделать, это скрыть петлю.

Кроме того, убедитесь, что вы делаете или не хотите "Оксфордская запятаяmsgstr "; этот алгоритм не вставит единицу, но будет небольшое изменение (добавьте запятую и пробел после каждого элемента, кроме последнего, а также добавьте" и "после следующего за последним).

 Sentient06 сент. 2012 г., 20:49
где k ++ => i ++
 jpmc2620 дек. 2018 г., 21:32
"Почему Линк?" «Действительно, все, что Linq позволит вам сделать, это скрыть петлю». Похоже, вы недооцениваете, насколько скрытие цикла или, точнее, замена цикла вызовом описательно названной функции улучшает читабельность.
 Timwi29 сент. 2010 г., 19:17
Вероятно, было бы предпочтительнее сохранить результатk.Count() в локальной переменной.
 MarkPflug29 сент. 2010 г., 19:57
@Timwi: Вы предпочитаете O (n²)? Странный.
 Amy B29 сент. 2010 г., 20:49
Count () использует свойство Count в IList или свойство Length в Array. k в настоящее время не является одним из них, но это не сложно сделать так.
 Timwi29 сент. 2010 г., 19:32
@KeithS: Если tom-ay-to - O (n), а tom-ah-to - O (n²), я предпочитаю tom-ah-to.
 Timwi30 сент. 2010 г., 00:28
@KeithS: я согласен с тем, что вы сказали, кроме последней части. Компилятор C # не оптимизируетлюбой общие подвыражения вообще. Компилятор .NET JIT иногда делает, но определенно не вызовы методов.
 KeithS29 сент. 2010 г., 19:26
@Rubys, Тимви: Том-ай-к, Том-а-к. Вы также должны ToList () или ToArray () результаты запроса OP, чтобы проиндексировать его, делая кардинальность доступной через свойство элемента Count или Length. Что касается эффективности, то оператор if if оценивается только в том случае, если первая половина имеет значение false, поэтому он не сильно экономит на том, чтобы вынуть последнюю часть, ЕСЛИ ОП не хочет получить оксфордскую запятую.
 KeithS29 сент. 2010 г., 20:13
@Timwi: Независимо от вашего опечатки, я упоминал, что вам нужно ToList () результаты запроса, чтобы проиндексировать их; Следовательно, алгоритм мог бы использовать Count (который имеет оценку в постоянном времени) вместо Count () (который имеет линейное значение). Я использовал Count () в своем алгоритме только потому, что, не зная, будут ли использоваться ToList () или ToArray (), он будет работать для обоих. Кроме того, так как Count () не изменяется и не используется вне области действия цикла, я не удивлюсь, если компилятор все равно оптимизирует вызовы Count () с помощью переменной.
 Rubys29 сент. 2010 г., 19:16
Я поддерживаю это. Сравнимый LINQ сложен без причины. Однако не лучше ли сделать i <k.Count () - 1, а затем добавить последний бит?

Ваш ответ на вопрос