2011-09-30 8 views
5

Ho un oggetto Message che racchiude un formato di messaggio su cui non ho il controllo. Il formato è un semplice elenco di coppie chiave/valore. Voglio estrarre un elenco di utenti da un determinato messaggio. Per esempio dato il seguente messaggio ...Sostituisci il ciclo per switch con una query 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 

Voglio estrarre quattro Utenti con le proprietà fornite impostate. I tasti iniziali 200/300 .... 405 rappresentano campi non necessari e possono saltare per accedere ai dati utente.

Ogni dato utente è in campi consecutivi ma il numero di campi varia a seconda della quantità di informazioni che si conoscono su un utente. Il seguente metodo fa quello che sto cercando. Utilizza un'enumerazione di possibili tipi di chiave e un metodo per trovare l'indice del primo campo con i dati dell'utente.

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

Mi chiedo se è possibile sostituire il metodo con una query Linq. Ho difficoltà a comunicare a Linq di selezionare un nuovo utente e popolare i suoi campi con tutti i dati corrispondenti finché non trovi l'inizio della successiva voce utente.

Nota: i numeri chiave relativi sono casuali (non 1,2,3,4) nel formato di messaggio reale.

+0

stai usando Resharper? è abbastanza bravo nel refactoring dei loop alle espressioni LINQ. –

+2

Quale sarebbe il vantaggio di trasformare questo in una query LINQ? Il tuo codice mi sembra buono così com'è. – dtb

+0

@Marian: solo dopo 5.x IIRC – sehe

risposta

5

non vedo il vantaggio nel cambiare il proprio codice a una query LINQ, ma è sicuramente possibile:

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

utilizzando

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: risponde alla domanda di OP ed è * abbastanza convincente * a dirgli di lascia il suo bel codice così com'è. – ANeves

+1

@dtb .. yup versione modificata funziona a meraviglia .. haha ​​.. non del tutto sicuro capisco esattamente come ancora ma grazie ancora .. è bello vedere c'è sempre un modo per fare qualcosa .. e sto imparando di più su Linq passando attraverso il tuo codice – Chris

1

Come dividere il messaggio in uno List<List<KeyValuePait<int, string>>> dove ogni List<KeyValuePair<int, string>> rappresenta un singolo utente. È quindi possibile fare qualcosa di simile:

// 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 .. ciao Joey .. grazie per il tuo post .. stato giocherellare con il codice di dtb .. che funziona a meraviglia ora (grazie dtb) .. Dovrei reimplementare il mio utente classe per lavorare con ConstructUser as is e Id sta leggendo il messaggio più di una volta in questo modo .. ma è interessante vedere i diversi approcci! grazie ancora – Chris

+0

Nessun problema. Penso che la soluzione migliore sia ancora quella che hai iniziato. Sebbene forse provi a rifattorizzare il passaggio in Utente o in qualche oggetto UserBuilder. – Joey

+0

+1 .. yup questo è un buon suggerimento .. passare un enumerabile di campi divisi come suggerito/tramite GroupAdjacent o simili e lasciare che si prenda cura della propria creazione, il formato utente è improbabile che cambi (Ahem!) ... quindi penso che andrei con Utente piuttosto che UserFactory/UserBuilder ma anche se mettere il codice di creazione nell'Utente sarebbe più bello che averlo all'interno dello switch in un'altra posizione. Grazie Joey. – Chris

1

No, e la ragione di essere, in generale, la maggior parte delle funzioni di LINQ, nello stesso modo in cui le query SQL, trattano i dati non ordinati, vale a dire non fanno ipotesi sull'ordine dei dati in arrivo. Ciò dà loro la possibilità di essere parallelizzati, ecc. I tuoi dati hanno un ordine intrinseco, quindi non si adattano al modello di query.

+0

@tim .. yup stavo pensando che quando ho scritto il ciclo ma ogni volta che ho saltato attraverso di esso ho avuto questa sensazione assurda che fosse possibile .. – Chris

1

io non credo che ci sia alcun miglioramento delle prestazioni , ma aumenta la leggibilità molto secondo me.

Una possibile soluzione potrebbe essere la seguente:

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

La vera magia sta accadendo dietro GroupByOrder, che è un metodo di estensione.

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

(Diniego: idea rubato da Tomas Petricek)

I suoi dati di esempio produce i seguenti gruppi, che ora solo devono essere analizzati nel vostro oggetto d'uso.

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

hi fjdumont .. grazie per la pubblicazione .. Sto ancora favorendo la risposta di dtb in quanto è completa cioè. accetta un messaggio e restituisce un elenco di utenti. Inoltre, la tua soluzione sembra interrompersi quando i dati non dell'utente sono presenti ... l'esempio originale sembra funzionare solo con i campi 200/300/405 rimossi – Chris

+0

@fjdumont .. hai un link alla discussione di Tomas Petricek su questo? – Chris

+0

Ho dovuto google da solo, ma eccolo: http://tomasp.net/blog/custom-linq-grouping.aspx – fjdumont