Skip to content

Add .lowest_common_ancestor #328

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,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
Expand Down
13 changes: 13 additions & 0 deletions lib/closure_tree/finders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
91 changes: 91 additions & 0 deletions spec/tag_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down