Sunday, April 6, 2008

Playing around with LINQ to SQL: Enumerable.ToDictionary Extension Method

Playing around with Linq I noticed the Enumerable.ToDictionary extension method on IEnumerables<T>. I found it very handy to store expected results for Unit Testing. Yesterday I was playing around with general stuff and I did some looking around a ToDictionary.

We all know what Dictionaries are in the programming world right? Well they are data structures where each element is a key/value pair. Each key in it must be unique and it cannot be null. They are blindly fast for element retrieval by key too, O(n) . So basically they are very efficient at locating, inserting, deleting records associated by keys,

The ToDictionary<TSource,TKey> can be used in many ways, 4 to be more precise but in all them you have to specify a Func<TSource,TElement> to be your key selector, you also have the choice to provide another function to your element selector, ie the object that you are going to store as key and finally you can pass on a IEqualityComparer<T> to compare keys so the underlying HashTable can determine whether keys used are equal. (If you try to add an Element with a Key that already exists in the Dictionary, an ArgumentException will be thrown.

Examples:

 

[TestMethod]
public void CreateDictionary_of_AnonymousTypes()
{
var ps = from p in productos
orderby p.ProductID ascending
select new
{ p.ProductID,p.ProductName};

// Creates a Dictionary where the Key is a productID and the Object is an Anonymous type.
var dummyDictionary = ps.ToDictionary(a => a.ProductID); // Note the Ano

// 77 Products in Northwind...
Assert.AreEqual(77, dummyDictionary.Count());
Assert.AreEqual("Tofu", dummyDictionary[14].ProductName);
// Is it a fib that oo.ProductName is not 'Chai tea' ?
Assert.IsFalse(dummyDictionary[1].ProductName != "Chai tea"); // Product Name of Product with ID = 1 is 'Chai tea'
}

In this example all I did was crated a Dictionary by using the productID as key selector, the dictionary will have a integer as key and the Element will be an Anonymous type.

[TestMethod]
public void CreateDictionary2()
{
var ps = from p in productos
orderby p.ProductID ascending
select new
{ p.ProductID, p.ProductName };

// Creating a dictionary by explicitly determining the Key and the Element
Dictionary<int,string> elDictionary = ps.ToDictionary(a => a.ProductID, a=>a.ProductName);
CollectionAssert.AllItemsAreUnique(elDictionary.Keys);
}

This example a function to select keys and select Elements was done, resulting in a Dictionary<int,String>

[TestMethod]
public voidDictionary_Products_By_CategoryName()
{
       Dictionary<string, IEnumerable<Product>> superDictionary;        
          
        var prods = fromp inproductos
                    groupp byp.Category.CategoryName intoprods_by_categoryName
                    select new
                  
{                           
                        list = prods_by_categoryName
                    };
        superDictionary = prods.ToDictionary(a => a.list.Select(b => b.Category.CategoryName).First(), c => c.list.Select(y=>y));
        IOrderedEnumerable<String> coll = superDictionary.Keys.OrderBy(a=>a);

        IEnumerable<String> orderedCategories = ((fromcats inctx.Categories
                                                     selectcats.CategoryName).OrderBy(h=>h));
        List<String> myList = coll.ToList();
        myList.Reverse();
        myList.Reverse();
        CollectionAssert.AreEqual(myList , orderedCategories.ToList());
       
}


This example I created a Dictionary of Products, where the key is the CategoryName and the Elements is a IEnumberable of Products for that Category.

The same can be achieved as below, much simpler!


[TestMethod]
public void Dictionary_Products_By_CategoryName_Simple()
{
Dictionary<string, IEnumerable<Product>> superDictionary;
superDictionary = (from categories in ctx.Categories
select new
{
categories.CategoryName,
ProductsInCategory = categories.Products.Select(a=>a)
}).ToDictionary(a=>a.CategoryName,a=>a.ProductsInCategory);

Assert.IsInstanceOfType(superDictionary, typeof(Dictionary<string, IEnumerable<Product>>));
}

Here I want to list all Products by Category, and sort them by total unit price...


[TestMethod]
public void do_all_that_stuff_in_less_lines_of_code()
{
NorthwindDataContext ctx = new NorthwindDataContext();
var result = (from categories in ctx.Categories
select new
{
CategoryName = categories.CategoryName,
TotalCategoryUnitPrice = categories.Products.Sum(a=>a.UnitPrice),
Products = categories.Products.OrderByDescending(a=>a.UnitPrice).ToList()
}).OrderByDescending(a=>a.TotalCategoryUnitPrice);

foreach (var category in result)
{
Console.WriteLine( String.Format("\n{0}:\n{1}",category.CategoryName, new String('-', category.CategoryName.Length )));
category.Products.ForEach(p => Console.WriteLine(String.Format("{0,-35} {1,10:C2}", p.ProductName, p.UnitPrice)));
Console.WriteLine(String.Format( "{0,46}", new String('-',6) ));
Console.WriteLine("Total:".PadLeft(38) + String.Format("{0,8:C2}", category.TotalCategoryUnitPrice));
}
}

Capture


Also the ToLookup function is worth mentioning. What it does is creates a Lookup<TKey,TElement> ( basically a collection where each key is mapped to one or more values)


[TestMethod]
public void ToLookup()
{
var prods_by_cat = from products in ctx.Products
select products;

Lookup<string, string> ellookup = (Lookup<string, string>)prods_by_cat.ToLookup(
a => a.Category.CategoryName, // Key
a => a.ProductName); // Value
// Iterate through each IGrouping in the Lookup and output the contents.
foreach (IGrouping<string, string> packageGroup in ellookup)
{
// Print the key value of the IGrouping.
Console.WriteLine(packageGroup.Key);
// Iterate through each value in the IGrouping and print its value.
foreach (string str in packageGroup)
Console.WriteLine(" {0}", str);
}
}

and the output..note that packageGroup is just a collection of Products for that particular Category. Cool eh?


Beverages
Chai tea
Changta
Guaraná Fantástica
Steeleye Stout
Côte de Blaye
Chartreuse verte
Ipoh Coffee
Laughing Lumberjack Lager
Outback Lager
Rhönbräu Klosterbier
Lakkalikööri
Condiments
Aniseed Syrup
Chef Anton's Cajun Seasoning
Chef Anton's Gumbo Mix
Grandma's Boysenberry Spread
Northwoods Cranberry Sauc

Anyway, I don't think I'll be playing with any Dictionaries for a while, done and dusted!

No comments: