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

I have hashes with numbers as keys (it makes it easier to sort properly that way). I have an admin file which allows the user (or me) to delete an element of the hash.

What I'm trying to do is after the delete occurs, rebuild the hash again so there aren't any number gaps with the keys.

If you run the code below, the results will be:

C:\Documents and Settings\my\Desktop>perl test.pl 0 ==== zero 1 ==== one 2 ==== three 3 ==== four 4 ==== four
I don't know what's happened, but I should only have 0-3 left in my hash since I deleted {2} and for some reason it prints FOUR twice.

Where did I go wrong?

#!/usr/bin/perl use warnings; use strict; my %hash = ( "0" => "zero", "1" => "one", "2" => "two", "3" => "three", "4" => "four" ); delete $hash{2}; my $num = "-1"; foreach (sort keys %hash) { $num++; my $value = $hash{$_}; $hash{$num} = $value; } foreach (sort keys %hash) { print "$_ ==== $hash{$_}\n"; }

Replies are listed 'Best First'.
Re: rebuilding hashes
by Zaxo (Archbishop) on Feb 02, 2004 at 19:06 UTC

    Once the '2' element is deleted, you go through the hash with a counter and reassign (2 => 'three', 3 => 'four' ). The '4' element remains, as well.

    I'm suspicious of the kind of sorting you do. What happens when you have 42 elements? Your sort will go 0,1,10,11,12... which I'm not convinced you will expect.

    Why not use aa array for this? You can delete elements with splice and the renumbering will be automatic.

    After Compline,
    Zaxo

      I still don't quite understand why 'four' is being repeated. When I delete {2} the key and value of 3/three should be removed and a print with the result:
      0 ==== zero 1 ==== one 2 ==== three 3 ==== four
      I wrote a note above that goes over why I'm not using an array. This hash in reality isn't being used, this is a test script I setup to see if it would work. I'm actually tying this hash to SDBM and sorting by numbered keys has never been a problem for me using for (grep defined($_), (sort { $a <=> $b } keys %code).

      The reason I need to rebuild the hash is because in the admin panel, when they remove a key/value, the user is returned to a menu with back and foward buttons that lets them scroll the data. For the forward button, I increment the key value by one, and the opposite for the back button. BUT, if we have keys 1, 2, 3, 4, 5 and they remove 3, the navigation buttons crash between two and four because it loads 3 which doesn't exist.

        I still don't quite understand why 'four' is being repeated.

        It's because you still have a key '4' with a value of "four" that you didn't remove from the hash. Perhaps an illustration. Your hash just after you delete the entry keyed by '2':

        0 === zero 1 === one 3 === three 4 === four

        Then you loop over the keys creating new entries. After the first iteration of the loop your hash looks like this:

        0 === zero # this one reassigned 1 === one 3 === three 4 === four

        Second iteration:

        0 === zero 1 === one # this one reassigned 3 === three 4 === four

        Third iteration:

        0 === zero 1 === one 2 === three # this one created 3 === three 4 === four

        Fourth iteration:

        0 === zero 1 === one 2 === three 3 === four # this one reassigned 4 === four

        See?

        I still don't quite understand why 'four' is being repeated.
        Abigail-II actually answered this question for you:
        Third, it's showing four twice because you never delete the highest number when re-assigning the numbers.
        I hate to push the issue, but even with your further description, an array sounds like a more appropriate (and less error-prone) data-structure for this problem. Matter of fact you wouldn't even have this problem at all if you used a tied-array for this. It seems to me that your index-keys aren't even really nessecary at all, they are just indecies to the natural order of your list.

        With an array, your HTML-display code could just use a "counter" variable for your page number display rather than the hash-key. Something like this.

        foreach my $page_num (1 .. scalar @pages) { print "<A HREF='script.pl?page=$page_num'>page $page_num</A>"; }
        Then your CGI code would just use the value of "page" as an index to your array, and it will always be guarenteed to be there (assuming they don't reach over the bounds of your array in some way, but thats another problem).

        If this weren't a tied hash i might recommend re-building it from a totally empty (new) hash then you wouldn't need to deal with this problem at all, but i assume the performance penalty on that would be painful.

        -stvn
        As Abigail-II said, you never do anything that would delete the {4} key. Assigning its value to the {3} key doesn't affect {4}.

        A crude way to fix it: After your reassignment loop, delete keys until there are only 1 + (original)$num.

        my $to_delete = keys(%hash) - $num+1; while ($to_delete) { --$to_delete if defined delete $hash{++$num}; }
Re: rebuilding hashes
by Limbic~Region (Chancellor) on Feb 02, 2004 at 19:04 UTC
    coldfingertips,
    I seriously question using numeric keys soley on the basis of sorting or the need to re-order them when they are deleted, but I think this will do what you want:
    #!/usr/bin/perl -w use strict; my %hash = (1 => 'foo', 2 => 'bar', 3 => 'asdf', 4 => 'blah', 10 => 'w +oah'); delete $hash{3}; FixHash(\%hash); print "$_ : $hash{$_}\n" for sort { $a <=> $b } keys %hash; sub FixHash { my $hashref = shift; my $index = 0; for my $key ( sort { $a <=> $b } keys %$hashref ) { $index++; next if $key == $index; $hash{$index} = delete $hash{$key}; } }
    I would probably look at one of the modules for sorted hashes like Tie::IxHash and Tie::Hash::Sorted or use an array.

    Cheers - L~R

Re: rebuilding hashes
by diotalevi (Canon) on Feb 02, 2004 at 19:05 UTC

    This makes sense when used as an array. You ought not to use used a hash here.

    #!/usr/bin/perl use warnings; use strict; my @whatevers = qw( zero one two three four ); splice @whatevers, 2, 1; foreach my $ix ( 0 .. $#whatevers ) { print "$ix ==== $whatever[$ix]\n"; }
Re: rebuilding hashes
by Abigail-II (Bishop) on Feb 02, 2004 at 19:03 UTC
    A couple of things. First, you really ought to use an array here, instead of simulating an array with a hash. Second, I doubt you want to sort lexicographically. Third, it's showing four twice because you never delete the highest number when re-assigning the numbers.

    I'm not going to waste time in showing you corrected code. Your design is absurd. Use an array.

    Abigail

    A reply falls below the community's threshold of quality. You may see it by logging in.