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

Hi Monks
I would like your wisdom regarding why my code below does not work:
Task : from the given hash, rank the keys based on the values, also taking into account that if 2 or more keys have the same value, the get the same ranking.
my %data = ( 'car' => 180, 'motorcycle' => 150, 'skate' => 150, 'bird' => 120, ); my @keys = keys %data; my @sorted[ sort { $data{$keys[$b]} <=> $data{$keys[$a]} } 0..$#keys ] + = 1..@keys; my $rank = 1; my @ranks = (); foreach $count (0..$#sorted) { $rank = $count + 1 if ($count > 0 && $sorted[$count] != $sorted[$c +ount - 1]); push @ranks, $rank; } print join ",", @ranks;

The code prints 1,2,3,4, while it should print 1,2,2,3 since motorcycle and skate have the same value. Also, could you tell me how I can also print the key next to its ranking (motorcycle, skate etc)?
Many thanks!

Replies are listed 'Best First'.
Re: Sorting and ranking
by tybalt89 (Monsignor) on Jan 21, 2018 at 16:33 UTC

    And now for something completely? different...

    #!/usr/bin/perl # http://perlmonks.org/?node_id=1207623 use strict; use warnings; my %data = ( 'car' => 180, 'motorcycle' => 150, 'skate' => 150, 'bird' => 120, ); my ($n, @rank) = 1; $rank[ $data{$_} ] .= "$_\n" for sort keys %data; defined and $n += print s/^/$n - /gmr for reverse @rank;

    Outputs:

    1 - car 2 - motorcycle 2 - skate 3 - bird

      This made more sense to me once I had a look at the array contents. The rank array stores the key names at the index locations corresponding to the values.

      use strict; use warnings; use Data::Dumper; my %data = ( 'car' => 10, 'motorcycle' => 7, 'skate' => 7, 'board' => 2, 'roller' => 4, 'vase' => 4, 'bird' => 4, ); my ($n, @rank) = 1; foreach( keys %data){ $rank[ $data{$_} ] .= "$_\n" ; } print Dumper(\@rank); defined and $n += print s/^/$n - /gmr for reverse @rank; __DATA__ $VAR1 = [ undef, undef, 'board ', undef, 'vase bird roller ', undef, undef, 'motorcycle skate ', undef, undef, 'car ' ]; 1 - car 2 - motorcycle 2 - skate 3 - vase 3 - bird 3 - roller 4 - board

        You've discovered my secret. Oh, wait, it's not a secret, it's a distribution sort.

        To see a hardware implementation of a distribution sort, check out IBM card sorter.

      $rank[ $data{$_} ] .= "$_\n" for sort keys %data; defined and $n += print s/^/$n - /gmr for reverse @rank;

      Clever as always :-) It should be noted, however, that memory usage (and speed) gets worse for increasingly large values in the hash, since it causes @rank to grow accordingly.

        Of course. One of the beauties of perl, however, is the ability to easily switch over to using a hash. The sort will have to be explicit, of course. instead of getting it as a freebie with the array.

        As a bonus, you get the ability to properly handle negative and/or non-integer values.

        #!/usr/bin/perl # http://perlmonks.org/?node_id=1207623 use strict; use warnings; my %data = ( 'car' => 180, 'motorcycle' => 150, 'skate' => 150, 'bird' => 120, ); my ($n, %rank) = 1; $rank{ $data{$_} + 0 } .= "$_\n" for sort keys %data; $n += print s/^/$n - /gmr for @rank{ sort { $b <=> $a } keys %rank };
Re: Sorting and ranking
by haukex (Archbishop) on Jan 21, 2018 at 13:10 UTC
    The code prints 1,2,3,4,

    The code you've posted doesn't print that, it prints 'syntax error at 1207623.pl line 10, near "@sorted["'. Please post the actual code you are running. (How do I post a question effectively?) Also, always Use strict and warnings.

    If I assume you meant my @sorted; @sorted[...] = ...;, then the code runs, but I think you're making things a little too complicated with the various arrays - note that the comparison $sorted[$count] != $sorted[$count-1] should always be true because you've assigned @sorted[...] = 1..@keys; that is, all the values of @sorted are different. Have a look at the Basic debugging checklist, especially the advice to use Data::Dumper or Data::Dump to look at your data structures.

    Here's one way to approach this. I first sort the keys (see also How do I sort an array by (anything)?), and then just run through those keys and look at the current and previous value, increasing the rank if they differ. (This assumes no undef values in the hash, since I use that value to keep track if there was a $previous value or this is the first.)

    use warnings; use strict; use Data::Dump; my %data = ( 'car' => 180, 'motorcycle' => 150, 'skate' => 150, 'bird' => 120, ); my @keys = sort { $data{$b} <=> $data{$a} or $a cmp $b } keys %data; my ($prev,$rank); for my $k (@keys) { $rank++ unless defined($prev) && $prev==$data{$k}; dd $k, $data{$k}, $rank; $prev = $data{$k}; } __END__ ("car", 180, 1) ("motorcycle", 150, 2) ("skate", 150, 2) ("bird", 120, 3)
      Great! Thank you for your help!
Re: Sorting and ranking
by 1nickt (Canon) on Jan 21, 2018 at 13:45 UTC

    Hi, keep track of the ranking as you go through the sorted list, and only increment it if the value is lower.

    Update: ++haukex posted basically the same solution while I was dithering over syntax, LOL!

    use strict; use warnings; use feature 'say'; use Data::Dumper; $Data::Dumper::Sortkeys = $Data::Dumper::Terse = 1; my %data = ( car => 180, motorcycle => 150, skate => 150, bird => 120, ); my @sorted = sort { $data{$b} <=> $data{$a} } keys %data; my $rank; my %result; for ( 0 .. $#sorted ) { my $curr_key = $sorted[$_]; my $prev_key = $_ ? $sorted[$_ - 1] : undef; if ( not $prev_key ) { $rank = 1; } elsif ( $data{$curr_key} != $data{$prev_key} ) { $rank++; } else { # values are the same, leave $rank unchanged } $result{$curr_key} = { val => $data{$curr_key}, rank => $rank }; } say "$_: ", Dumper $result{$_} for @sorted; __END__
    Output:
    perl 1207623.pl car: { 'rank' => 1, 'val' => 180 } skate: { 'rank' => 2, 'val' => 150 } motorcycle: { 'rank' => 2, 'val' => 150 } bird: { 'rank' => 3, 'val' => 120 }

    Hope this helps!


    The way forward always starts with a minimal test.
Re: Sorting and ranking
by johngg (Canon) on Jan 21, 2018 at 13:50 UTC

    Use a HoA keyed by the value and sort that in descending order, incrementing rank for each key. Uncomment the Data::Dumper lines to see the structure of the HoA. The code

    use strict; use warnings; use 5.022; # use Data::Dumper; my %data = ( car => 180, motorcycle => 150, skate => 150, bird => 120, ); my %byValue; push @{ $byValue{ $data{ $_ } } }, $_ for keys %data; # print Data::Dumper->Dumpxs( [ \ %byValue ], [ qw{ *byValue } ] ); my $rank; for my $descVal ( sort { $b <=> $a } keys %byValue ) { $rank ++; say qq{$rank - $_} for sort @{ $byValue{ $descVal } }; }

    The output.

    1 - car 2 - motorcycle 2 - skate 3 - bird

    I hope this is helpful.

    Update: An updated script to show the difference between "rank" and "place" as in "Fred and Mary were placed 3rd equal."

    use strict; use warnings; use 5.022; my %data = ( car => 180, tortoise => 90, concorde => 300, motorcycle => 150, bird => 120, skate => 150, pedestrian => 100, rocket => 300, aeroplane => 210, unicycle => 175, sheep => 100, frog => 120, cow => 110, bus => 150, ); my %byValue; push @{ $byValue{ $data{ $_ } } }, $_ for keys %data; say q{Rank Place Item}; my $rank = 0; my $place = 1; for my $descVal ( sort { $b <=> $a } keys %byValue ) { $rank ++; my $count = scalar @{ $byValue{ $descVal } }; my $eq = $count > 1 ? q{=} : q{ }; printf qq{%3d%5d%1s %s\n}, $rank, $place, $eq, $_ for sort @{ $byValue{ $descVal } }; $place += $count; }

    The output.

    Rank Place Item 1 1= concorde 1 1= rocket 2 3 aeroplane 3 4 car 4 5 unicycle 5 6= bus 5 6= motorcycle 5 6= skate 6 9= bird 6 9= frog 7 11 cow 8 12= pedestrian 8 12= sheep 9 14 tortoise

    Cheers,

    JohnGG

Re: Sorting and ranking
by Anonymous Monk on Jan 21, 2018 at 12:06 UTC

    why do you get the keys and not the values if you want to rank the keys based on the values?

      But my sorting is actually based on the values, or?
      sort { $data{$keys[$b]} <=> $data{$keys[$a]}

        All i meant was why not take the values iso the keys and sort those.

Re: Sorting and ranking
by Anonymous Monk on Jan 24, 2018 at 09:36 UTC

    After i thought about it for a long time i came up with this:

    use strict; use warnings; my %list=( 'car' => 180, 'motorcycle' => 150, 'skate' => 150, 'bird' => 120, ); my @val = sort{$list{$a}<=>$list{$b}} keys %list; my $teller = 0; my $test = 0; foreach my $value(@val){ if($test == $list{$value}){ print "$teller $value $list{$value}\n"; } else{ $teller++; print "$teller $value $list{$value}\n"; $test = $list{$value}; } }