Skip to content

Commit aeab49d

Browse files
authored
Merge pull request #415 from kbrock/partial_tree_sort
Make arrange_nodes more memory efficient
2 parents b5f2292 + 719a457 commit aeab49d

File tree

2 files changed

+42
-31
lines changed

2 files changed

+42
-31
lines changed

Diff for: lib/ancestry/class_methods.rb

+21-15
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,10 @@ def arrange options = {}
4646
end
4747
end
4848

49-
# Arrange array of nodes into a nested hash of the form
50-
# {node => children}, where children = {} if the node has no children
49+
# arranges array of nodes to a hierarchical hash
50+
#
51+
# @param nodes [Array[Node]] nodes to be arranged
52+
# @returns Hash{Node => {Node => {}, Node => {}}}
5153
# If a node's parent is not included, the node will be included as if it is a top level node
5254
def arrange_nodes(nodes)
5355
node_ids = Set.new(nodes.map(&:id))
@@ -60,6 +62,18 @@ def arrange_nodes(nodes)
6062
end
6163
end
6264

65+
# convert a hash of the form {node => children} to an array of nodes, child first
66+
#
67+
# @param arranged [Hash{Node => {Node => {}, Node => {}}}] arranged nodes
68+
# @returns [Array[Node]] array of nodes with the parent before the children
69+
def flatten_arranged_nodes(arranged, nodes = [])
70+
arranged.each do |node, children|
71+
nodes << node
72+
flatten_arranged_nodes(children, nodes) unless children.empty?
73+
end
74+
nodes
75+
end
76+
6377
# Arrangement to nested array for serialization
6478
# You can also supply your own serialization logic using blocks
6579
# also allows you to pass the order just as you can pass it to the arrange method
@@ -89,29 +103,21 @@ def tree_view(column, data = nil)
89103
end
90104

91105
# Pseudo-preordered array of nodes. Children will always follow parents,
106+
# This is deterministic unless the parents are missing *and* a sort block is specified
92107
def sort_by_ancestry(nodes, &block)
93108
arranged = nodes if nodes.is_a?(Hash)
94109

95110
unless arranged
96111
presorted_nodes = nodes.sort do |a, b|
97-
a_cestry, b_cestry = a.ancestry || '0', b.ancestry || '0'
98-
99-
if block_given? && a_cestry == b_cestry
100-
yield a, b
101-
else
102-
a_cestry <=> b_cestry
103-
end
112+
rank = (a.ancestry || ' ') <=> (b.ancestry || ' ')
113+
rank = yield(a, b) if rank == 0 && block_given?
114+
rank
104115
end
105116

106117
arranged = arrange_nodes(presorted_nodes)
107118
end
108119

109-
arranged.inject([]) do |sorted_nodes, pair|
110-
node, children = pair
111-
sorted_nodes << node
112-
sorted_nodes += sort_by_ancestry(children, &block) unless children.blank?
113-
sorted_nodes
114-
end
120+
flatten_arranged_nodes(arranged)
115121
end
116122

117123
# Integrity checking

Diff for: test/concerns/sort_by_ancestry_test.rb

+21-16
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def test_sort_by_ancestry_full_tree
3737
AncestryTestDatabase.with_model do |model|
3838
n1, n2, n3, n4, n5, n6 = build_tree(model)
3939

40-
records = model.sort_by_ancestry(model.all.order(:id).reverse)
40+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:id => :desc))
4141
assert_equal [n1, n5, n6, n2, n4, n3].map(&:id), records.map(&:id)
4242
end
4343
end
@@ -51,7 +51,8 @@ def test_sort_by_ancestry_no_parents_siblings
5151
AncestryTestDatabase.with_model do |model|
5252
_, _, n3, n4, _, _ = build_tree(model)
5353

54-
assert_equal [n4, n3].map(&:id), model.sort_by_ancestry([n4, n3]).map(&:id)
54+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:id => :desc).offset(3).take(2))
55+
assert_equal [n4, n3].map(&:id), records.map(&:id)
5556
end
5657
end
5758

@@ -61,15 +62,18 @@ def test_sort_by_ancestry_no_parents_same_level
6162
AncestryTestDatabase.with_model do |model|
6263
_, _, n3, n4, n5, _ = build_tree(model)
6364

64-
assert_equal [n5, n4, n3].map(&:id), model.sort_by_ancestry([n5, n4, n3]).map(&:id)
65+
records = [n5, n4, n3]
66+
# records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:id => :desc).offset(3).take(3))
67+
assert_equal [n5, n4, n3].map(&:id), records.map(&:id)
6568
end
6669
end
6770

6871
def test_sort_by_ancestry_partial_tree
6972
AncestryTestDatabase.with_model do |model|
7073
n1, n2, _, _, n5, _ = build_tree(model)
7174

72-
assert_equal [n1, n5, n2].map(&:id), model.sort_by_ancestry([n5, n2, n1]).map(&:id)
75+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:id => :desc).offset(0).take(3))
76+
assert_equal [n1, n5, n2].map(&:id), records.map(&:id)
7377
end
7478
end
7579

@@ -142,7 +146,7 @@ def test_sort_by_ancestry_with_block_full_tree_sql_sort
142146
AncestryTestDatabase.with_model :extra_columns => {:rank => :integer} do |model|
143147
n1, n2, n3, n4, n5, n6 = build_ranked_tree(model)
144148

145-
records = model.sort_by_ancestry(model.all.order(:rank))
149+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:rank))
146150
assert_equal [n1, n5, n3, n2, n4, n6].map(&:id), records.map(&:id)
147151
end
148152
end
@@ -151,7 +155,8 @@ def test_sort_by_ancestry_with_block_all_parents_some_children
151155
AncestryTestDatabase.with_model :extra_columns => {:rank => :integer} do |model|
152156
n1, n2, _, _, n5, _ = build_ranked_tree(model)
153157

154-
assert_equal [n1, n5, n2].map(&:id), model.sort_by_ancestry([n1, n2, n5], &RANK_SORT).map(&:id)
158+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:rank).take(3), &RANK_SORT)
159+
assert_equal [n1, n5, n2].map(&:id), records.map(&:id)
155160
end
156161
end
157162

@@ -176,7 +181,7 @@ def test_sort_by_ancestry_with_block_no_parents_all_children
176181
AncestryTestDatabase.with_model :extra_columns => {:rank => :integer} do |model|
177182
_, _, n3, n4, n5, n6 = build_ranked_tree(model)
178183

179-
records = model.sort_by_ancestry([n3, n4, n5, n6], &RANK_SORT)
184+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:rank).offset(2), &RANK_SORT)
180185
if CORRECT || records[0] == n5
181186
assert_equal [n5, n3, n4, n6].map(&:id), records.map(&:id)
182187
else
@@ -191,20 +196,20 @@ def test_sort_by_ancestry_with_block_no_parents_all_children
191196
# - n2 (1)
192197
# - n4 (0)
193198
# - x
194-
# Issue: n2 will always go before n4.
199+
# Issue: n2 will always go before n4, n5.
195200
# But n1 is not available to put n3 before the n2 tree.
196201
# not sure why it doesn't follow the input order
197202
#
198203
# NOTE: even for partial trees, if the input records are ranked, the output works
199204
def test_sort_by_ancestry_with_sql_sort_paginated_missing_parents_and_children
200205
AncestryTestDatabase.with_model :extra_columns => {:rank => :integer} do |model|
201-
_, n2, n3, n4, _, _ = build_ranked_tree(model)
206+
_, n2, n3, n4, n5, _ = build_ranked_tree(model)
202207

203-
records = model.sort_by_ancestry([n2, n4, n3])
208+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:rank).offset(1).take(4))
204209
if CORRECT
205-
assert_equal [n3, n2, n4].map(&:id), records.map(&:id)
210+
assert_equal [n3, n2, n4, n5].map(&:id), records.map(&:id)
206211
else
207-
assert_equal [n2, n4, n3].map(&:id), records.map(&:id)
212+
assert_equal [n2, n4, n5, n3].map(&:id), records.map(&:id)
208213
end
209214
end
210215
end
@@ -218,13 +223,13 @@ def test_sort_by_ancestry_with_sql_sort_paginated_missing_parents_and_children
218223
# - n5
219224
def test_sort_by_ancestry_with_block_paginated_missing_parents_and_children
220225
AncestryTestDatabase.with_model :extra_columns => {:rank => :integer} do |model|
221-
_, n2, n3, n4, _, _ = build_ranked_tree(model)
226+
_, n2, n3, n4, n5, _ = build_ranked_tree(model)
222227

223-
records = model.sort_by_ancestry([n2, n4, n3], &RANK_SORT)
228+
records = model.sort_by_ancestry(model.all.ordered_by_ancestry_and(:rank).offset(1).take(4), &RANK_SORT)
224229
if CORRECT
225-
assert_equal [n3, n2, n4].map(&:id), records.map(&:id)
230+
assert_equal [n3, n2, n4, n5].map(&:id), records.map(&:id)
226231
else
227-
assert_equal [n2, n4, n3].map(&:id), records.map(&:id)
232+
assert_equal [n2, n4, n5, n3].map(&:id), records.map(&:id)
228233
end
229234
end
230235
end

0 commit comments

Comments
 (0)