2

I couldn't find or come up with a generic and elegant algorithm that would let me populate the tree-like structure. The simplest example is a blog archive: I have a bunch of records which can be selected and sorted by date. I want to have a tree where years may be top level, months are next level and actual post titles are next again.

So far I've came up with a naive straight forward implementation that works, but I'm sure that could be improved using LINQ, etc. Here I just sort records by date and iterate checking if the year or month has changed, and add tree nodes accordingly. "BlogEntry" is a class that has a reference to both a parent and children and is later used to generate HTML.

I welcome suggestions on improving the algorithm!

IEnumerable<Post> posts = db.Posts.OrderBy(p => p.DateCreated);
var topPost = posts.First();

int curYear = topPost.DateCreated.Year;
int curMonth = topPost.DateCreated.Month;

//create first "year-level" item
var topYear = new BlogEntry { Name = topPost.DateCreated.Year.ToString().ToLink(string.Empty) };
entries.Add(topYear);
var currentYear = topYear;

var topMonth = new BlogEntry { Name = topPost.DateCreated.ToString("MMMM").ToLink(string.Empty), Parent = currentYear };
currentYear.Children.Add(topMonth);
var currentMonth = topMonth;

foreach (var post in posts)
{
    if(post.DateCreated.Year == curYear)
    {
        if (post.DateCreated.Month != curMonth)
        {
            //create "month-level" item
            var month = new BlogEntry { Name = post.DateCreated.ToString("MMMM").ToLink(string.Empty), Parent = currentYear };
            currentYear.Children.Add(month);
            currentMonth = month;

            curMonth = post.DateCreated.Month;
        }

        //create "blog entry level" item
        var blogEntry = new BlogEntry { Name = post.Title.ToLink("/Post/" + post.PostID + "/" + post.Title.ToSeoUrl() ), Parent = currentMonth };
        currentMonth.Children.Add(blogEntry);
    }
    else
    {
        //create "year-level" item
        var year = new BlogEntry { Name = post.DateCreated.Year.ToString().ToLink(string.Empty) };
        entries.Add(year);
        currentYear = year;

        curMonth = post.DateCreated.Month;
        curYear = post.DateCreated.Year;
    }
}
Evgeny
  • 3,320
  • 7
  • 39
  • 50
  • Have you tried using Resharper? It can automatically convert simple loops to LINQ code. [I'm not affiliated with Resharper in any form :)] – miniBill Dec 14 '12 at 20:30
  • What are you trying to improve? Performance? Maintainability? Code elegance? – ean5533 Dec 14 '12 at 20:33
  • 1
    Maybe you're looking for something like this? http://stackoverflow.com/questions/2230202/how-can-i-hierarchically-group-data-using-linq – Chris Sinclair Dec 14 '12 at 20:41
  • ean5533 - code elegance, which will supposedly lead to maintainability. This is a 'hobby' project, so performance is not important, but learning 'best practices' will be beneficial too. – Evgeny Dec 15 '12 at 11:43
  • Chris, you should have submitted that as an answer - exactly what I needed – Evgeny Dec 15 '12 at 12:54

2 Answers2

4

I've created a test example, to check the correctness of the logic. I think this is what you need.

public class BlogEntyTreeItem
{
   public string Text { set; get; }
   public string URL { set; get; }
   public List<BlogEntyTreeItem> Children { set; get; }

   public List<BlogEntyTreeItem> GetTree()
   {
       NWDataContext db = new NWDataContext();
       var p = db.Posts.ToList();

       var list = p.GroupBy(g => g.DateCreated.Year).Select(g => new BlogEntyTreeItem
       {
           Text = g.Key.ToString(),
           Children = g.GroupBy(g1 => g1.DateCreated.ToString("MMMM")).Select(g1 => new BlogEntyTreeItem
           {
               Text = g1.Key,
               Children = g1.Select(i => new BlogEntyTreeItem { Text = i.Name }).ToList()
           }).ToList()
       }).ToList();

       return list;        
   }
}
Rutix
  • 861
  • 1
  • 10
  • 22
Michael Samteladze
  • 1,310
  • 15
  • 38
1

using the link Playing with Linq grouping: GroupByMany ? suggested in How can I hierarchically group data using LINQ?

I first refactored my code into

Solution 1

var results = from allPosts in db.Posts.OrderBy(p => p.DateCreated)
        group allPosts by allPosts.DateCreated.Year into postsByYear

        select new
        {
            postsByYear.Key,
            SubGroups = from yearLevelPosts in postsByYear
                        group yearLevelPosts by yearLevelPosts.DateCreated.Month into postsByMonth
                        select new
                        {
                            postsByMonth.Key,
                            SubGroups = from monthLevelPosts in postsByMonth
                                           group monthLevelPosts by monthLevelPosts.Title into post
                                           select post
                        }
        };

foreach (var yearPosts in results)
{
    //create "year-level" item
    var year = new BlogEntry { Name = yearPosts.Key.ToString().ToLink(string.Empty) };
    entries.Add(year);
    foreach (var monthPosts in yearPosts.SubGroups)
    {
            //create "month-level" item
        var month = new BlogEntry { Name = new DateTime(2000, (int)monthPosts.Key, 1).ToString("MMMM").ToLink(string.Empty), Parent = year };
        year.Children.Add(month);
        foreach (var postEntry in monthPosts.SubGroups)
        {
            //create "blog entry level" item
            var post = postEntry.First() as Post;
            var blogEntry = new BlogEntry { Name = post.Title.ToLink("/Post/" + post.PostID + "/" + post.Title.ToSeoUrl()), Parent = month };
            month.Children.Add(blogEntry);                       
        }
    }
}

And then into a more generic

Solution 2

var results = db.Posts.OrderBy(p => p.DateCreated).GroupByMany(p => p.DateCreated.Year, p => p.DateCreated.Month);

foreach (var yearPosts in results)
{
    //create "year-level" item
    var year = new BlogEntry { Name = yearPosts.Key.ToString().ToLink(string.Empty) };
    entries.Add(year);

    foreach (var monthPosts in yearPosts.SubGroups)
    {
            //create "month-level" item
        var month = new BlogEntry { Name = new DateTime(2000, (int)monthPosts.Key, 1).ToString("MMMM").ToLink(string.Empty), Parent = year };
        year.Children.Add(month);

        foreach (var postEntry in monthPosts.Items)
        {
            //create "blog entry level" item
            var post = postEntry as Post;
            var blogEntry = new BlogEntry { Name = post.Title.ToLink("/Post/" + post.PostID + "/" + post.Title.ToSeoUrl()), Parent = month };
            month.Children.Add(blogEntry);
        }
    }
}

................................................

public static class MyEnumerableExtensions
{
    public static IEnumerable<GroupResult> GroupByMany<TElement>(
        this IEnumerable<TElement> elements,
        params Func<TElement, object>[] groupSelectors)
    {
        if (groupSelectors.Length > 0)
        {
            var selector = groupSelectors.First();

            //reduce the list recursively until zero
            var nextSelectors = groupSelectors.Skip(1).ToArray();
            return
                elements.GroupBy(selector).Select(
                    g => new GroupResult
                    {
                        Key = g.Key,
                        Items = g,
                        SubGroups = g.GroupByMany(nextSelectors)
                    });
        }
        else
            return null;
    }
}

public class GroupResult
{
    public object Key { get; set; }
    public IEnumerable Items { get; set; }
    public IEnumerable<GroupResult> SubGroups { get; set; }
}
Community
  • 1
  • 1
Evgeny
  • 3,320
  • 7
  • 39
  • 50