понедельник, сентября 14, 2009

Building tree out of nested set


This keeps popping up over and over again. About 2 years ago I mentioned solution on how to build tree out of nested set, but the solution was lost in google groups. I'm going to publish it here.

So, nested set: every item has LEFT and RIGHT numbers. LEFT number is less than any LEFT or RIGHT number of any descendant item. RIGHT number is greater than any LEFT or RIGHT number of any descendant item. To get a subtree, select all items with LEFT greater than given LEFT and RIGHT less than given RIGHT number. You get the flat array of items of that tree structure. Next thing is to convert it to tree which means for any given item you need the ability to iterate over all of it's children.

First thing you need to do is to order items by LEFT number, than by RIGHT number.

A small sidenote: we will assume that LEFT and RIGHT numbers are compact so that if your root item has LEFT=1 and RIGHT=10 then for each number X in interval [1, 10] there is item for which X is the value of LEFT or RIGHT. This allows easy calculation of how many descendants particular item has.

So, say we have an flat array and we need to know how many items in array you need to skip to get to given item's next sibling. And that number is (RIGHT-LEFT) (minus one if you do not count the item itself).
# Class that allows performance wise traversing a list of items
# that form a part of nested set structure.
class NestedSetTreePresenter
  include Enumerable

  # Constructs presenter with a items that form part of nested set,
  # offset of first item on desired level and count - number of items
  # on current level.
  # Items should be ordered by their left bound values.
  def initialize(items, offset = 0, count = nil)
    @items = items
    @offset = offset
    @count = count || @items.size
  end

  # For each item of the same level in nested set as the first item
  # calls block passing corresponding item.
  def each
    i = @offset
    bound = @offset + @count
    bound = @items.size if bound > @items.size

    while i<bound
      item = @items[i]
      descendants_count = (item.rgt-item.lft)/2
      yield item
      i += 1 + descendants_count
    end
  end

  # For each item of the same level in nested set as the first item
  # calls block passing corresponding item and collection, that
  # represent all immediate children of that item. Collection is also
  # an instance of this class.
  def each_with_children
    i = @offset
    bound = @offset + @count
    bound = @items.size if bound > @items.size

    while i<bound
      item = @items[i]
      descendants_count = (item.rgt-item.lft)/2
      yield item, NestedSetTreePresenter.new(@items, i+1, descendants_count)
      i += 1 + descendants_count
    end
  end
end
I chose not to yield a proxy, container or anything else to minimize the number of objects created.

Here is how to use it:
def print_tree(items, level = 0)
  items.each_with_children do |item, children|
    puts "  "*level + item.name
    print_tree(children, level+1)
  end
end

print_tree(NestedSetTreePresenter.new(items))