views:

166

answers:

3

How would one write a LINQ query which takes a hierarchical source data and transforms it so that the grouping is inverted?

Say I have a list of Topic objects each of which contains a collection of Tags which represent meta-data tags on that topic. What I need is to write a LINQ query to basically flip the hierarchy inside out so that I have a list of Tags each of which have a collection of topics that are tagged with that particular tag.

Topic { Title = "Political Debate #1", Posted = 01/02/2008 }
   Tag { Name = "Contraversial", Color = "Red" }
   Tag { Name = "Politics", Color = "LightBlue" }
Topic { Title = "iPhone to support SiliverLight!", Posted = 02/23/2009 }
   Tag { Name = "BleedingEdge", Color = "LightBlue" }
   Tag { Name = "Contraversial", Color = "Red" }
   Tag { Name = ".NET", Color = "LightGreen" }
Topic { Title = "Fed Chairman admits guilt for causing second Great Depression", Posted = 06/15/2010 }
   Tag { Name = "Politics", Color = "LightBlue" }
   Tag { Name = "Contraversial", Color = "Red" }

I want the above data to look instead like the results below.

Tag { Name = "Contraversial", Color = "Red" }
    Topic { Title = "Political Debate #1", Posted = 01/02/2008 }
    Topic { Title = "iPhone to support SiliverLight!", Posted = 23/02/2009 }
    Topic { Title = "Fed Chairman admits guilt for causing second Great Depression", Posted = 06/15/2010 }
Tag { Name = "Politics", Color = "LightBlue" }
    Topic { Title = "Political Debate #1", Posted = 01/02/2008 }
    Topic { Title = "Fed Chairman admits guilt for causing second Great Depression", Posted = 06/15/2010 }
Tag { Name = ".NET", Color = "LightGreen" }
    Topic { Title = "iPhone to support SiliverLight!", Posted = 23/02/2009 }

You can assume that any repeated piece of data is referentially unique in that that is a single instance in memory and these there are just several references to that same object. Also it is reasonable for the answer to use anonymous classes to produce the projection since I realize the shape of the classes may be slightly different after the inversion.

UPDATE: I added the code below which sets up the example data. I'm playing around with the answers posted and some of my own ideas in LinqPad.

var tags = new[]
{
    new { Name = "Contraversial", Color = "Red" },
    new { Name = "Politics", Color = "LightBlue" },
    new { Name = ".NET", Color = "LightGreen" },
    new { Name = "BleedingEdge", Color = "LightBlue" }

};

var topics = new[]
{
    new 
    { 
     Title = "Political Debate #1", 
     Posted = DateTime.Parse("01/02/2008"), 
     Tags = (from t in tags where new []{"Contraversial", "Politics"}.Contains(t.Name) select t),
    },
    new 
    { 
     Title = "iPhone to support SiliverLight!", 
     Posted = DateTime.Parse("02/23/2009"), 
     Tags = (from t in tags where new []{"BleedingEdge", "Contraversial", ".NET", }.Contains(t.Name) select t),
    },
    new 
    { 
     Title = "Fed Chairman admits guilt for causing second Great Depression", 
     Posted = DateTime.Parse("06/15/2010"), 
     Tags = (from t in tags where new []{"Contraversial", "Politics"}.Contains(t.Name) select t),
    },
};
+3  A: 

What you're looking for is a Pivot.

http://stackoverflow.com/questions/167304/is-it-possible-to-pivot-data-using-linq

This source contains C# code for a Linq Pivot extension method:

public static class LinqExtensions 
{

    public static Dictionary<TFirstKey, Dictionary<TSecondKey, TValue>> Pivot<TSource, TFirstKey, TSecondKey, TValue>(this IEnumerable<TSource> source, Func<TSource, TFirstKey> firstKeySelector, Func<TSource, TSecondKey> secondKeySelector, Func<IEnumerable<TSource>, TValue> aggregate) 
    {
        var retVal = new Dictionary<TFirstKey, Dictionary<TSecondKey, TValue>>();

        var l = source.ToLookup(firstKeySelector);
        foreach (var item in l) 
        {
            var dict = new Dictionary<TSecondKey, TValue>();
            retVal.Add(item.Key, dict);
            var subdict = item.ToLookup(secondKeySelector);
            foreach (var subitem in subdict) 
            {
                dict.Add(subitem.Key, aggregate(subitem));
            }
        }

        return retVal;
    }

}
Robert Harvey
A pivot may work for the specific inversion case that I specified but what I'm looking for is a way to "re-group" an arbitrary hierarchy into one that is organized differently.
jpierson
A: 
IDictionary<Topic, IList<Tag>> data;
var n = data.SelectMany(x => x.Value.Select(y => new { Topic = x.Key, Tag = y }))
  .GroupBy(x => x.Tag, x => x.Topic);
queen3
This may work for a dictionary example but I'm looking more for an example that works with a natural object hierarchy. Please see the my updated post which contains an example setup.
jpierson
A: 

After playing around in LinqPad a bit I think I may have found a suitable solution.

Here is the simple example.

var topicsByTags = 
    from topic in topics
    from tag in topic.Tags
    group topic by tag;

And in order to get rid of the redundant Tags collection under each topic we can do the following.

var topicsByTags = 
    from topic in topics
    from tag in topic.Tags
    group new 
    {
        Title = topic.Title,
        Color = topic.Posted,
    } by tag into g
    select new
    {
     g.Key.Name,
     g.Key.Color,
     Topics = g,
    };

UPDATE: Below is another alternative which takes advantage of the grouping itself in the projection. Upside is slightly cleaner query, downside is that the group Key sticks around with the group even if it's not going to be used.

var topicsByTags = 
    from topic in topics
    from tag in topic.Tags
    group new 
    {
     Title = topic.Title,
     Color = topic.Posted,
    } by tag into g
    select new
    {
     g.Key.Name,
     g.Key.Color,
     Topics = g,
    };

I'll hold off accepting my own answer to allow some debate over which solution solves the problem that I posed the best.

jpierson