tags:

views:

356

answers:

5

I have an odd sorting case I'm struggling to work out using LINQs GroupBy method.

I have two classes: Category and Item. Every Item has a category, and a Category can have a parent Category. What I need to do is organize all of the Items by their proper Category, but I also want to sort the Categories by the parent Category if there is one.

So ideally I should be able to visualize my results like:

<Category 1> 
  <Item 1>  
  <Item 2> 
</Category 1> 
<Category 2>
  <Category 3>  
    <Item 3>  
    <Item 4>  
  </Category 3>
</Category 2>  
<Category 4>  
  <Item 5>
</Category 4>
<Category 5>  
  <Item 6>
</Category 5>

I'm currently using items.GroupBy(x => x.Category) which gives me everything except the parent categories. So my results look like:

<Category 1> 
    <Item 1>  
    <Item 2> 
</Category 1> 

    <Category 3>  
      <Item 3>  
      <Item 4>  
    </Category 3>

<Category 4>  
  <Item 5>
</Category 4> 
<Category 5>  
  <Item 6>
</Category 5>

The issue being that (in this example) the parent category for Category 3 (Category 2) isn't listed.

I started goofing around with nested groups, but I didn't get very far before considering just manually walking the tree myself (foreach'ing). Before I do that, I'm hoping the LINQ gurus here can help me out...

+2  A: 

Well, what data type are you expecting to get back? Currently, it'll be IGrouping<Category, Item> but if you want the topmost category to be the key, then the values could presumably be items or categories.

You've shown the results as XML, but how are you actually going to use them? Given the results you've got, can't you easily get the parent category anyway? Do you need to use the parent category in the actual grouping part? If two categories have the same parent category, do you want all the items in that parent category to be mashed together?

Sorry for all the questions - but the more we know, the better we'll be able to help you.

EDIT: If you just want to group items by the topmost category, you can do

items.GroupBy(x => GetTopmostCategory(x))
...
public Category GetTopmostCategory(Item item)
{
    Category category = item.Category;
    while (category.Parent != null)
    {
        category = category.Parent;
    }
    return category;
}

(You could put this into Category or Item, potentially.) That would give you exactly the same return type, but the grouping would just be via the topmost category. Hope this is actually what you want...

Jon Skeet
I used xml-ish just as an example, but I'm actually shoving the results of the GroupBy method into a ViewModel which will end up getting rendered to an MVC view page via a couple foreach loops (one for the group key and one for the group items). I can get the parent category with the results I have, but the rendering then gets a little messy trying to figure out if I've already displayed the parent category, etc. I'm open to other ideas...
mannish
And when I say "shoving the results... into a ViewModel", what I mean is a property on a ViewModel that is currently of type IEnumerable<IGrouping<Category, Item>>.
mannish
So is that the type you actually want it to be? Do you actually want to group by the *top-most* category for any item? If so, that's not too hard... although you'll still need to do a bit of work to render everything, I suspect.
Jon Skeet
Yes, I'd like to group by the top most category. I figured I'd have to do a little bit of work, but I was hoping to leverage what Linq throws my way to alleviate some of that.
mannish
Editing answer...
Jon Skeet
That sort of works... I get the outermost category and all of the items underneath, but the issue is that I also need to be able to group by the inner categories as well. This is a hierarchical representation I need, meaning I need to show all of the categories and their associated items, nesting the groupings by the items category. Does that make sense? This is a step in the right direction, though, so I'll play around and see if I can modify it to do what I need.
mannish
It doesn't really make a lot of sense to me, I'm afraid. If you could explain what output type you want in code rather than XML, that would make it a lot clearer.
Jon Skeet
huh, I thought my psuedo xml would be pretty easy to visualize compared to code. I just want to be able to display, to the user, a category, with it's subcategories and any items that are grouped by those categories. But I only want to show the category or subcategory label once. I found another solution that is working for me that I'll drop in as an edit.
mannish
I dropped in an answer instead of an edit as it's how I ended up resolving my problem. Not the cleanest on the rendering side, I'll admit, but it works for now.
mannish
The pseudo-xml shows what you want to display, but it doesn't give *any* idea of what collection type you want to work with. Will you only ever have two levels of hierarchy? If not, I suspect your current solution may become very complicated...
Jon Skeet
Honestly, I don't care what collection "type" I would get back as long as it is relatively painless to iterate that collection and display the items in one fashion or another. My current solution assumes we won't have anything deeper than one child on a top most category. For the business problem that's fine due to the constraints on the data that will ensure that's always the case. As a general solution, however, I know this should be capable of handling children with children. I don't want to mash the items of child categories together and I don't simply want to... (cont.)
mannish
see only the top most category.
mannish
A: 

Look at this blog. I like the ByHierarchy extension method

Vasu Balakrishnan
+1  A: 

If you had a tree to walk, you'd already have items grouped by category. Do you control the interface of your view?

public abstract class TreeNode {
  private readonly int name;
  private Category c = null;

  public int Name { get { return name; } }
  public Category Parent { get { return c; } }
  public abstract string Tag { get; }

  public TreeNode(int n, Category c) {
    this.name = n;
    AssignCategory(c);
  }

  public void AssignCategory(Category c) {
    if (c != null) {
      this.c = c;
      c.AddChild(this);
    }
  }

  public virtual IList<TreeNode> Children {
    get { return null; }
  }
}

Item and Category look like

public class Item : TreeNode {
  public Item(int n, Category c) : base(n, c) {}

  public override string Tag { get { return "Item"; } }
}

public class Category : TreeNode {
  List<TreeNode> kids = new List<TreeNode>();

  public Category(int n, Category c) : base(n, c) {}

  public void AddChild(TreeNode child) {
    kids.Add(child);
  }

  public override string Tag { get { return "Category"; } }

  public override IList<TreeNode> Children {
    get { return kids; }
  }
}

Then you can show them with, say, a corny console display:

public class CornyTextView {
  public int NodeDepth(TreeNode n) {
    if (n.Parent == null)
      return 0;
    else
      return 1 + NodeDepth(n.Parent);
  }

  public void Display(IEnumerable<TreeNode> nodes) {
    foreach (var n in nodes.OrderBy(n => n.Name)) {
      for (int i = 0; i < NodeDepth(n); i++)
        Console.Write("  ");

      Console.WriteLine("- " + n.Tag + " " + n.Name.ToString());
      if (n.Children != null)
        Display(n.Children);
    }
  }
}

So to generate output for your example:

public static void Main() {
  var cats = new [] {
    new Category(1, null),
    new Category(2, null),
    new Category(3, null),
    new Category(4, null),
    new Category(5, null),
  };
  cats[2].AssignCategory(cats[1]);

  var items = new[] {
    new Item(6, cats[4]),
    new Item(5, cats[3]),
    new Item(3, cats[2]), new Item(4, cats[2]),
    new Item(1, cats[0]), new Item(2, cats[0]),
  };

  new CornyTextView()
        .Display(cats.Where(c => c.Parent == null)
                     .Select(c => c as TreeNode));
}

Notice that even though items is shuffled, the output is

- Category 1
  - Item 1
  - Item 2
- Category 2
  - Category 3
    - Item 3
    - Item 4
- Category 4
  - Item 5
- Category 5
  - Item 6
Greg Bacon
This is definitely a workable solution and gets what I want in terms of display. But when I pull my date out of the db, my items and categories already have relationships, so rebuilding them into a Tree seems like an additional step. I just wanted to be able to group items hierarchically that are already related in an object graph.
mannish
A: 

This is how I resolved my problem:

var items = Items.GroupBy(x => x.Category).GroupBy(y => y.Key.Parent);

Which allowed me to do the following (nasty) rendering markup:

<table>
    <tr>
        <th>Item Description</th>
        <th>Value</th>
    </tr>
    <% foreach (var parentGroup in Model.Items) { %>
        <% if (parentGroup.Key != null) { %>

        <tr>
            <th colspan="2" style="background-color:#ff9900;"><%= parentGroup.Key.Label %></th>
        </tr>

            <% foreach (var childGroup in parentGroup) { %>
                <tr>
                    <th colspan="2"><%= childGroup.Key.Label %></th>
                </tr>
                <% foreach (var item in childGroup) { %>
                    <tr>
                        <td><%= item.Description %></td>
                        <td><%= String.Format("{0:c}", item.Value) %></td>
                    </tr>
                <% } %>
            <% } %>
        <% } else { %>
            <% foreach (var childGroup in parentGroup) { %>
                <tr>
                    <th colspan="2" style="background-color:#ff9900;"><%= childGroup.Key.Label %></th>
                </tr>
                <% foreach (var item in childGroup) { %>
                    <tr>
                        <td><%= item.Description %></td>
                        <td><%= String.Format("{0:c}", item.Value) %></td>
                    </tr>
                <% } %>
            <% } %>

        <% } %>
    <% } %>
</table>

I'd love to consolidate this tag soup, but I'll have to go with this for now.

mannish
A: 

I have a feeling that org.ccil.cowan.tagsoup removes style attributes from td tag. eg...Bill From NetFlow Analyzer after applying commandline.process it returns only Bill From NetFlow Analyzer.

Sathish