Skip to content

Commit 69f3a68

Browse files
author
cschen
committed
adds print elision logic also for similar consecutive nodes.
1 parent 5ed4c69 commit 69f3a68

File tree

2 files changed

+103
-13
lines changed

2 files changed

+103
-13
lines changed

src/printing.jl

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ Print a text representation of `tree` to the given `io` object.
1515
* `io::IO` - IO stream to write to.
1616
* `tree` - tree to print.
1717
* `maxdepth::Integer = 5` - truncate printing of subtrees at this depth.
18+
* `maxsibling::Union{Nothing,Integer} = nothing` - if set, print at most this many
19+
consecutive similar siblings and then elide the rest of that run.
20+
* `sibling_similarity_threshold::Real = 0` - threshold for considering sibling node values
21+
similar. Non-numeric node values currently only support exact matching.
1822
* `indicate_truncation::Bool = true` - print a vertical ellipsis character beneath
1923
truncated nodes.
2024
* `charset::TreeCharSet` - [`TreeCharSet`](@ref) to use to print branches.
@@ -191,15 +195,46 @@ print_child_key(io::IO, key::CartesianIndex) = show(io, Tuple(key))
191195

192196
branchwidth(cs::TreeCharSet) = sum(textwidth.((cs.mid, cs.dash)))
193197

198+
199+
"""
200+
nodevalue_distance(v1, v2; kw...)::Real
201+
202+
Compute the distance between the values of two child nodes. By default, equivalent to isequal.
203+
It must return a Real
204+
205+
**OPTIONAL**: This can be extended for custom types and controls how nodes are shown
206+
in [`print_tree`](@ref).
207+
"""
208+
nodevalue_distance(v1, v2) = ifelse(isequal(v1, v2), zero(Float64), Inf)
209+
210+
function print_and_child_prefix(io::IO, prefix::AbstractString, is_last::Bool, charset::TreeCharSet)
211+
print(io, prefix)
212+
213+
if is_last
214+
print(io, charset.terminator)
215+
child_prefix = prefix * " " ^ (textwidth(charset.skip) + textwidth(charset.dash) + 1)
216+
else
217+
print(io, charset.mid)
218+
child_prefix = prefix * charset.skip * " " ^ (textwidth(charset.dash) + 1)
219+
end
220+
221+
print(io, charset.dash, ' ')
222+
223+
return child_prefix
224+
end
225+
194226
function print_tree(printnode::Function, print_child_key::Function, io::IO, node;
195227
maxdepth::Integer=5,
228+
maxsibling::Union{Nothing,Integer}=nothing,
229+
sibling_similarity_threshold::Real=0,
196230
indicate_truncation::Bool=true,
197231
charset::TreeCharSet=TreeCharSet(),
198232
printkeys::Union{Bool,Nothing}=nothing,
199233
depth::Integer=0,
200234
prefix::AbstractString="",
201235
printnode_kw=(;),
202236
)
237+
203238
# Get node representation as string
204239
buf = IOBuffer()
205240
printnode(IOContext(buf, io), node; printnode_kw...)
@@ -233,28 +268,43 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node
233268
# Print children
234269
s = Iterators.Stateful(this_printkeys ? pairs(c) : c)
235270

236-
while !isempty(s)
237-
child_prefix = prefix
271+
use_sibling_elision = !isnothing(maxsibling)
272+
prev_child_value = nothing
273+
seen_child = false
274+
similar_run_length = 0
275+
elided_count = 0
238276

277+
while !isempty(s)
239278
if this_printkeys
240279
child_key, child = popfirst!(s)
241280
else
242281
child = popfirst!(s)
243282
child_key = nothing
244283
end
245284

246-
print(io, prefix)
285+
child_value = nodevalue(child)
286+
if use_sibling_elision
287+
if seen_child && nodevalue_distance(prev_child_value, child_value) <= sibling_similarity_threshold
288+
similar_run_length += 1
289+
else
290+
if elided_count > 0
291+
print_and_child_prefix(io, prefix, false, charset)
292+
println(io, charset.trunc, " (", elided_count, " siblings elided)")
293+
elided_count = 0
294+
end
295+
similar_run_length = 1
296+
end
297+
end
247298

248-
# Last child?
249-
if isempty(s)
250-
print(io, charset.terminator)
251-
child_prefix *= " " ^ (textwidth(charset.skip) + textwidth(charset.dash) + 1)
252-
else
253-
print(io, charset.mid)
254-
child_prefix *= charset.skip * " " ^ (textwidth(charset.dash) + 1)
299+
prev_child_value = child_value
300+
seen_child = true
301+
302+
if use_sibling_elision && similar_run_length > maxsibling
303+
elided_count += 1
304+
continue
255305
end
256306

257-
print(io, charset.dash, ' ')
307+
child_prefix = print_and_child_prefix(io, prefix, isempty(s) && elided_count == 0, charset)
258308

259309
# Print key
260310
if this_printkeys
@@ -267,10 +317,17 @@ function print_tree(printnode::Function, print_child_key::Function, io::IO, node
267317
end
268318

269319
print_tree(printnode, print_child_key, io, child;
270-
maxdepth=maxdepth, indicate_truncation=indicate_truncation, charset=charset,
271-
printkeys=printkeys, depth=depth+1, prefix=child_prefix, printnode_kw=printnode_kw,
320+
maxdepth=maxdepth, maxsibling=maxsibling,
321+
sibling_similarity_threshold=sibling_similarity_threshold,
322+
indicate_truncation=indicate_truncation, charset=charset, printkeys=printkeys,
323+
depth=depth+1, prefix=child_prefix, printnode_kw=printnode_kw,
272324
)
273325
end
326+
327+
if elided_count > 0
328+
print_and_child_prefix(io, prefix, true, charset)
329+
println(io, charset.trunc, " (", elided_count, " siblings elided)")
330+
end
274331
end
275332

276333
print_tree(printnode::Function, io::IO, node; kw...) = print_tree(printnode, print_child_key, io, node; kw...)

test/printing.jl

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,39 @@ AbstractTrees.printnode(io::IO, u::UnindexableChildren) = AbstractTrees.printnod
9494
@test numlines == 4 # 1 (head node) + 3 (depth)
9595
end
9696

97+
@testset "Sibling elision" begin
98+
truncation_str = TreeCharSet().trunc
99+
100+
# Start eliding only after the first N similar siblings.
101+
ptxt = repr_tree([1, 1, 1, 2, 2, 2, 3], maxsibling=2)
102+
@test endswith(ptxt, """
103+
├─ 1
104+
├─ 1
105+
├─ $(truncation_str) (1 siblings elided)
106+
├─ 2
107+
├─ 2
108+
├─ $(truncation_str) (1 siblings elided)
109+
└─ 3
110+
""")
111+
112+
# If an elided run ends the sibling list, the summary line is the terminator.
113+
ptxt = repr_tree((1, 1, 1), maxsibling=2, printkeys=true)
114+
@test endswith(ptxt, """
115+
├─ 1 ⇒ 1
116+
├─ 2 ⇒ 1
117+
└─ $(truncation_str) (1 siblings elided)
118+
""")
119+
120+
tree = UnindexableChildren([1, 1, 1, 2])
121+
@test repr_tree(tree, maxsibling=2) == repr_tree(tree.node, maxsibling=2)
122+
123+
# Elided siblings are not recursed into, so they do not contribute maxdepth truncation lines.
124+
ptxt = repr_tree([[1, 2], [1, 2], [1, 2], [3, 4]], maxdepth=1, maxsibling=2)
125+
lines = [strip(line) for line in split(ptxt, '\n') if !isempty(strip(line))]
126+
@test count(==(truncation_str), lines) == 3
127+
@test any(line -> occursin(" (1 siblings elided)", line), lines)
128+
end
129+
97130

98131
@testset "Child keys" begin
99132
@testset "AbstractVector" begin

0 commit comments

Comments
 (0)