Anonymous Monk has asked for the wisdom of the Perl Monks concerning the following question:

Hello. I am at my wit's end on this recursive problem.

I would like to take a list of strings, delimited like so:

one foo / bar foo / baz foo / qux / two foo / qux / three foo / qux / four five
And turn that into an array of hashes like so:
$VAR = [ { name = 'one', }, { name = 'foo', children => [ { name => 'bar', }, { name => 'baz', }, { name => 'qux', children => [ { name => 'two', }, { name => 'three', }, { name => 'four', }, ], }, ], }, { name = 'five', }, ];
The data can be N levels deep, and while my goal has been to use a recursive solution, I am open to any and all ideas/solutions. (Past nodes, etc.)

I have attempted this several times today and while I seemed to come close, I end up with with a 1 level deep array of hashes (with duplicate names for the parents). :(

Replies are listed 'Best First'.
Re: Convert delimited string into nested data structure
by BrowserUk (Patriarch) on Feb 20, 2007 at 01:54 UTC

    Update: Or more compactly. Use as below.

    sub x { my $aref = shift; my( $name, @kids ) = @_; push @{ $aref }, { name => $name } unless @$aref and $aref->[ -1 ]{ name } eq $name; x( $aref->[ -1 ]{ children } ||= [], @kids ) if @kids; }
    #! perl -slw use strict; use Data::Dumper; sub x { my $aref = shift; my( $name, @kids ) = @_; unless( @$aref and $aref->[ -1 ]{ name } eq $name ) { push @{ $aref }, { name => $name }; } if( @kids ) { if( not exists $aref->[ -1 ]{ children } ) { $aref->[ -1 ]{ children } = []; } x( $aref->[ -1 ]{ children }, @kids ); } } my @data; chomp() and x( \@data, split ' / ', $_ ) while <DATA>; print Dumper \@data; __DATA__ one foo / bar foo / baz foo / qux / two foo / qux / three foo / qux / four five

    Produces

    c:\test>junk9 | more $VAR1 = [ { 'name' => 'one' }, { 'name' => 'foo', 'children' => [ { 'name' => 'bar' }, { 'name' => 'baz' }, { 'name' => 'qux', 'children' => [ { 'name' => 'two' }, { 'name' => 'three' }, { 'name' => 'four' } ] } ] }, { 'name' => 'five' } ];

    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.
Re: Convert delimited string into nested data structure
by jdporter (Paladin) on Feb 19, 2007 at 19:29 UTC

    Perhaps Recursively walk a hash to get to an element would serve as a good starting point.

    Update: I'm sure it could be done, but probably tougher than necessary. So here's a solution from scratch:

    use strict; use warnings; my @d; # the data structure we're building. my @p; # current (actually previous) vector while (<DATA>) { chomp; my $r = \@d; my @vc = my @v = split m# / #; my @pc = @p; # bashable copy while ( @vc and @pc and $vc[0] eq $pc[0] ) { shift @vc; shift @pc; $r = $r->[-1]{'children'}; } while ( @vc ) { push @$r, { name => shift @vc }; $r = ( @vc and $r->[-1]{'children'} = [] ); } @p = @v; } use Data::Dumper; print Dumper \@d; __DATA__ one foo / bar foo / baz foo / qux / two foo / qux / three foo / qux / four five

    Note, this presumes that the input lines are in proper order, as they are in your example data.

    A word spoken in Mind will reach its own level, in the objective world, by its own weight
    A reply falls below the community's threshold of quality. You may see it by logging in.
Re: Convert delimited string into nested data structure
by GrandFather (Saint) on Feb 19, 2007 at 20:25 UTC

    The name keys seem redundant to me. If you want to generate a nested hash structure based on the key strings then the following (non-recursive) code may get you started:

    use strict; use warnings; use Data::Dump::Streamer; my %hash; while (<DATA>) { chomp; my @elements = split /\s*\/\s*/; next if ! @elements; $hash{$elements[0]} = {} if ! exists $hash{$elements[0]}; my $subHash = $hash{shift @elements}; for (@elements) { $subHash->{$_} = {} unless exists $subHash->{$_}; $subHash = $subHash->{$_}; }; } Dump \%hash; __DATA__ one foo / bar foo / baz foo / qux / two foo / qux / three foo / qux / four five

    Prints:

    $HASH1 = { five => {}, foo => { bar => {}, baz => {}, qux => { four => {}, three => {}, two => {} } }, one => {} };

    DWIM is Perl's answer to Gödel
      The output will be used in an HTML::Template loop, so the name keys are required. Thanks for trying anyways, but the output I gave is what I need, and this won't work for me. :(

        Well why didn't you say so?

        use strict; use warnings; use HTML::Template; use strict; use warnings; use Data::Dump::Streamer; my %hash; while (<DATA>) { chomp; my @elements = split /\s*\/\s*/; next if ! @elements; $hash{$elements[0]} = {} if ! exists $hash{$elements[0]}; my $subHash = $hash{shift @elements}; for (@elements) { $subHash->{$_} = {} unless exists $subHash->{$_}; $subHash = $subHash->{$_}; }; } my @tree; push @tree, buildFamily (\%hash, $_) for keys %hash; my $template = <<'HTML'; <html> <head><title>Test Template</title> <body><TMPL_LOOP NAME="Parents"> Parent: <TMPL_VAR NAME="name"><br> <TMPL_LOOP NAME="children"> Child: <TMPL_VAR NAME="name"><br +> <TMPL_LOOP NAME="children"> Grandchild: <TMPL_VAR NAME=" +name"><br> </TMPL_LOOP></TMPL_LOOP></TMPL_LOOP></body> </html> HTML my $t = HTML::Template->new (scalarref => \$template); $t->param (Parents => \@tree); print $t->output (); sub buildFamily { my ($hash, $parent) = @_; my %root = (name => $parent); if (keys %{$hash->{$parent}}) { my @children; push @children, buildFamily ($hash->{$parent}, $_) for keys %{$hash->{$parent}}; $root{children} = \@children; } return \%root; } __DATA__ one foo / bar foo / baz foo / qux / two foo / qux / three foo / qux / four five

        Prints:

        <html> <head><title>Test Template</title> <body> Parent: five<br> Parent: one<br> Parent: foo<br> Child: bar<br> Child: baz<br> Child: qux<br> Grandchild: three<br> Grandchild: two<br> Grandchild: four<br> </body> </html>

        DWIM is Perl's answer to Gödel
Re: Convert delimited string into nested data structure
by Moron (Curate) on Feb 19, 2007 at 19:30 UTC
    Actually I think OP is right to identify this as a recursion problem given the arbitrary number of levels and I feel one should not question the requirement just to make the solution seem easier (an illusion in this case anyway). My own take on this is that the recursive routine needs to be given the array reference that relates to the corresponding depth in the structure, something like: (untested)
    my @result; while( <> ) { chomp(); my @fld = split /\s*\/\s*/; InsertFlds( \@result, @fld ); } sub InsertFlds { my $aref = shift; my $name = shift; defined ($aref -> [ $#$aref ]{ name }) or push @$aref, { name => $n +ame }; if ( @_ ) { $aref -> [ $#$aref ]{ children } ||= []; InsertFlds( $aref -> [$#$aref ]{ children }, @_ ); } }

    -M

    Free your mind

Re: Convert delimited string into nested data structure
by philcrow (Priest) on Feb 19, 2007 at 18:47 UTC
    You need a stack, but not recursion. For each item, split it on the delimiter. Count the parts. That is your current level. If the current level is the same as the level of the element on top of the stack, pop the stack and push the current item onto it. If the current level is deeper than the top of the stack, push the current item. If the level is smaller (shallower) than the top of the stack, pop until the stack top has a smaller level than the current node, then push the current node.

    After popping to the right level, and BEFORE pushing the current node, insert information from the current node into the array owned by the current stack top.

    Your nodes are ok, except that I would add level to name and children. Also, add a dummy root node at level 0.

    Phil

      I followed you all the way up until the fifth sentance:

      "If the current level is the same as the level of the element on top of the stack ..."

      What is the "element" you are talking about? What are you storing in the stack? How can I attach children using this method?

      Thanks.

        Perhaps I should show a bit of code...
        my @stack = { name => 'root', level => 0, children => [] }; foreach my $input ( @inputs ) { my @pieces = split /\s+\/\s+/, $input; my $level = scalar @pieces; my $new_node = { name => pieces[-1], level => $level }; if ( $level == $stack[-1]{level} ) { pop @stack; } elsif ( $level < $stack[-1]{level} ) { pop @stack while ( $level <= $stack[-1]{level} ); } # else top level must be our parent push @stack[-1]{children}, $new_node; push @stack, $new_node }
        I wrote this up a few years ago.

        Phil

Re: Convert delimited string into nested data structure
by educated_foo (Vicar) on Feb 19, 2007 at 18:41 UTC
    I think you might be better off with a hash of hashes, e.g.
    %foo = (one => 1, foo => { bar => 1, # etc. } );
    but in any case, could you post what you've tried so far?
      use strict; use warnings; use Data::Dumper qw( Dumper ); my %tree; while (<DATA>) { chomp; my $p = \%tree; foreach (split(qr{\s*/\s*}, $_)) { $p = $p->{$_} ||= {}; } } print Dumper \%tree; __DATA__ one foo / bar foo / baz foo / qux / two foo / qux / three foo / qux / four five

        A little recursive sub to compile this into the right format:

        use strict; use warnings; use Data::Dumper qw( Dumper ); my %tree; while (<DATA>) { chomp; my $p = \%tree; foreach (split(qr{\s*/\s*}, $_)) { $p = $p->{$_} ||= {}; } } sub re_format { my $data = shift; my $out = []; for my $key (keys %$data) { my $temp = { name => $key}; if (keys %{ $data->{$key} }) { $temp->{children} = re_format($data->{$key}) } push @$out, $temp; } return $out; } print Dumper('First Step' => \%tree, 'Final' => re_format(\%tree)); __DATA__ one foo / bar foo / baz foo / qux / two foo / qux / three foo / qux / four five

        ___________
        Eric Hodges
        Are you suggesting that with a few modifications that I can use your code snippet to achieve my goal? Because I don't see how I can push the children onto their parents using your code (which doesn't produce the output I need).

        But thanks anyways -- good ideas in there.

      Well, I am quite scatter-brained at the moment. Under more ideal circumstances, I would "sleep on this problem" and solve it in the morning -- but I have to get this part done ASAP so I can continue with the larger task at hand.

      As such I keep tearing down my past efforts -- but just so you know I am working on this instead of just waiting for others to do my work for me ...

      sub recurse { my ($ref, $car, @cdr) = @_; if ($prev_car eq $car) { my $hash = { name => $car, children => [] }; push @$ref, $hash; return unless @cdr; $prev_car = $car; recurse($hash->{children}, @cdr); } push @$ref, { name => $car, children => [] }; return unless @cdr; $prev_car = $car; recurse($ref, @cdr); }
      Which produces the following output: I forgot to say "Thanks in advance" too. :)
        If your keys are well-behaved, then this will do the trick:
        while (<>) { chomp; s!\s*/\s*!}{!g; eval "\$h\{$_\} = 1;"; }
          A reply falls below the community's threshold of quality. You may see it by logging in.
          A reply falls below the community's threshold of quality. You may see it by logging in.