Skip to content

Lowest common ancestor #326

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

Closed
Closed
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``` finds the lowest common ancestor of the relation.
### 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
ancestor_id = hierarchy_class
.select(:ancestor_id)
.where(descendant_id: all)
.group(:ancestor_id)
.having("COUNT(ancestor_id) = #{count}")
.order(Arel.sql('MIN(generations) ASC'))
.limit(1)
.pluck(:ancestor_id).first

default_scoped(unscoped).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
53 changes: 53 additions & 0 deletions spec/tag_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,59 @@ 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.where(name: %w(t112 t111)).lowest_common_ancestor).to eq t11
expect(tag_class.where(name: %w(t12 t11)).lowest_common_ancestor).to eq t1
end

it 'finds the grandparent for cousins' do
expect(tag_class.where(name: %w(t112 N111 t121)).lowest_common_ancestor).to eq t1
end

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

it 'finds the self/parent for parent/child' do
expect(tag_class.where(name: %w(t12 t121)).lowest_common_ancestor).to eq t12
expect(tag_class.where(name: %w(t1 t12)).lowest_common_ancestor).to eq t1
end

it 'finds the self/grandparent for grandparent/grandchild' do
expect(tag_class.where(name: %w(t211 t2)).lowest_common_ancestor).to eq t2
expect(tag_class.where(name: %w(t111 t1)).lowest_common_ancestor).to eq t1
end

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

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

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

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

context 'paths' do
context 'with grandchild' do
before do
Expand Down