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

Hi monks,

In the following code, I'm trying to sort the data according to marks:
use strict; #use warnings; # I'm trying to simulate input from a textfile containing # lines of NameSpaceMarks my @names_marks = ("cliff 76", "john 52", "keith 90", "rob 52"); my (%hash, @names, @marks); foreach (@names_marks) { ($names[$_], $marks[$_]) = split / /, $_; $hash{"$marks[$_]".$names[$_]} = ucfirst($names[$_]). " " . $marks[$ +_]; } foreach my $key (sort {$b <=> $a} keys %hash) { # Prints each value print "\$key: $key \$value: $hash{$key}\n"; } #Output of print $key: 90keith $value: Keith 90 $key: 76cliff $value: Cliff 76 $key: 52rob $value: Rob 52 $key: 52john $value: John 52
I'm able to get the output I expected i.e. the sorted values (Keith 90, followed by Cliff 76 and so on).

The reason I need to append the name to the marks is because I need the hash to contain unique values. But when I do that and with strict warnings on, I get warnings that the element in sort is not numeric. In fact, there are also warnings that the element in the array is not numeric. Can I ignore those warnings?

And how can I improve the code?

I look forward to reading your comments and improvements. Thanks in anticipation.

kiat

Replies are listed 'Best First'.
Re: sort hash elements...
by Chady (Priest) on Mar 27, 2002 at 12:30 UTC

    If your really need the hash.. maybe this will work:

    my @names_marks = ("cliff 76", "john 52", "keith 90", "rob 52"); my (%hash, @names, @marks); foreach (@names_marks) { my ($name, $mark) = split / /, $_; $hash{$_} = [ucfirst($name), $mark]; } ## then sort by the $mark foreach my $key (sort {$b->[1] <=> $a->[1]} keys %hash) { print "\$key: $key \$value: $hash{$key}->[0] $hash{$key}->[1]\n"; }

    but if you only want to sort, try the Efficient sorting using the Schwartzian Transform

    my @names_marks = ("cliff 76", "john 52", "keith 90", "rob 52"); my @sorted_names = map { $_->[0] } sort { $b->[1] <=> $a->[1] } map { [$_, (split(/ /, $_))[1] ] } @names_marks;

    update: fixed typo in code.

    Update 2: fixed missing parens in second part of code.


    He who asks will be a fool for five minutes, but he who doesn't ask will remain a fool for life.

    Chady | http://chady.net/
      Hi chady, Thanks for helping :)

      I tried out your pieces of code. The first one does not sort the results in descending order (I forgot to mention the descending order bit) and the second one doesn't work - there is an error somewhere but I can't figure out what it was.

      Any advice?

        to sort in descending order, just reverse $a and $b.. and as for the second code.. I fixed it up, there was a missing parens.. sorry.


        He who asks will be a fool for five minutes, but he who doesn't ask will remain a fool for life.

        Chady | http://chady.net/
      Hi Chady,

      Thanks! I tried out your second piece of code. The error is gone and I was able to print the results. However, my intention was to sort the results accoding to score in descending order, and not according to alphabetical order. Thus the output should be "Keith 90, Cliff 76, John 52, Rob 52).

      cheers,

      kiat
Re: sort hash elements...
by particle (Vicar) on Mar 27, 2002 at 14:15 UTC
    i'm not sure of your requirements. you say you need unique keys... is that because there may be two "keith"s? what if they each get a 90? in your implementation, they're still not unique. or is it because rob and john might both get a 52? if this is a hash value, then there isn't a uniqueness problem.

    if the name alone is good for uniqueness, you can use this simple short method, which works with the data you provided.

    #!/usr/bin/perl -w use strict; $|++; my @names_marks = ( 'cliff 76', 'john 52', 'keith 90', 'rob 52' ); my %hash = map { split } @names_marks; my @sorted_names = sort { $hash{$b} <=> $hash{$a} } keys %hash; print $hash{$_},' ',$_,$/ for @sorted_names;
    also, your code
    foreach (@names_marks) { ($names[$_], $marks[$_]) = split / /, $_; $hash{"$marks[$_]".$names[$_]} = ucfirst($names[$_]). " " . $marks[$ +_]; }
    is not correct. $_ contains the value from @names_marks, not the index. so you're trying to access $names[keith], which isn't numeric. you cannot ignore these warnings, your code is broken.

    anytime you think 'unique', think hash. i don't think you need arrays at all, but you might know something i don't.

    ~Particle ;Þ

      Hi Particle,

      Thanks! I tried out your code and got it works perfectly. Thanks for pointing '$_' out - I got confused with 'for' and 'foreach' and thought it was an index.

      I don't understand the following line in your code and would appreciate it very much if you could explain how it works:

      my %hash = map { split } @names_marks;
      Another question I've is "What if I've two persons with the same name?". When I added another 'john' to the data, only one of them shows up. How do I avoid this problem?

        first, i'll explain my code:
        my %hash = map { split } @names_marks;
        okay, let's break it down.

         @names_marks is a list of items, containing strings with two items seperated by a single space, such as 'keith 90'. you want to seperate these items.

        split, by default, will split on spaces, and will split $_. it's in the documentation: online at split, or command line at perldoc -f split. so split could be replaced with split ' ', $_

        i can be sure that split will return a list, since the doc (for perl 5.6.1) says scalar context is deprecated. since i know the data only contains one space, an item on either side, i can use these two items to assign to a hash.

        it's perfectly legal to say

        my %hash = ( 'bob', 1 ); # or my %hash = ( bob => 1 );
        where i'm assigning the first list element as a key in %hash, and the second list element as the key's value.

        on a sidenote--you can use Data::Dumper; to see into data structures. add <use Data::Dumper;</code> to the top of the script, and print Dumper [%hash]; after the hash is created. you'll see the keys and values. this is particularly helpful for debugging complex data structures.

        map is somewhat similar to for or foreach, when used to iterate over a list, see the doc: map. (by the way, for and foreach are the same, docs: for, foreach.)

        i could turn my map into a for:

        my %hash; for( @names_marks ) { %hash = split } # or, expanding split for( @names_marks ) { %hash = split ' ', $_ }
        note there's no semi-colon between the braces--this is okay because the final statement in braces does not require a trailing semi-colon.

        okay, i hope that helps you a bit. now, on to the problem of two persons with the same name. my questions to you are: how do *you* know who's who? if you're reporting who got what marks, and there are two johns, wouldn't each want to know which mark he received?

        in this case, you'll need unique names, like 'john1', or 'john q. public'. the former will work with my current implementation. the latter will require some coding changes. for instance, space is no longer a good seperator for @names_marks, because the names might have spaces in them. instead, use a pipe, or colon, or carat, or question mark, or exclamation point. make sure it's some character that won't show up in either data field.

        then, split the data on the seperator instead of space. hash keys can have spaces in them, so the rest of my example should still work. although, if this were important code, i'd recommend a more robust implementation than the one i've provided.

        ~Particle ;Þ