2010-09-29 5 views
6

ich eine Linq-Abfrage, die etwas Einfach wie tut:LINQ Liste Satz Format (insert Komma & „und“)

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

ich dann eine Funktion oder Linq Lambda will, oder etwas, das gibt die Namen im Satzformat mit Kommas und "ands".

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

zu

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

Ich bin fein mit hartzucodieren den ID + ":" + Name Teil, aber es könnte eine ToString() je nach Art des Linq-Abfrage Ergebnisses. Ich frage mich nur, ob es eine nette Möglichkeit gibt, dies mit Linq oder String.Format() zu tun.

+1

ähnliche Frage: http://stackoverflow.com/questions/788535 – Foole

Antwort

6

Warum Linq?

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

Wirklich, alle Linq lassen Sie tun, ist die Schleife zu verstecken.

Auch stellen Sie sicher, dass Sie die "Oxford Comma" oder nicht wollen; Dieser Algorithmus wird nicht einen einfügen, aber eine kleine Änderung wird (fügen Sie das Komma und Leerzeichen nach jedem Element mit Ausnahme der letzten, und fügen Sie auch "und" nach dem vorletzten).

+0

ich dies unterstützen. Die vergleichbare LINQ ist ohne Grund kompliziert. Aber wäre es nicht besser, i Rubys

+1

Es wäre wahrscheinlich vorzuziehen, das Ergebnis von 'k.Count()' in einer lokalen Variablen zu speichern. – Timwi

+0

@Rubys, Timwi: tom-ay-to, tom-ah-to. Sie müssten auch die Ergebnisse der OP-Abfrage ToList() oder ToArray() indexieren, um die Kardinalität über eine Elementeigenschaft Count oder Length zugänglich zu machen. Hinsichtlich der Effizienz wird das else if nur ausgewertet, wenn die erste Hälfte falsch ist, so dass es nicht viel spart, den letzten Teil herauszunehmen, WENN das OP das Oxford-Komma möchte. – KeithS

-1

Es gibt Möglichkeiten, dies, da es zu optimieren ist nicht sehr effizient, aber so etwas wie dies funktionieren kann:

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

Eingabe: '[a, b, b, b]' Ausgabe: 'a und b und b und b' – Timwi

0

Verbesserung der (hoffentlich) auf Keiths Antwort:

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

Autsch. Ausgänge ', 1: John, 2: Mar und 3: George' – Timwi

3

Just for Spaß, hier ist etwas, das wirklich funktionale LINQ verwendet - keine Schleife und keine StringBuilder. Natürlich ist es ziemlich langsam.

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

Es sieht so aus, als ob' ToList() 'nicht benötigt wird. Trotzdem ändert es den Code nicht. –

+0

@Ahmad: Danke, entfernt :) – Timwi

0

Das ist nicht schön, aber wird die Arbeit mit 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; 
+0

Dies funktioniert nicht, wenn Ihr letztes Element mehrfach in der Liste erscheint. – Gabe

+0

Die Frage gibt nicht an, dass es unterscheidbar sein soll. – Viv

0

Y'all machen es zu kompliziert:

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

String Ansatz

Ist hier ein Aggregate mit einem StringBuilder. Es gibt einige Positionsbestimmungen, die vorgenommen werden, um die Zeichenfolge zu bereinigen und das "und" einzufügen, aber alles geschieht unter der StringBuilder Ebene.

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

Ein String.Join Ansatz wurde stattdessen könnte aber das „und“ Einsetzen und Entfernen Komma erzeugen würde ~ 2 neue Saiten verwendet.


Regex Ansatz

Hier ist ein weiterer Ansatz regex, die durchaus verständlich ist (nichts zu kryptisch).

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

Das Muster ist einfach ", ". Die Magie liegt in der RegexOptions.RightToLeft, die die Übereinstimmung von rechts macht und dadurch die Ersetzung beim letzten Komma-Auftreten bewirkt. Es gibt keine statische Regex-Methode, die die Anzahl der Ersetzungen mit der RegexOptions akzeptiert, daher die Instanzverwendung.

1

Ähnlich wie der Rest, das ist nicht besser als einen String Builder verwenden, aber Sie können gehen (die ID zu ignorieren, können Sie es hinzufügen):

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

Dieser Ansatz ziemlich Mißbräuche Skip und Take die Fähigkeit, negative Zahlen zu nehmen, und String.Join die Bereitschaft, einen einzigen Parameter zu nehmen, so dass es für ein, zwei oder mehr Strings funktioniert.

0

Dies kann die Art und Weise sein, Ihr Ziel

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

Schließen. Das gibt ', 1: John, 2: Mark und 3: George' zurück. Sie müssen prüfen, ob es das erste Element ist und nicht das Komma in diesem Fall anhängen. –

+0

Ja, Sie haben Recht, ich habe die Änderungen aktualisiert – Sany

0

Wie wäre dies erreichen können?

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(); 
+0

Was ist mit dem Rand Fall einer leeren Liste? –

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

Called mit:

string result = ToPrettyCommas(people, p => p.ID.ToString() + ":" + p.Name); 
+1

Ooh, ich mag deine Lösung. Es trägt Eleganz von funktionalen Sprachen und einige pragmatische StringBuilder-Ansatz. Kudos. –

1

die Operation auswählen, die Sie einen Index gibt, kann dies als eine einzeilige Erweiterung Methode geschrieben werden:

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

z.B.

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

Ich habe meine vorherige Antwort verfeinert und ich glaube, das ist die eleganteste Lösung noch.
Allerdings würde es nur für Referenztypen funktionieren, die sich nicht in der Sammlung wiederholen (andernfalls müssten wir andere Mittel verwenden, um herauszufinden, ob der Artikel das erste/letzte Mal ist).

Viel Spaß!

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

Hier ist eine Methode, die nicht LINQ verwendet, sondern ist wahrscheinlich so effizient wie Sie bekommen können:

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

Da es nie eine Liste erstellt, schaut auf ein Element zweimal oder anhängt Extra Sachen zum StringBuilder, ich glaube nicht, dass du effizienter werden kannst. Es funktioniert auch für 0, 1 und 2 Elemente in der Liste (und natürlich auch mehr).

0

Hier ist eine mit einer leicht modifizierten Version meiner answer zu Eric Lippert's Challenge, die IMHO ist die prägnanteste mit leicht zu folgen Logik (wenn Sie mit LINQ vertraut sind).

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

durch Bewegen k.Last() Berechnung vor dem Dies ist die Methode

0
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]; 
     } 
    } 

Schleife optimiert werden könnte, ist schneller als die von Gabe geschrieben Methode 'effizient' Join. Für ein und zwei Items ist es um ein Vielfaches schneller und für 5-6 Strings um 10% schneller.Es besteht keine Abhängigkeit von LINQ. String.Join ist schneller als StringBuilder für kleine Arrays, die typisch für lesbaren Text sind. In der Grammatik werden diese listing commas10 genannt, und das letzte Komma sollte immer enthalten sein, um Mehrdeutigkeiten zu vermeiden. Hier ist der resultierende Code:

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