Showing posts with label LINQ. Show all posts
Showing posts with label LINQ. Show all posts

Tuesday, October 19, 2010

Creating a ToDictionaryList Extension

ToDictionary

.Net 3.5 introduced the Enumerable.ToDictionary Method. It allows you to take a list of items, and convert them into a dictionary based on some key given as a lambda expression. For example, this code snippet for Snippet Compiler will put a weeks worth of dates into a Dictionary<int,DateTime>, so you could do a dictionary lookup by day:

public static void RunSnippet()
{
DateTime time = new DateTime(2228, 3, 22);
List<DateTime> items = new List<DateTime>();
items.Add(time);
items.Add(time.AddDays(1));
items.Add(time.AddDays(2));
items.Add(time.AddDays(3));
items.Add(time.AddDays(4));
items.Add(time.AddDays(5));
items.Add(time.AddDays(6));

var dayByDay = items.ToDictionary(d => d.Day);
foreach (var dayKeyValue in dayByDay)
{
WL(dayKeyValue.Key + " " + dayKeyValue.Value);
}
}

Which results in this output:

22 3/22/2228 12:00:00 AM
23 3/23/2228 12:00:00 AM
24 3/24/2228 12:00:00 AM
25 3/25/2228 12:00:00 AM
26 3/26/2228 12:00:00 AM
27 3/27/2228 12:00:00 AM
28 3/28/2228 12:00:00 AM
Press any key to continue...



ToDictionaryList


But what if the key you want to use, is not unique? You’ll end up getting this warning:

System.ArgumentException: An item with the same key has already been added.
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement](IEnumerable`1 source, Func`2 keySelector, Func`2 elementSelector, IEqualityComparer`1 comparer)

I had this same issue and decided to create a ToDictionaryList() extension method that would return a Dictionary<key,List<item>>. Here is an example using the ToDictionaryList() grouping the dateTimes by day:



public static void RunSnippet()
{
DateTime time = new DateTime(2228, 3, 22);
List<DateTime> items = new List<DateTime>();
items.Add(time);
items.Add(time.AddHours(1));
items.Add(time.AddMinutes(1));
items.Add(time.AddDays(1));
items.Add(time.AddHours(26));

var results = items.ToDictionaryList(d => d.Day);
foreach (var result in results)
{
foreach (var item in result.Value)
{
WL(result.Key + " " + item);
}
}
}


Which results in this output:

22 3/22/2228 12:00:00 AM
22 3/22/2228 1:00:00 AM
22 3/22/2228 12:01:00 AM
23 3/23/2228 12:00:00 AM
23 3/23/2228 2:00:00 AM
Press any key to continue...


Basically it is two dictionary entries, one for the 22nd which is a List<DateTime> of 3 DateTimes, and one for the 23, which contains two items. You can even write a lambda expression for the type of item to be in the list. So in the last example, if you changed "var results = items.ToDictionaryList(d => d.Day);" to "var results = items.ToDictionaryList(d => d.Day, d => d.Hour); " item would in int and which would result in this output:

22 0
22 1
22 0
23 0
23 2
Press any key to continue...


The Code


Here is the code required to make the ToDictionaryList Possible:


public static class Extensions
{
private static Func<TElement, TElement> Instance<TElement>()
{
return delegate(TElement x) { return x; };
}

public static Dictionary<TKey, List<TSource>> ToDictionaryList<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
return source.ToDictionaryList(keySelector, Instance<TSource>());
}

public static Dictionary<TKey, List<TElement>> ToDictionaryList<TSource, TKey, TElement>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector)
{
Dictionary<TKey, List<TElement>> dictionary = new Dictionary<TKey, List<TElement>>();
List<TElement> elements;
foreach (TSource local in source)
{
if (!dictionary.TryGetValue(keySelector(local), out elements))
{
elements = new List<TElement>();
dictionary.Add(keySelector(local), elements);
}
elements.Add(elementSelector(local));
}

return dictionary;
}
}



The Instance Method is used to return a delegate function that returns a single item of the given type (Basically a (c => c)). The ToDictionaryList() will loop through all items in the list, adding them to a new list if the key doesn’t already exist in the dictionary, or add the item to the already existing list in the dictionary. Pretty simple really.

Smile

Saturday, February 7, 2009

Easing Constraints Without Duplicating Queries

I had a rather common problem at work that I created a rather elegent solution for.  Lets say for instance you had a database table that contained a list of businesses, the city they operated in, the state they operated in, and their FEIN.  You were given a list of business names, and asked to find the correct FEIN for each business. You need to search for the business by name, city, and state first. If you don't find it, then search for the business by name and city. If you still don't find it, you finally just need to search by the name, returning the first record each time if there are duplicates. You've used LINQ to SQL to get a collection of the data, and now you need to return the best match.  At first you may do what I did at first, something like this:

var feins1 = from business in Database
where business.Name == name
&& (business.City == city)
&& (
business.State == state)
select business.FEIN;

if (feins1.Count() > 0) {
return feins1.First();
}

var feins2 = from business in Database
where business.Name == name
&& (business.City == city)
select business.FEIN;

if (feins2.Count() > 0) {
return feins2.First();
}

var feins3 = from business in Database
where business.Name == name
select business.FEIN;

if (feins3.Count() > 0) {
return feins3.First();
}


You reach for the ctrl-c and ctrl-v on your keyboard, and suddenly feel really dirty. What if another criteria needs to be added? You'll have to add another criteria to each where statement, as well as adding an entirely new query. I spent a few minutes thinking about it, and came up with this solution:

    for (int i = 0; i < 3; i++) {
fein = GetBestFEIN("Good Times Inc", "Indianapolis", "IN", i > 1, i > 0);

if (fein != string.Empty) {
break;
}
}
return fein;
}

private string GetBestFEIN(string name, string city, string state, bool skipCity, bool skipState) {

var Database = from i in new string[] { "0", "1", "2", "3" }
select new { Name = i, FEIN = i, City = i, State = i };

var feins = from business in Database
where business.Name == name
&& (
skipCity || business.City == city)
&& (
skipState || business.State == state)
select business.FEIN;

if (feins.Count() == 0) {
return string.Empty;
}
else {
return feins.First();
}
}





Now if another criteria is added, you just need to increase the for each loop by one, and add another parameter. I made my change, and went home feeling a lot cleaner.