LINQ: ToLookup

We’ve seen how to create a Dictionary using LINQ. A Dictionary is a hash table in which only one object may be stored for each key. It can also be useful to store more than one object for a given key, and for that, the C# Lookup<> (part of the System.Linq namespace) generic type can be used.

LINQ provides the ToLookup() method for creating Lookups. It works in much the same way as ToDictionary(), except that as many objects as you like can be attached to each key.

Returning to our example using Canadian prime ministers, we can create a Lookup in which the key is the first letter of the prime minister’s last name. The code is

      PrimeMinisters[] primeMinisters = PrimeMinisters.GetPrimeMinistersArray();
      var pmLookup01 = primeMinisters.ToLookup(pm => pm.lastName[0]);
      var keys01 = pmLookup01.Select(pm => pm.Key).OrderBy(key => key);
      Console.WriteLine("----->pmLookup01");
      foreach (var key in keys01)
      {
        Console.WriteLine("PMs starting with {0}", key);
        foreach (var pm in pmLookup01[key])
        {
          Console.WriteLine("  -  {0}, {1}", pm.lastName, pm.firstName);
        }
      }

This is the simplest version of ToLookup(). The method takes a single argument, which is a function specifying how to calculate the key. In this case, we just take the first char in the string pm.lastName.

For some reason, the Lookup class doesn’t contain a property for retrieving the list of keys, so we need to use a roundabout method to get them. Line 5 uses a Select() to retrieve the keys and an OrderBy() to sort them into alphabetical order. We can then iterate over the keys and, for each key, we can iterate over the prime ministers for that key. Note that the object pmLookup01[key] is not a single object; rather it contains a list of all prime ministers whose last name begins with the letter contained in the key.

The output from this code is:

----->pmLookup01
PMs starting with A
  -  Abbott, John
PMs starting with B
  -  Bowell, Mackenzie
  -  Borden, Robert
  -  Bennett, Richard
PMs starting with C
  -  Clark, Joe
  -  Campbell, Kim
  -  Chrétien, Jean
PMs starting with D
  -  Diefenbaker, John
PMs starting with H
  -  Harper, Stephen
PMs starting with L
  -  Laurier, Wilfrid
PMs starting with M
  -  Macdonald, John
  -  Mackenzie, Alexander
  -  Meighen, Arthur
  -  Mackenzie King, William
  -  Mulroney, Brian
  -  Martin, Paul
PMs starting with P
  -  Pearson, Lester
PMs starting with S
  -  St. Laurent, Louis
PMs starting with T
  -  Thompson, John
  -  Tupper, Charles
  -  Trudeau, Pierre
  -  Turner, John

We can do the same thing using the second form of ToLookup(), which allows us to specify an EqualityComparer to be used in determining which keys are equal. The comparer class looks like this:

using System.Collections.Generic;

namespace LinqObjects01
{
  class LookupComparer : IEqualityComparer<string>
  {
    public bool Equals(string x, string y)
    {
      return x[0] == y[0];
    }

    public int GetHashCode(string obj)
    {
      return obj[0].GetHashCode();
    }
  }
}

This comparer compares two strings and says they are equal if their first characters are equal. Using this class, we can apply the second form of ToLookup():

      PrimeMinisters[] primeMinisters = PrimeMinisters.GetPrimeMinistersArray();
      var pmLookup02 = primeMinisters.ToLookup(pm => pm.lastName,
        new LookupComparer());
      var keys02 = pmLookup02.Select(pm => pm.Key).OrderBy(key => key);
      Console.WriteLine("----->pmLookup02");
      foreach (var key in keys02)
      {
        Console.WriteLine("PMs starting with {0}", key[0]);
        foreach (var pm in pmLookup02[key])
        {
          Console.WriteLine("  -  {0}, {1}", pm.lastName, pm.firstName);
        }
      }

The first argument to ToLookup() now passes the entire pm.lastName, and the second argument to ToLookup() is the comparer object.

When we print out the results, we have to remember that the key for each entry in the Lookup is now the full last name of the first prime minister encountered whose name starts with a given letter. Thus if we printed out the full key, we’d get a full last name. That’s why we print out key[0] on line 8; that way we get the first letter of the name.

The third version of ToLookup() allows us to specify a custom data type to return. If we wanted just the first and last names of each prime minister, for example, we could write:

      PrimeMinisters[] primeMinisters = PrimeMinisters.GetPrimeMinistersArray();
      var pmLookup03 = primeMinisters.ToLookup(pm => pm.lastName[0],
        pm => new
        {
          lastName = pm.lastName,
          firstName = pm.firstName
        });
      var keys03 = pmLookup03.Select(pm => pm.Key).OrderBy(key => key);
      Console.WriteLine("----->pmLookup03");
      foreach (var key in keys03)
      {
        Console.WriteLine("PMs starting with {0}", key);
        foreach (var pm in pmLookup03[key])
        {
          Console.WriteLine("  -  {0}, {1}", pm.lastName, pm.firstName);
        }
      }

We’ve returned to using the first letter of the last name as the key (that is, there’s no comparer), and passed in an anonymous data type as the second argument to ToLookup(). Apart from that, the code is the same as in the first example.

Finally, the fourth version allows us to specify both a custom data type and a comparer, so we can combine that last two examples to get this:

      PrimeMinisters[] primeMinisters = PrimeMinisters.GetPrimeMinistersArray();
       var pmLookup04 = primeMinisters.ToLookup(pm => pm.lastName,
       pm => new
        {
          lastName = pm.lastName,
          firstName = pm.firstName
        },
        new LookupComparer());
       var keys04 = pmLookup04.Select(pm => pm.Key).OrderBy(key => key);
       Console.WriteLine("----->pmLookup04");
       foreach (var key in keys04)
       {
         Console.WriteLine("PMs starting with {0}", key[0]);
         foreach (var pm in pmLookup04[key])
         {
           Console.WriteLine("  -  {0}, {1}", pm.lastName, pm.firstName);
         }
       }

Now we’re back to using the full last name as the key, since the comparer does the checking for matching first letters. Just make sure you pass in the arguments in the right order: (1) choose key; (2) choose custom data type; (3) choose comparer.

Advertisements
Post a comment or leave a trackback: Trackback URL.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: