Replace for-switch loop with Linq request

I have a Message object that wraps a message format in which I do not control. The format is a simple list of key / value pairs. I want to extract a list of users from this post. For example, given the following message ...

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  

I want to extract four users with the provided properties. The initial keys 200/300 .... 405 represent fields that I do not need, and I can skip to go to the user data.

Each user’s data is in consecutive fields, but the number of fields depends on how much information is known about the user. The following method does what I am looking for. It uses an enumeration of possible key types and a method to search for the index of the first field with user data.

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

, Linq. , Linq , .

. ( 1,2,3,4) .

+5
4

LINQ, :

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

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

List<List<KeyValuePait<int, string>>>, List<KeyValuePair<int, string>> . - :

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

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

, , LINQ, SQL-, , .. . .. , .

+1

I don’t think there is any kind of performance advantage, but in my opinion it improves readability.

A possible solution might look like this:

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

The real magic comes from GroupByOrder, which is an extension method.

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

(Disclaimer: idea stolen from Tomasz Petrichek)

The following groups are listed in your sample data that you now just need to analyze in the user object.

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
+1
source

All Articles