Permalink
Browse files

Merge pull request #328 from robotdana/lowest_common_ancestor_non_chain

Add .lowest_common_ancestor
  • Loading branch information...
mceachen committed Oct 2, 2018
2 parents b771362 + 0328522 commit 4587d8c60f8963c6d879c8b5b399c0e22fe7fd5b
Showing with 105 additions and 1 deletion.
  1. +1 −1 README.md
  2. +13 −0 lib/closure_tree/finders.rb
  3. +91 −0 spec/tag_examples.rb
@@ -339,7 +339,7 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
* ```Tag.find_or_create_by_path(path, attributes)``` returns the node whose name path is ```path```, and will create the node if it doesn't exist already.See (#find_or_create_by_path).
* ```Tag.find_all_by_generation(generation_level)``` returns the descendant nodes who are ```generation_level``` away from a root. ```Tag.find_all_by_generation(0)``` is equivalent to ```Tag.roots```.
* ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestor is in the given list.
* ```Tag.lowest_common_ancestor(descendants)``` finds the lowest common ancestor of the descendants.
### Instance methods
* ```tag.root``` returns the root for this node
@@ -99,6 +99,19 @@ def with_descendant(*descendants)
_ct.scope_with_order(scope)
end

def lowest_common_ancestor(*descendants)
descendants = descendants.first if descendants.length == 1 && descendants.first.respond_to?(:each)
ancestor_id = hierarchy_class
.where(descendant_id: descendants)
.group(:ancestor_id)
.having("COUNT(ancestor_id) = #{descendants.count}")
.order(Arel.sql('MIN(generations) ASC'))
.limit(1)
.pluck(:ancestor_id).first

find_by(primary_key => ancestor_id) if ancestor_id
end

def find_all_by_generation(generation_level)
s = joins(<<-SQL.strip_heredoc)
INNER JOIN (
@@ -446,6 +446,97 @@ def assert_parent_and_children
end
end

context 'lowest_common_ancestor' do
let!(:t1) { tag_class.create!(name: 't1') }
let!(:t11) { tag_class.create!(name: 't11', parent: t1) }
let!(:t111) { tag_class.create!(name: 't111', parent: t11) }
let!(:t112) { tag_class.create!(name: 't112', parent: t11) }
let!(:t12) { tag_class.create!(name: 't12', parent: t1) }
let!(:t121) { tag_class.create!(name: 't121', parent: t12) }
let!(:t2) { tag_class.create!(name: 't2') }
let!(:t21) { tag_class.create!(name: 't21', parent: t2) }
let!(:t211) { tag_class.create!(name: 't211', parent: t21) }

it 'finds the parent for siblings' do
expect(tag_class.lowest_common_ancestor(t112, t111)).to eq t11
expect(tag_class.lowest_common_ancestor(t12, t11)).to eq t1

expect(tag_class.lowest_common_ancestor([t112, t111])).to eq t11
expect(tag_class.lowest_common_ancestor([t12, t11])).to eq t1

expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t112', 't111']))).to eq t11
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't11']))).to eq t1
end

it 'finds the grandparent for cousins' do
expect(tag_class.lowest_common_ancestor(t112, t111, t121)).to eq t1
expect(tag_class.lowest_common_ancestor([t112, t111, t121])).to eq t1
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t112', 't111', 't121']))).to eq t1
end

it 'finds the parent/grandparent for aunt-uncle/niece-nephew' do
expect(tag_class.lowest_common_ancestor(t12, t112)).to eq t1
expect(tag_class.lowest_common_ancestor([t12, t112])).to eq t1
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't112']))).to eq t1
end

it 'finds the self/parent for parent/child' do
expect(tag_class.lowest_common_ancestor(t12, t121)).to eq t12
expect(tag_class.lowest_common_ancestor(t1, t12)).to eq t1

expect(tag_class.lowest_common_ancestor([t12, t121])).to eq t12
expect(tag_class.lowest_common_ancestor([t1, t12])).to eq t1

expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't121']))).to eq t12
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t1', 't12']))).to eq t1
end

it 'finds the self/grandparent for grandparent/grandchild' do
expect(tag_class.lowest_common_ancestor(t211, t2)).to eq t2
expect(tag_class.lowest_common_ancestor(t111, t1)).to eq t1

expect(tag_class.lowest_common_ancestor([t211, t2])).to eq t2
expect(tag_class.lowest_common_ancestor([t111, t1])).to eq t1

expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t211', 't2']))).to eq t2
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t111', 't1']))).to eq t1
end

it 'finds the grandparent for a whole extended family' do
expect(tag_class.lowest_common_ancestor(t1, t11, t111, t112, t12, t121)).to eq t1
expect(tag_class.lowest_common_ancestor(t2, t21, t211)).to eq t2

expect(tag_class.lowest_common_ancestor([t1, t11, t111, t112, t12, t121])).to eq t1
expect(tag_class.lowest_common_ancestor([t2, t21, t211])).to eq t2

expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t1', 't11', 't111', 't112', 't12', 't121']))).to eq t1
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t2', 't21', 't211']))).to eq t2
end

it 'is nil for no items' do
expect(tag_class.lowest_common_ancestor).to be_nil
expect(tag_class.lowest_common_ancestor([])).to be_nil
expect(tag_class.lowest_common_ancestor(tag_class.none)).to be_nil
end

it 'is nil if there are no common ancestors' do
expect(tag_class.lowest_common_ancestor(t111, t211)).to be_nil
expect(tag_class.lowest_common_ancestor([t111, t211])).to be_nil
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t111', 't211']))).to be_nil
end

it 'is itself for single item' do
expect(tag_class.lowest_common_ancestor(t111)).to eq t111
expect(tag_class.lowest_common_ancestor(t2)).to eq t2

expect(tag_class.lowest_common_ancestor([t111])).to eq t111
expect(tag_class.lowest_common_ancestor([t2])).to eq t2

expect(tag_class.lowest_common_ancestor(tag_class.where(name: 't111'))).to eq t111
expect(tag_class.lowest_common_ancestor(tag_class.where(name: 't2'))).to eq t2
end
end

context 'paths' do
context 'with grandchild' do
before do

0 comments on commit 4587d8c

Please sign in to comment.