Skip to content

Commit 0328522

Browse files
committed
Add .lowest_common_ancestor
1 parent 0095093 commit 0328522

File tree

3 files changed

+105
-1
lines changed

3 files changed

+105
-1
lines changed

README.md

+1-1
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(descendants)``` finds the lowest common ancestor of the descendants.
345345
### Instance methods
346346
347347
* ```tag.root``` returns the root for this node

lib/closure_tree/finders.rb

+13
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(*descendants)
103+
descendants = descendants.first if descendants.length == 1 && descendants.first.respond_to?(:each)
104+
ancestor_id = hierarchy_class
105+
.where(descendant_id: descendants)
106+
.group(:ancestor_id)
107+
.having("COUNT(ancestor_id) = #{descendants.count}")
108+
.order(Arel.sql('MIN(generations) ASC'))
109+
.limit(1)
110+
.pluck(:ancestor_id).first
111+
112+
find_by(primary_key => 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

+91
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,97 @@ 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.lowest_common_ancestor(t112, t111)).to eq t11
462+
expect(tag_class.lowest_common_ancestor(t12, t11)).to eq t1
463+
464+
expect(tag_class.lowest_common_ancestor([t112, t111])).to eq t11
465+
expect(tag_class.lowest_common_ancestor([t12, t11])).to eq t1
466+
467+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t112', 't111']))).to eq t11
468+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't11']))).to eq t1
469+
end
470+
471+
it 'finds the grandparent for cousins' do
472+
expect(tag_class.lowest_common_ancestor(t112, t111, t121)).to eq t1
473+
expect(tag_class.lowest_common_ancestor([t112, t111, t121])).to eq t1
474+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t112', 't111', 't121']))).to eq t1
475+
end
476+
477+
it 'finds the parent/grandparent for aunt-uncle/niece-nephew' do
478+
expect(tag_class.lowest_common_ancestor(t12, t112)).to eq t1
479+
expect(tag_class.lowest_common_ancestor([t12, t112])).to eq t1
480+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't112']))).to eq t1
481+
end
482+
483+
it 'finds the self/parent for parent/child' do
484+
expect(tag_class.lowest_common_ancestor(t12, t121)).to eq t12
485+
expect(tag_class.lowest_common_ancestor(t1, t12)).to eq t1
486+
487+
expect(tag_class.lowest_common_ancestor([t12, t121])).to eq t12
488+
expect(tag_class.lowest_common_ancestor([t1, t12])).to eq t1
489+
490+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t12', 't121']))).to eq t12
491+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t1', 't12']))).to eq t1
492+
end
493+
494+
it 'finds the self/grandparent for grandparent/grandchild' do
495+
expect(tag_class.lowest_common_ancestor(t211, t2)).to eq t2
496+
expect(tag_class.lowest_common_ancestor(t111, t1)).to eq t1
497+
498+
expect(tag_class.lowest_common_ancestor([t211, t2])).to eq t2
499+
expect(tag_class.lowest_common_ancestor([t111, t1])).to eq t1
500+
501+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t211', 't2']))).to eq t2
502+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t111', 't1']))).to eq t1
503+
end
504+
505+
it 'finds the grandparent for a whole extended family' do
506+
expect(tag_class.lowest_common_ancestor(t1, t11, t111, t112, t12, t121)).to eq t1
507+
expect(tag_class.lowest_common_ancestor(t2, t21, t211)).to eq t2
508+
509+
expect(tag_class.lowest_common_ancestor([t1, t11, t111, t112, t12, t121])).to eq t1
510+
expect(tag_class.lowest_common_ancestor([t2, t21, t211])).to eq t2
511+
512+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t1', 't11', 't111', 't112', 't12', 't121']))).to eq t1
513+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t2', 't21', 't211']))).to eq t2
514+
end
515+
516+
it 'is nil for no items' do
517+
expect(tag_class.lowest_common_ancestor).to be_nil
518+
expect(tag_class.lowest_common_ancestor([])).to be_nil
519+
expect(tag_class.lowest_common_ancestor(tag_class.none)).to be_nil
520+
end
521+
522+
it 'is nil if there are no common ancestors' do
523+
expect(tag_class.lowest_common_ancestor(t111, t211)).to be_nil
524+
expect(tag_class.lowest_common_ancestor([t111, t211])).to be_nil
525+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: ['t111', 't211']))).to be_nil
526+
end
527+
528+
it 'is itself for single item' do
529+
expect(tag_class.lowest_common_ancestor(t111)).to eq t111
530+
expect(tag_class.lowest_common_ancestor(t2)).to eq t2
531+
532+
expect(tag_class.lowest_common_ancestor([t111])).to eq t111
533+
expect(tag_class.lowest_common_ancestor([t2])).to eq t2
534+
535+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: 't111'))).to eq t111
536+
expect(tag_class.lowest_common_ancestor(tag_class.where(name: 't2'))).to eq t2
537+
end
538+
end
539+
449540
context 'paths' do
450541
context 'with grandchild' do
451542
before do

0 commit comments

Comments
 (0)