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

Fellow Monasterians,

I'm stumped, and I'm sure it's something obvious I'm overlooking, or just not understanding. The object is to delete a matching element in a larger AoH. If I splice off the last match, or last element, the array is decreased properly and all is fine. If I splice out the first match, the hash key and value are deleted, but the empty hash or array element is still here. Tried deleting and got different results, but not the desired (see below). This arrayref is eventually going to be a list of options for an HTML dropdown box and I don't need the extra blank option(s) that are showing up. Ideas? TIA

#!/usr/bin/perl print "Content-type: text/plain\n\n"; use strict; use Data::Dumper; my @AoH_all = ( { name => "Bill", id => 1 }, { name => "Mike", id => 3 } ); my @AoH_one = ( { name => "Bill", id => 1 } ); my $AoH_all = \@AoH_all; my $AoH_dup = \@AoH_all; my $AoH_one = \@AoH_one; for my $j (0 .. $#$AoH_all) { for my $i (0 ..$#$AoH_one) { if ($AoH_one->[$i]{'id'} == $AoH_all->[$j]{'id'}) { splice( @{$AoH_dup}, $j, 1 ); } } } print Dumper($#$AoH_dup, $AoH_dup )."\n";
prints fine:
$VAR1 = 0; $VAR2 = [{'id' =>1, 'name' => 'Bill'}];
BUT I change @AoH_one to: name => "Bill", id => 1 it prints (note the extra {} ):
$VAR1 = 1; $VAR2 = [{'id' =>3, 'name' => 'Mike'}, {} ];
Instead of splicing, tried:
delete $AoH_dup->[$j]{'name'}; delete $AoH_dup->[$j]{'id'}; delete $AoH_dup->[$j];
and it Dumped:
$VAR1 = 1; $VAR2 = [ undef,{'id' =>3, 'name' => 'Mike'} ];
Not any better for my final application. Thanks again.

—Brad
"A little yeast leavens the whole dough."

Replies are listed 'Best First'.
Re: Splice of ref to AoH not removing element
by chromatic (Archbishop) on May 29, 2004 at 21:34 UTC

    Adding to or removing from an array you're iterating over is a bad idea. If you create a loop over two elements (indices 0 and 1) and, on the first iteration, remove one element, what will happen on the second iteration?

    An alternate approach would be to build up a third data structure to hold the difference of the two sets. I think perlfaq4 has the right idea. The code might be (warning, untested!):

    my @AoH_all = ( { name => "Bill", id => 1, }, { name => "Mike", id => 3 }); my @AoH_one = ( { name => "Bill", id => 1, } ); my %keep_ids = map { $_->{id} => 1 } @AoH_all; delete @keep_ids{ map { $_->{id} } @AoH_one }; my @difference = grep { exists $keep_ids{ $_->{id} } } @AoH_all;

    Update: two typos fixed.

      Adding to or removing from an array you're iterating over is a bad idea. If you create a loop over two elements (indices 0 and 1) and, on the first iteration, remove one element, what will happen on the second iteration?

      However, if you would iterate in reverse order (1,0) then there would be no problem at all.

      Liz

      Thanks chromatic, it looks like an elegant solution. But I'm still rather new at things like map and grep. I've been trying hard to make your code work, but am reaching the head-banging stage.

      I did add the final right curly bracket to the "delete" line:

      delete @keep_ids{ map { $_->{id} } @AoH_one }; print Dumper (%keep_ids);
      which prints:
      $VAR1 = '3'; $VAR2 = 1;

      So, I have the id of the hash I want to keep, but I'm not sure what is happening on the grep line. Also, I'm not quite sure of how to build the AoH I need for my tmpl_loop from just knowing that resulting id.

      I'm going to continue to work on it, but in the meantime, if another monk comes along that can fill in some of the details, I'm very teachable right now ;^). Thanks!


      —Brad
      "A little yeast leavens the whole dough."

        map and grep are really pretty easy, once you know the secret: read them right to left.

        map iterates over a list, executing the code block for each element in turn, returning a list of whatever the code block returns. Use it to transform one list into another.

        grep iterates over a list, executing the code block for each element in turn, returning a list of only those elements for which the code block returns true. Use it to filter out list elements.

Re: Splice of ref to AoH not removing element
by tkil (Monk) on May 31, 2004 at 03:03 UTC

    If you phrase your problem as: "how do I construct a new list of hrefs that excludes hrefs with certain IDs?", then the grep solution becomes much more obvious.

    # build a hash that contains only excluded IDs my %exclude = map { ( $_->{id} => 1 ) } @AoH_one; # now keep only those that are not excluded: my @keepers = grep { not $exclude{ $_->{id} } } @AoH_all;

    If you actually want to replace @AoH_all with this new list, there is nothing wrong with just assigning back into the same variable:

    @AoH_all = grep { not $exclude{ $_->{id} } } @AoH_all;

    As liz points out, you can use splice if you traverse the indexes in reverse order. You can also track the number of elements remaining.

    # use liz's technique foreach my $i ( reverse 0 .. $#AoH_all ) { if ( $exclude{ $AoH_all[$i]{id} } ) { splice @AoH_all, $i, 1; } } # keep track of things manually my $n_elt = @AoH_all; for ( my $i = 0; $i < $n_elt; ++$i ) { if ( $exclude{ $AoH_all[$i]{id} } ) { splice @AoH_all, $i, 1; --$n_elt; # whoops, bug here, see update... } } # finally, perl updates the length of @AoH_all for us, so: for ( my $i = 0; $i < @AoH_all; ++$i ) { if ( $exclude{ $AoH_all[$i]{id} } ) { splice @AoH_all, $i, 1; # whoops, bug here, see update... } }

    Update

    Both of the last two loops have a serious error. After you splice out an element, everything else is shifted down — but the value shifted into $AoH_all[$i] is never examined again. Oops. We can either subvert the increment, or we can switch to only increment if we didn't do any replacement:

    # adjust $i after replacement so it is reexamined: for ( my $i = 0; $i < @AoH_all; ++$i ) { if ( $exclude{ $AoH_all[$i]{id} } ) { splice @AoH_all, $i, 1; --$i; } } # or, only increment $i if we didn't do a replacement: for ( my $i = 0; $i < @AoH_all; ) { if ( $exclude{ $AoH_all[$i]{id} } ) { splice @AoH_all, $i, 1; } else { ++$i; } }