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

I have a hash that's keyed with integers. The integers can be any ol thing, say for example:

hash{-10} = bar hash{-1} = foo hash{12} = baz

Now I want to change the keys of the hash to be sequential, starting at 0, so that I would have:

hash{0} = bar hash{1} = foo hash{2} = baz

I wrote this:

$counter=0; foreach (sort keys(%hashone)) { $hashone{$counter} = $hashone{$_}; delete $hashone{$_}; $counter++; }

but it does odd things, like tossing out memory addresses and stuff. (incidentally, it always messes $hash{0} = some memory address) I also realize that I will have problems if my original hash was something like:


hash{-10} = bar hash{1} = foo hash{20} = baz

Can this be done without swapping values with a temporary hash? Thanks!

Edited by davido: Added code tags.

Replies are listed 'Best First'.
Re: re-key a hash
by Ovid (Cardinal) on Aug 02, 2004 at 21:29 UTC

    A hash is great if the data is sparsely populated, but if you make the keys sequential integers, why not go with an array?

    my @array = @hash{ sort {$a <=> $b} keys %hash};

    Incidentally, note that you're deleting hash elements while iterating over it. You're not supposed to do that.

    Update: To keep a hash, try something like this:

    my %hash2; @hash2{ 0 .. (keys %hash)-1 } = map { $hash{$_} } sort {$a <=> $b} key +s %hash;

    It's really ugly, though :)

    Update 2 ysth and Aristotle both caught me sleeping. Pay no attention to the man in the classical poet outfit.

    Cheers,
    Ovid

    New address of my CGI Course.

      @hash2{ 0 .. (keys %hash)-1 } = map { $hash{$_} } sort {$a <=> $b} key +s %hash;

      You had it right before; why did you think you need the map there?

      @hash2{ 0 .. (keys %hash)-1 } = @hash{ sort { $a <=> $b } keys %hash } +;

      I'm too lazy to verify, but I think you don't need to assign to a separate hash if you do it this way, either. The list should be assigned atomically once it is built completely.

      Makeshifts last the longest.

      You are explicitly allowed to delete each element as you iterate; you ought not to make other changes (as this code does).
        I can understand the caveat if you're iterating over values or with each, but sort keys is not a hash iterator.

        We're not really tightening our belts, it just feels that way because we're getting fatter.
Re: re-key a hash
by shemp (Deacon) on Aug 02, 2004 at 21:41 UTC
    You're losing some values because you're overwriting them before they get dealt with. The first key processed overwrites the value of $hash{0}. You could put the 'ordered' info into a different hash, and this wouldnt happen anymore.

    Also, sort by default is a string comparison sort, so it goes like 1,10,11,12,13,14,15,16,17,18,19,2,20, etc, and negaitve values would come before the positive. Run this bit of code:

    foreach my $i (sort (-20 .. 20)) { print "$i\n"; }
    To do a numeric sort, you need
    sort {$a <=> $b} (keys %hashone)
    In the end, i'd still agree with ovid about what you're doing!
Re: re-key a hash
by ysth (Canon) on Aug 02, 2004 at 22:17 UTC
    First get a list of the values in sorted key order. Note that @hash{ LIST } produces a list of the values corresponding to the keys in list.
    my @values = @hash{ sort { $a <=> $b } keys %hash };
    Then clear your hash and assign the values using sequential keys:
    %hash = (); @hash{ 0..@values-1 } = @values;
    or do it all in place without a temporary array:
    @hash{ keys(%hash), 0..keys(%hash)-1 } = ((undef) x keys(%hash), @hash +{ sort { $a <=> $b } keys %hash }); delete @hash{ grep !defined $hash{$_}, keys %hash };
    Update:
    @hash{ 0..keys(%hash)-1 } = delete @hash{ sort { $a <=> $b } keys %has +h };
    almost works, but the keys(%hash) on the left is evaluated after the hash has been empty :(

      The OP is not interested in preserving the old keys, so a bunch of the hoops you're jumping through is superfluous.

      Makeshifts last the longest.

        The hoops are necessary to avoid preserving the old keys.
Re: re-key a hash
by Aristotle (Chancellor) on Aug 02, 2004 at 23:08 UTC

    Here's my shot at doing it in a single step — but don't try this at home.

    %hash = ( 0 .. keys( %hash ) - 1, @hash{ sort { $a <=> $b } keys %hash } )[ map +( $_, $_ + keys %hash ), 0 .. keys( %hash ) - 1 ];

    Makeshifts last the longest.

      Much better :)

      { local $_ = keys( %h ) -1; @h{ 0 .. $_ } = delete @h{ sort { $a<=>$b } keys %h }; }

      Examine what is said, not who speaks.
      "Efficiency is intelligent laziness." -David Dunham
      "Think for yourself!" - Abigail
      "Memory, processor, disk in that order on the hardware side. Algorithm, algorithm, algorithm on the code side." - tachyon

        If we're using multiple statements anyway (not doing so was the point of my obscure ditty), I'd shuffle things around.

        { my @key = sort { $a <=> $b } keys %h; @h{ 0 .. $#key } = delete @h{ @key }; }

        That seems like a cleaner separation of concerns. The symmetry of the second line here seems more pleasing and makes it more robust — it remains valid regardless of how the list of keys is generated.

        Makeshifts last the longest.

      Let the garbage collector do the deletes... I know, I know, it's not the problem spec!
      my $count = 0; %hash = map { $count++ => $a{$_} } sort { $a <=> $b } keys %hash;

        There's no delete in mine either of course, and you still need two statements your way. :-) It's what I'd probably use in production code though.

        Makeshifts last the longest.

      CPU is no object:
      %hash = map { ($_, $hash{(sort keys %hash)[$_]}) } 0..keys(%hash)-1;

      Caution: Contents may have been coded under pressure.

        That is a bit silly… but I'll concede that it's less obscure than mine. :-)

        Makeshifts last the longest.

      Very nice!
Re: re-key a hash ( Best (-: )
by ccn (Vicar) on Aug 02, 2004 at 22:08 UTC

    $hash{-10} = 'bar'; $hash{-1} = 'foo'; $hash{12} = 'baz'; %hash = map { (keys %hash) - 1 => delete $hash{$_} } sort {$b <=> $a} keys %hash; print map {"$_ => $hash{$_}\n"} sort { $a <=> $b } keys %hash;

      What's the point of your delete? You're assigning a completely new list to that hash in the end anyway.

      It certainly doesn't seem like the "best" version to me, at least in terms of my usual metric: self-documentation.

      Makeshifts last the longest.

        Since ccn didn't answer your question... The delete is there to make the calculation of the new keys ( (keys %hash) - 1 ) work.

        - tye        

        Ok, it's not for production use, but it's fun. I like such stuff.

Re: re-key a hash
by BrowserUk (Patriarch) on Aug 02, 2004 at 22:59 UTC

    Yes, but it requires a temporary scalar.

    my $n = keys( %hash ) -1; @hash{ 0 .. $n } = delete @hash{ sort { $a<=>$b } keys %hash };

    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "Think for yourself!" - Abigail
    "Memory, processor, disk in that order on the hardware side. Algorithm, algorithm, algorithm on the code side." - tachyon
Re: re-key a hash (tye)
by tye (Sage) on Aug 03, 2004 at 01:07 UTC
    use Algorithm::Loops 'MapCarMin'; %hash = MapCarMin {@_} [ 0 .. keys %hash ], [ @hash{ sort {$a<=>$b} keys %hash } ];
    or
    %hash = map { ref($_) && $_ == \$hash{''} ? ( delete $hash{''} )[1..0] : ( ++$hash{''}, $_ ) } @hash{ sort {$a<=>$b} keys %hash }, \$hash{''};
    ( though this next one not working is quite clearly and simply a bug in Perl:
    @hash{ 1..keys %hash } = delete @hash{ sort {$a<=>$b} keys %hash };
    ! :)

    - tye        

      by-pass road is modifying of the left part and a bit of right one:

      $hash{keys %hash} = $_ for delete @hash{sort { $a<=>$b } keys %hash};
      ( though this next one not working is quite clearly and simply a bug in Perl
      @hash{ 1..keys %hash } = delete @hash{ sort {$a<=>$b} keys %hash };

      That was my thought until I realised that by the time the right-hand side of the assignment is evaluated, you've already deleted all the keys, so scalar keys %hash is zero. Whether you run the iterator from 0 as the OP asked or from 1 as your have it, it generates no keys and the values go in the bin.


      Examine what is said, not who speaks.
      "Efficiency is intelligent laziness." -David Dunham
      "Think for yourself!" - Abigail
      "Memory, processor, disk in that order on the hardware side. Algorithm, algorithm, algorithm on the code side." - tachyon

        /msg BrowserUk Turn on your chatterbox nodelet

        My point (which includes a smiley) is that the left-hand side should be evaluated enough to give us full context (including how many slots we will be assigning into) before the right-hand side code is run in that context.

        - tye