Skip to content

Commit e6cebfd

Browse files
committed
Add .lowest_common_ancestor
1 parent 1bc10c8 commit e6cebfd

File tree

3 files changed

+67
-1
lines changed

3 files changed

+67
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ When you include ```has_closure_tree``` in your model, you can provide a hash to
341341
* ```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).
342342
* ```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```.
343343
* ```Tag.with_ancestor(ancestors)``` scopes to all descendants whose ancestor is in the given list.
344-
344+
* ```Tag.lowest_common_ancestor``` finds the lowest common ancestor of the relation.
345345
### Instance methods
346346
347347
* ```tag.root``` returns the root for this node

lib/closure_tree/finders.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,19 @@ def with_descendant(*descendants)
9999
_ct.scope_with_order(scope)
100100
end
101101

102+
def lowest_common_ancestor
103+
ancestor_id = hierarchy_class
104+
.select(:ancestor_id)
105+
.where(descendant_id: all)
106+
.group(:ancestor_id)
107+
.having("COUNT(ancestor_id) = #{count}")
108+
.order(Arel.sql('MIN(generations) ASC'))
109+
.limit(1)
110+
.pluck(:ancestor_id).first
111+
112+
unscoped.find(ancestor_id) if ancestor_id
113+
end
114+
102115
def find_all_by_generation(generation_level)
103116
s = joins(<<-SQL.strip_heredoc)
104117
INNER JOIN (

spec/tag_examples.rb

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,59 @@ def assert_parent_and_children
446446
end
447447
end
448448

449+
context 'lowest_common_ancestor' do
450+
let!(:t1) { tag_class.create!(name: 't1') }
451+
let!(:t11) { tag_class.create!(name: 't11', parent: t1) }
452+
let!(:t111) { tag_class.create!(name: 't111', parent: t11) }
453+
let!(:t112) { tag_class.create!(name: 't112', parent: t11) }
454+
let!(:t12) { tag_class.create!(name: 't12', parent: t1) }
455+
let!(:t121) { tag_class.create!(name: 't121', parent: t12) }
456+
let!(:t2) { tag_class.create!(name: 't2') }
457+
let!(:t21) { tag_class.create!(name: 't21', parent: t2) }
458+
let!(:t211) { tag_class.create!(name: 't211', parent: t21) }
459+
460+
it 'finds the parent for siblings' do
461+
expect(tag_class.where(name: %w(t112 t111)).lowest_common_ancestor).to eq t11
462+
expect(tag_class.where(name: %w(t12 t11)).lowest_common_ancestor).to eq t1
463+
end
464+
465+
it 'finds the grandparent for cousins' do
466+
expect(tag_class.where(name: %w(t112 N111 t121)).lowest_common_ancestor).to eq t1
467+
end
468+
469+
it 'finds the parent/grandparent for aunt-uncle/niece-nephew' do
470+
expect(tag_class.where(name: %w(t12 t112)).lowest_common_ancestor).to eq t1
471+
end
472+
473+
it 'finds the self/parent for parent/child' do
474+
expect(tag_class.where(name: %w(t12 t121)).lowest_common_ancestor).to eq t12
475+
expect(tag_class.where(name: %w(t1 t12)).lowest_common_ancestor).to eq t1
476+
end
477+
478+
it 'finds the self/grandparent for grandparent/grandchild' do
479+
expect(tag_class.where(name: %w(t211 t2)).lowest_common_ancestor).to eq t2
480+
expect(tag_class.where(name: %w(t111 t1)).lowest_common_ancestor).to eq t1
481+
end
482+
483+
it 'finds the grandparent for a whole extended family' do
484+
expect(tag_class.where(name: %w(t1 t11 t111 t112 t12 t121)).lowest_common_ancestor).to eq t1
485+
expect(tag_class.where(name: %w(t2 t21 t211)).lowest_common_ancestor).to eq t2
486+
end
487+
488+
it 'is nil for no items' do
489+
expect(tag_class.none.lowest_common_ancestor).to be_nil
490+
end
491+
492+
it 'is nil if there are no common ancestors' do
493+
expect(tag_class.where(name: %w(t111 t211)).lowest_common_ancestor).to be_nil
494+
end
495+
496+
it 'is itself for single item' do
497+
expect(tag_class.where(name: 't111').lowest_common_ancestor).to eq t111
498+
expect(tag_class.where(name: 't2').lowest_common_ancestor).to eq t2
499+
end
500+
end
501+
449502
context 'paths' do
450503
context 'with grandchild' do
451504
before do

0 commit comments

Comments
 (0)