Reputation: 453
So, I have next array of hashes:
my @arr = (
#subways, "0" - superroot
{id => "1", parent_id => "0", name => "subway 1"},
#lines
{id => "12642", parent_id => "1", name => "no category"},
{id => "12645", parent_id => "1", name => "line 1"},
#cars
{id => "12646", parent_id => "1", name => "carriage 1"},
{id => "12646", parent_id => "12645", name => "carriage 1"},
{id => "12647", parent_id => "1", name => "carriage 2"},
{id => "12647", parent_id => "12645", name => "carriage 2"},
{id => "12679", parent_id => "1", name => "separate cars"},
{id => "12679", parent_id => "12642", name => "separate cars"},
{id => "12643", parent_id => "1", name => "ungrouped"},
{id => "12643", parent_id => "12642", name => "ungrouped"}
);
and I heed to make a tree of them like:
subway->line->carriage
By the way, here's a problem. As you see - here's a "1" in doubling carriages, but I need line as parent_id. Is there a way to do this?
Upvotes: 0
Views: 423
Reputation: 386371
use strict;
use warnings qw( all );
use feature qw( current_sub say );
my @rows = (
#subways, "0" - superroot
{id => "1", parent_id => "0", name => "subway 1"},
#lines
{id => "12642", parent_id => "1", name => "no category"},
{id => "12645", parent_id => "1", name => "line 1"},
#cars
{id => "12646", parent_id => "1", name => "carriage 1"},
{id => "12646", parent_id => "12645", name => "carriage 1"},
{id => "12647", parent_id => "1", name => "carriage 2"},
{id => "12647", parent_id => "12645", name => "carriage 2"},
{id => "12679", parent_id => "1", name => "separate cars"},
{id => "12679", parent_id => "12642", name => "separate cars"},
{id => "12643", parent_id => "1", name => "ungrouped"},
{id => "12643", parent_id => "12642", name => "ungrouped"}
);
my $tree = { name => "[root]", children => [] };
{
my %tree = ( 0 => $tree );
for my $row (@rows) {
my $node = $tree{ $row->{id} } //= { name => undef, children => [] };
$node->{name} = $row->{name};
my $parent_node = $tree{ $row->{parent_id} } //= { name => undef, children => [] };
push @{ $parent_node->{children} }, $node;
}
}
# Add depth to nodes.
# use a breadth-first search so that the depth of nodes
# at multiple depths are set to the node's deepest depth.
{
my @todo = ( [ $tree, 0 ] );
while (@todo) {
my ($node, $depth) = @{ shift(@todo) };
$node->{depth} = $depth;
++$depth;
push @todo, map { [ $_, $depth ] } @{ $node->{children} };
}
}
# Trim shortcuts to deeper nodes.
{
my @todo = $tree;
while (@todo) {
my $node = shift(@todo);
my $depth = delete($node->{depth}) + 1;
@{ $node->{children} } = grep { $_->{depth} == $depth } @{ $node->{children} };
push @todo, @{ $node->{children} };
}
}
# Display tree
my $visitor = sub {
my ($node, $depth) = @_;
say " " x $depth, $node->{name};
__SUB__->($_, $depth+1) for @{ $node->{children} };
};
$visitor->($_, 0) for @$tree;
Output:
subway 1
no category
separate cars
ungrouped
line 1
carriage 1
carriage 2
Upvotes: 3
Reputation: 126742
My apologies. I missed your final paragraph that explained how an item may have a spurious parent "1" in addition to the real value. I've added some code to sanitise the original data and create a map %parent
of every node to its true parent before building the graph
use strict;
use warnings 'all';
use Graph::Directed;
my @arr = (
#subways, "0" - superroot
{ id => "1", parent_id => "0", name => "subway 1" },
#lines
{ id => "12642", parent_id => "1", name => "no category" },
{ id => "12645", parent_id => "1", name => "line 1" },
#cars
{ id => "12646", parent_id => "1", name => "carriage 1" },
{ id => "12646", parent_id => "12645", name => "carriage 1" },
{ id => "12647", parent_id => "1", name => "carriage 2" },
{ id => "12647", parent_id => "12645", name => "carriage 2" },
{ id => "12679", parent_id => "1", name => "separate cars" },
{ id => "12679", parent_id => "12642", name => "separate cars" },
{ id => "12643", parent_id => "1", name => "ungrouped" },
{ id => "12643", parent_id => "12642", name => "ungrouped" }
);
# Sanitise data to remove "1" parents
#
my %parent;
for my $node ( @arr ) {
my ($id, $parent_id) = @{$node}{qw/ id parent_id /};
next unless $parent_id;
$parent{$id} = $parent_id unless $parent{$id} and $parent{$id} ne 1;
}
# Build the graph
#
my $tree = Graph::Directed->new;
for my $node ( keys %parent ) {
$tree->add_edge( $parent{$node} => $node );
}
# Display the data
#
my %names = map { @{$_}{qw/ id name /} } @arr;
print_tree($tree, $_) for $tree->predecessorless_vertices;
sub print_tree {
my ($tree, $root, $indent) = @_;
$indent //= 0;
printf "%s%s\n", ' ' x $indent, $names{$root};
print_tree($tree, $_, $indent + 1) for $tree->successors($root);
}
subway 1
line 1
carriage 1
carriage 2
no category
separate cars
ungrouped
I suggest that you make use of the Graph
module. A tree is a directed graph, and all you need to do is create a graph, add the connections ("edges") and interrogate the result
This program does exactly that. I have written a print_tree
subroutine which displays the tree in indented lines from a given starting point. A call to predecessorless_vertices
finds all the roots of the tree: nodes which no other node connects to. In this case there is just one root, as there should be
use strict;
use warnings 'all';
use Graph::Directed;
my @arr = (
#subways, "0" - superroot
{ id => "1", parent_id => "0", name => "subway 1" },
#lines
{ id => "12642", parent_id => "1", name => "no category" },
{ id => "12645", parent_id => "1", name => "line 1" },
#cars
{ id => "12646", parent_id => "1", name => "carriage 1" },
{ id => "12646", parent_id => "12645", name => "carriage 1" },
{ id => "12647", parent_id => "1", name => "carriage 2" },
{ id => "12647", parent_id => "12645", name => "carriage 2" },
{ id => "12679", parent_id => "1", name => "separate cars" },
{ id => "12679", parent_id => "12642", name => "separate cars" },
{ id => "12643", parent_id => "1", name => "ungrouped" },
{ id => "12643", parent_id => "12642", name => "ungrouped" }
);
# Build the graph
#
my $tree = Graph::Directed->new;
for my $node ( @arr ) {
$tree->add_edge( @{$node}{qw/ parent_id id /} ) if $node->{parent_id};
}
# Display the structure
#
my %names = map { @{$_}{qw/ id name /} } @arr;
print_tree($tree, $_) for $tree->predecessorless_vertices;
sub print_tree {
my ($tree, $root, $indent) = @_;
$indent //= 0;
printf "%s%s\n", ' ' x $indent, $names{$root};
print_tree($tree, $_, $indent + 1) for $tree->successors($root);
}
subway 1
no category
ungrouped
separate cars
carriage 2
carriage 1
separate cars
ungrouped
line 1
carriage 2
carriage 1
Upvotes: 1