Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replace for-switch loop with a Linq query

Tags:

c#

linq

I have a Message object which wraps a message format I do not have control over. The format is a simple list of Key/Value pairs. I want to extract a list of Users from a given Message. 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 set. The initial keys 200/300....405 represent fields I don't need and can skip to get to the User data.

Each users data is in consecutive fields but the number of fields varies depending on how much information is known about a user. The following method does what I'm looking for. It uses an enumeration of possible key types and a method to find 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;
}

I'm wondering if its possible to replace the method with a Linq query. I'm having trouble telling Linq to select a new user and populate its fields with all matching data until you find the start of the next user entry.

Note: Relative key numbers are random (not 1,2,3,4) in the real message format.

like image 707
Chris Avatar asked Sep 30 '11 08:09

Chris


2 Answers

I don't see the benefit in changing your code to a LINQ query, but it's definitely possible:

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

using

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;
}
like image 103
dtb Avatar answered Oct 04 '22 17:10

dtb


No, and the reason being, in general, most LINQ functions, in the same way as SQL queries, deal with unordered data, i.e. they don't make assumptions about the order of the incoming data. That gives them flexibility to be parallelized, etc. Your data has intrinsic order, so doesn't fit the model of querying.

like image 31
Tim Rogers Avatar answered Oct 04 '22 16:10

Tim Rogers