2011-09-30 15 views
5

Tengo un objeto Message que ajusta un formato de mensaje que no tengo control. El formato es una lista simple de pares clave/valor. Quiero extraer una lista de usuarios de un mensaje dado. Por ejemplo, dada la siguiente mensaje ...Reemplazar bucle for-switch con una consulta Linq

1. 200->.... 
2. 300->.... 
3. .... 
4. 405->.... 
5. 001->first_user_name 
6. 002->first_user_phone 
7. 003->first_user_fax 
8. 001->second_user_name 
9. 001->third_user_name 
10. 002->third_user_phone 
11. 003->third_user_fax 
12. 004->third_user_address 
13. ..... 
14. 001->last_user_name 
15. 003->last_user_fax 

Quiero extraer cuatro usuarios con las propiedades proporcionadas establecidos. Las claves iniciales 200/300 .... 405 representan campos que no necesito y pueden omitir para acceder a los datos de usuario.

Cada uno de los datos de los usuarios está en campos consecutivos pero el número de campos varía según la cantidad de información que se tenga sobre un usuario. El siguiente método hace lo que estoy buscando. Utiliza una enumeración de posibles tipos de clave y un método para encontrar el índice del primer campo con datos de usuario.

private List<User> ParseUsers(Message message) 
{ 
    List<User> users = new List<User>(); 
    User user = null; String val = String.Empty; 

    for(Int32 i = message.IndexOfFirst(Keys.Name); i < message.Count; i++) 
    { 
     val = message[ i ].Val; 

     switch(message[ i ].Key) 
     { 
      case Keys.Name: 
       user = new User(val); 
       users.Add(user); 
       break; 
      case Keys.Phone: 
       user.Phone = val; 
       break; 
      case Keys.Fax: 
       user.Fax = val; 
       break; 
      case Keys.Address: 
       user.Address = val; 
       break; 
      default: 
       break; 
     } 
    } 

    return users; 
} 

Me pregunto si es posible reemplazar el método con una consulta de Linq. Tengo problemas para decirle a Linq que seleccione un nuevo usuario y llene sus campos con todos los datos coincidentes hasta que encuentre el inicio de la siguiente entrada de usuario.

Nota: Los números de clave relativa son aleatorios (no 1,2,3,4) en el formato de mensaje real.

+0

¿Estás utilizando Resharper? es bastante bueno para refactorizar bucles a expresiones LINQ. –

+2

¿Cuál sería el beneficio de convertir esto en una consulta LINQ? Tu código se ve bien para mí tal como es. – dtb

+0

@Marian: solo después de 5.x IIRC – sehe

Respuesta

5

no veo el beneficio en el cambio de su código para una consulta LINQ, pero es definitivamente posible:

private List<User> ParseUsers(Message message) 
{ 
    return Enumerable 
     .Range(0, message.Count) 
     .Select(i => message[i]) 
     .SkipWhile(x => x.Key != Keys.Name) 
     .GroupAdjacent((g, x) => x.Key != Keys.Name) 
     .Select(g => g.ToDictionary(x => x.Key, x => x.Val)) 
     .Select(d => new User(d[Keys.Name]) 
     { 
      Phone = d.ContainsKey(Keys.Phone) ? d[Keys.Phone] : null, 
      Fax  = d.ContainsKey(Keys.Fax)  ? d[Keys.Fax]  : null, 
      Address = d.ContainsKey(Keys.Address) ? d[Keys.Address] : null, 
     }) 
     .ToList(); 
} 

usando

static IEnumerable<IEnumerable<T>> GroupAdjacent<T>(
    this IEnumerable<T> source, Func<IEnumerable<T>, T, bool> adjacent) 
{ 
    var g = new List<T>(); 
    foreach (var x in source) 
    { 
     if (g.Count != 0 && !adjacent(g, x)) 
     { 
      yield return g; 
      g = new List<T>(); 
     } 
     g.Add(x); 
    } 
    yield return g; 
} 
+1

+1: responde la pregunta de OP y es * bastante convincente * al decirle que deja su buen código como está. – ANeves

+1

@dtb .. la versión editada de yup funciona de maravilla ... jaja .. no estoy del todo seguro de entender exactamente cómo exactamente, pero gracias de nuevo ... es genial ver que siempre hay una manera de hacer algo ... y estoy aprendiendo más sobre Linq yendo a través de su código – Chris

1

¿Qué hay de dividir el mensaje en un List<List<KeyValuePait<int, string>>> donde cada List<KeyValuePair<int, string>> representa un solo usuario. A continuación, podría hacer algo como:

// SplitToUserLists would need a sensible implementation. 
List<List<KeyValuePair<int,string>>> splitMessage = message.SplitToUserLists(); 
IEnumerable<User> users = splitMessage.Select(ConstructUser); 

Con

private User ConstructUser(List<KeyValuePair<int, string>> userList) 
{ 
    return userList.Aggregate(new User(), (user, keyValuePair) => user[keyValuePair.Key] = keyValuePair.Val); 
} 
+0

@joey .. hola Joey ... gracias por tu publicación ... he estado jugando con el código de dtb ... lo cual funciona ahora (gracias dtb) ... Tendría que volver a implementar mi Usuario clase para trabajar con ConstructUser como está y Id leer el mensaje más de una vez de esta manera ... pero es interesante ver los diferentes enfoques! gracias de nuevo – Chris

+0

No hay problema. Creo que la mejor solución sigue siendo la que comenzaste. Aunque quizás trate de refactorizar el interruptor en Usuario o algún objeto UserBuilder. – Joey

+0

+1 .. y eso es una buena sugerencia ... páselo un enumerable de campos divididos como sugeriste/a través de GroupAdjacent o similar y deja que se encargue de su propia creación, es poco probable que el formato de usuario cambie (¡ejem!) ... así que creo que iría con User en lugar de UserFactory/UserBuilder, pero aun así poner el código de creación en el Usuario sería mejor que tenerlo dentro del switch en otra ubicación. Gracias Joey. – Chris

1

No, y la razón de ser, en general, la mayoría de las funciones de LINQ, en la misma forma que las consultas SQL, se ocupan de los datos no ordenados, es decir, no hacen suposiciones sobre el orden de los datos entrantes. Eso les da flexibilidad para ser paralelizados, etc. Sus datos tienen un orden intrínseco, por lo que no se ajustan al modelo de consulta.

+0

@tim .. yup Estaba pensando que cuando escribí el ciclo, pero cada vez que lo saltaba tenía la sensación de que era posible ... – Chris

1

yo no creo que haya ninguna ventaja de rendimiento , pero aumenta mucho la legibilidad en mi opinión.

Una posible solución podría tener este aspecto:

var data = File.ReadAllLines("data.txt") 
      .Select(line => line.Split(new[] {"->"}, StringSplitOptions.RemoveEmptyEntries)) 
      .GroupByOrder(ele => ele[0]); 

La verdadera magia está sucediendo detrás GroupByOrder, que es un método de extensión.

public static IEnumerable<IEnumerable<T>> GroupByOrder<T, K>(this IEnumerable<T> source, Func<T, K> keySelector) where K : IComparable { 
    var prevKey = keySelector(source.First()); 
    var captured = new List<T>(); 
    foreach (var curr in source) { 
    if (keySelector(curr).CompareTo(prevKey) <= 0) { 
     yield return captured; 
     captured = new List<T>(); 
    } 
    captured.Add(curr); 
    } 
    yield return captured; 
} 

(Negación: idea robado a Tomás Petricek)

Sus datos de la muestra se obtienen los siguientes grupos, que ahora sólo tienen que ser analizado en su objeto de usuario.

User: 
    first_user_name 
    first_user_phone 
    first_user_fax 
User: 
    second_user_name 
User: 
    third_user_name 
    third_user_phone 
    third_user_fax 
    third_user_address 
User: 
    last_user_name 
    last_user_fax 
+0

hola fjdumont .. gracias por publicar .. Todavía estoy a favor de la respuesta de dtb, ya que es completa, es decir. toma una entrada de mensaje y devuelve una lista de usuarios. Además, su solución parece fallar cuando no hay datos de usuario presentes ... el ejemplo original solo parece funcionar con 200/300/405 campos eliminados – Chris

+0

@fjdumont ... ¿tiene un enlace al análisis de Tomas Petricek sobre esto? – Chris

+0

Tuve que buscarlo yo mismo, pero aquí está: http://tomasp.net/blog/custom-linq-grouping.aspx – fjdumont