2010-09-29 20 views
6

Tengo una consulta LINQ que hace algo tan simple como:lista de LINQ a condenar formato (insertar comas y "y")

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

entonces quiero una función o LINQ lambda, o algo que va de salida de los nombres en formato de oraciones usando comas y "ands".

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

a

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

Estoy bien con codificando la parte ID + ":" + Name, pero podría ser un ToString() en función del tipo de resultado de la consulta LINQ. Me pregunto si hay una buena manera de hacer esto con linq o String.Format().

+1

Pregunta similar: http://stackoverflow.com/questions/788535 – Foole

Respuesta

6

¿Por qué 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 "); 
} 

De verdad, todo lo que Linq te dejará es ocultar el ciclo.

Además, asegúrese de que desea o no el "Oxford Comma"; este algoritmo no insertará uno, pero un pequeño cambio lo hará (agregue la coma y el espacio después de cada elemento excepto el último, y también agregue "y" después del penúltimo).

+0

apoyo esta. El LINQ comparable es complicado sin ningún motivo. Sin embargo, ¿no sería mejor hacer i Rubys

+1

Probablemente sería preferible almacenar el resultado de 'k.Count()' en una variable local. – Timwi

+0

@Rubys, Timwi: tom-ay-to, tom-ah-to. También tendría que ToList() o ToArray() los resultados de la consulta del OP para indexarlo, haciendo que la cardinalidad sea accesible a través de la propiedad Count o Length de un miembro. En cuanto a la eficiencia, el resto si solo se evalúa si la primera mitad es falsa, por lo que no ahorra mucho sacar la última parte A MENOS QUE el OP desee la coma de Oxford. – KeithS

-1

Hay maneras de optimizar esto, ya que no es muy eficiente, pero algo como esto pueden trabajar:

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

Entrada: '[a, b, b, b]' Salida: 'a y b y b y b' – Timwi

0

Mejora (con suerte) sobre la respuesta de 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); 
+0

Ouch. Salidas ', 1: John, 2: Mar y 3: George' – Timwi

3

Sólo por Diversión, aquí hay algo que realmente utiliza LINQ funcional - sin bucle y no StringBuilder. Por supuesto, es bastante lento.

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

Parece que' ToList() 'no es necesario. Sin embargo, no cambia el código. –

+0

@Ahmad: Gracias, eliminado :) – Timwi

0

Esto no es bonita, pero hará el trabajo usando 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

Esto no funcionará si su último elemento aparece varias veces en la lista. – Gabe

+0

La pregunta no especifica que debe ser distinta. – Viv

0

Todos ustedes están haciendo que sea demasiado complicado:

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

Enfoque StringBuilder

aquí está un Aggregate con un StringBuilder. Hay algunas determinaciones de posición que se realizan para limpiar la cadena e insertar el "y", pero todo se hace en el nivel 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()); 

Un enfoque String.Join podrían haberse utilizado en su lugar, pero el "y" la eliminación de inserción y coma generaría ~ 2 nuevas cadenas.


Enfoque Regex

Aquí hay otro método que utiliza expresiones regulares que es bastante comprensible (nada demasiado críptica).

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

El patrón es simplemente ", ". La magia se encuentra en el RegexOptions.RightToLeft que hace que la coincidencia se produzca desde la derecha y, por lo tanto, hace que el reemplazo ocurra en la última aparición de coma. No hay un método estático Regex que acepte el número de reemplazos con el RegexOptions, de ahí el uso de la instancia.

1

Al igual que el resto, esto no es mejor que el uso de un generador de cadenas, pero se puede ir (ignorando el ID, puede añadirlo en):

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

Esta aproximación más o menos abusos y SkipTake tiene la capacidad de tomar números negativos, y la disposición de String.Join de tomar un solo parámetro, por lo que funciona para una, dos o más cadenas.

0

Ésta puede ser la forma en que puede lograr su objetivo

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

Cerrar. Esto devuelve ', 1: John, 2: Mark y 3: George'. Debe verificar si es el primer elemento y no agregar la coma en ese caso. –

+0

Sí, tiene razón, he actualizado los cambios – Sany

0

¿Qué tal esto?

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

¿Qué pasa con el caso límite de una lista vacía? –

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

Llamado con:

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

Ooh, me gusta su solución. Ambas llevan la elegancia de los lenguajes funcionales y cierto enfoque pragmático de StringBuilder. Prestigio. –

1

Uso de la Selección de operación que le da un índice, esto se puede escribir como un método de extensión una línea:

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

por ejemplo,

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

He refinado mi respuesta anterior y creo que esta es la solución más elegante hasta ahora.
Sin embargo, solo funcionaría en los tipos de referencia que no se repiten en la colección (o de lo contrario tendríamos que utilizar diferentes medios para averiguar si el artículo es el primero o el último).

¡Disfrútalo!

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

Aquí es un método que no utiliza LINQ, pero es probablemente tan eficaz como se puede llegar:

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

Ya que nunca se crea una lista, mira a un elemento dos veces, o añade adicional cosas para StringBuilder, no creo que puedas ser más eficiente. También funciona para 0, 1 y 2 elementos en la lista (y también más, obviamente).

0

Aquí está uno que usa una versión ligeramente modificada de mi answer a Eric Lippert's Challenge que es IMHO el más conciso con lógica fácil de seguir (si usted está familiarizado con 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); 
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); 
} 

puede ser optimizado moviendo k.Last cómputo() para antes del bucle

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

Este es el método es más rápido que el método de 'eficiente' Únete publicado por Gabe. Para uno y dos elementos, es mucho más rápido, y para 5-6 cadenas, es aproximadamente un 10% más rápido.No hay dependencia en LINQ. String.Join es más rápido que StringBuilder para arreglos pequeños, que son típicos para textos legibles por humanos. En gramática, estos se llaman listing commas, y la última coma siempre debe incluirse para evitar ambigüedades. Aquí está el código resultante:

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

Cuestiones relacionadas