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

This is an assignment turned in for a programming languages class. It was meant to be a simple introduction to Perl and I wrote it fairly simply. However, I am curious if anyone has thoughts on the code and/or any ideas for more "creative" :) ways to accomplish the problem. Again, I am not asking for help to solve the assignment since it has already been completed, just seeking constructive criticism. :)

Here's the assignment:

Given an input file of the following format

Firstname Lastname list_of_grades(any number of grades)

You should output

Lastname, Firstname Average list_of_grades

The output should be sorted on lastname, followed by first name to break ties on lastnames. Also, the report should mark any students whose average is at least 20% less than the median class average.

Sample Input File

David Allen 50 60 70
Jane Aardmore 100 100 100
John Smith 75 74 72 73 89
Carl Allen 25 100 100 20
Mike Jones 80 85 82 89 88 86 80


Here's my code.

#--------------------------------------------------------------------- +----- #-- Author: Adam McManus #-- Date: 2/10/03 #-- File: program2.pl #-- Description: #-- #--------------------------------------------------------------------- +----- use strict; #-- Check usage die "Usage: $0 input_file\n" if( scalar(@ARGV) != 1 ); #-- Get cmd line arguments my $input_file = $ARGV[0]; #-- file containing student info #-- Declare variables #-------------------------------------------------------------- #-- %students is a hash of student information, indexed by name #-- $students{last,first}{grades} is the array of grades #-- $students{last,first}{avg_grade} is the student's average #-------------------------------------------------------------- my %students; my @averages; #-- sorted array of student averages my @buffer; #-- buffer to read line from file into my $name_index; #-- index for %students "lastname,firstname" my $median_class_avg; #-- median class average #-- Open the input file open(INPUT, '<', $input_file) or die "Couldn't open $input_file: $!\n" +; #-- Read the input file one line at a time while( <INPUT> ) { next if( /^$/ ); #-- skip blank lines @buffer = split; #-- split on whitespace $name_index = "$buffer[1],$buffer[0]"; #-- create the name index #-- Load the student's grades and begin computing the average #-- by summing up the student's grades foreach my $i ( 2 .. $#buffer ) { push @{ $students{$name_index}{grades} }, $buffer[$i]; $students{$name_index}{avg_grade} += $buffer[$i]; } #-- Finish computing the student's average by dividing by number of + grades $students{$name_index}{avg_grade} /= scalar(@{ $students{$name_inde +x}{grades} }); #-- Store the average into the class averages push @averages, sprintf("%.1f", $students{$name_index}{avg_grade}); } #-- Close the input file close(INPUT) or warn "Warning! $input_file was not closed properly: $! +\n"; #-- Sort the array of averages numerically for median purposes @averages = sort { $a <=> $b } @averages; #------------------------------------------------------- #-- Find the class median #-- If even number of grades, take the average of the #-- middle two grades #-- Else, take the middle grade #------------------------------------------------------- if( scalar(@averages) % 2 == 0 ) { my $i = scalar(@averages) / 2; $median_class_avg = ( $averages[$i] + $averages[$i - 1] ) / 2; } else { my $i = sprintf("%d", scalar(@averages) / 2 ); $median_class_avg = $averages[$i]; } #-- Print the header for final report printf "\n%21s%16s%11s\n", 'Name', 'Average', 'Grades'; print '-' x 79 . "\n"; #-- Print the student info foreach my $name ( sort keys %students ) { #-- split out the first and last name of students my ($last, $first) = split(',', $name); #---------------------------------------- #-- Check if the student's avg is lower #-- than median class average and flag #-- if it is #---------------------------------------- if( $students{$name}{avg_grade} < ($median_class_avg * 0.8) ) { printf "**<.8med** %8s, %-10s", $last, $first; } else { printf "%10s %8s, %-10s", ' ', $last, $first; } #-- Print student's average printf "%5.1f%2s", $students{$name}{avg_grade}; #-- Print student's grades foreach ( 0 .. $#{ $students{$name}{grades} } ) { printf "%4d", $students{$name}{grades}[$_]; } print "\n"; } #-- Print the number of students and class median average printf "\nNumber of students processed: %d\n", scalar(@averages); printf "Median Average: %4.1f\n", $median_class_avg; print '-' x 79 . "\n"; #-- Program End


Thanks,
Inelukii

Replies are listed 'Best First'.
Re: Style and Coding criticism requested
by FoxtrotUniform (Prior) on Feb 12, 2003 at 01:09 UTC

    I'm glad to see that you're using strict.pm.

    Your code is overcommented. Stuff like:

    my $median_class_avg; #-- median class average ... #-- Open the input file open(INPUT, '<', $input_file) or die "Couldn't open $input_file: $!\n" +; ... #-- Print student's average printf "%5.1f%2s", $students{$name}{avg_grade};

    is worse than redundant: it's distracting, and if the code changes, it's a good bet that the comment won't (which is pretty confusing to all concerned). Your code's pretty well written anyways, so you can probably just remove most of the comments without harming readability.

    This loop could be rewritten from

    foreach ( 0 .. $#{ $students{$name}{grades} } ) { printf "%4d", $students{$name}{grades}[$_]; }

    to

    foreach $grade (@{$students{$name}{grades}}) { printf "%4d", $grade; }

    which is not only clearer, but avoids indexing errors.

    Actually, that brings up a good point: I find it easier to pull out nested references explicitly, like so:

    my @grades = $students{$name}{grades}; foreach my $grade (@grades) { printf "%4d", $grade; }

    Those complaints aside, this is pretty well-written code.

    --
    F o x t r o t U n i f o r m
    Found a typo in this node? /msg me
    The hell with paco, vote for Erudil!

Re: Style and Coding criticism requested
by tall_man (Parson) on Feb 12, 2003 at 03:56 UTC
    The output should be sorted on lastname, followed by first name to break ties on lastnames.

    There's a subtle bug in the sorting trick you used, sorting on "$last,$first". The inserted comma can throw off the sort, as in the following:

    my @names = ("O'Toole,Peter", "O,Ari"); my @sorted = sort @names; print join(";",@names),"\n";
    This prints:
    O'Toole,Peter;O,Ari

    To get the correct sort order, you need something like this:

    my @names = (["O'Toole","Peter"], ["O", "Ari"]); sub namesort { $a->[0] cmp $b->[0] or $a->[1] cmp $b->[1] } sub namejoin { return join(",",@{$_[0]}); } my @sorted = sort namesort @names; print join(";",namejoin($sorted[0]),namejoin($sorted[1])),"\n";
    My only comment on style is that you might use more subroutines; for example the median calculation would make a nice re-usable utility function.
Re: Style and Coding criticism requested
by Enlil (Parson) on Feb 12, 2003 at 01:32 UTC
    Your specifications state:

    Firstname Lastname list_of_grades(any number of grades)

    Hence, you need to check to make sure a student has at least 1 grade before trying this division:

    $students{$name_index}{avg_grade} /= scalar(@{ $students{$name_index}{ +grades} });

    teachers are sneaky at times (or so experience would tell me). all else looks good though.

    -enlil

      My mistake. The assignment allowed us to assume that each student had at least one grade, otherwise I would have done exactly as you suggested.

      Thanks,
      Inelukii
Re: Style and Coding criticism requested
by submersible_toaster (Chaplain) on Feb 12, 2003 at 01:27 UTC

    1) ++inelukii for posting homework...after the fact, so to speak. As already mentioned, there are enough sharps in your code to make the eyes bleed, and for the most part are commenting code that is pretty obvious.

    foreach (0.. $#(somearray) ) can be a real drag sometimes, and almost always better written as foreach my $element (@array) unless you absolutly need the numeric index. Out of interest, how were you marked for this assignment?


    I can't believe it's not psellchecked
      Received 100 for the assignment and an extra 5 points for the shortest solution. ;)
      As far as all the comments go, I apologize, but you just have to know this professor. It annoys me more than anything the amount of commenting we have to do. Thanks for the kind words so far.

      Inelukii

        I had my suspicions that the comments were for the benefit of your prof. Interesting aside: A colleague turned in several assignments in uni as pseudo code, splattered with comments, after 'learning' the prof never ran the student assignments! May the karma catch him eventually

Re: Style and Coding criticism requested
by Hofmator (Curate) on Feb 12, 2003 at 10:08 UTC
    Overall nice code minus the comments - but there have been enough comments about the comments so I refrain from commenting on that :)

    I'd restructure the loop for reading in the input file a bit:

    use List::Util qw/sum/; while( <INPUT> ) { next if /^$/; my ($first, $last, @grades) = split; my $name_index = "$last,$first"; my $avg_grade = sum(@grades) / scalar(@grades); $students{$name_index}{grades} = \@grades; $students{$name_index}{avg_grade} = $avg_grade; push @averages, $avg_grade; }
    As you can see, I split the line directly into the needed parts. Furthermore the variable only needed in this loop are declared inside the loop - this locality gives less chances for mistakes.

    I don't know whether you are allowed to use modules to solve your exercise - but the sum subroutine from List::Util comes in handy here. Otherwise you could just program the sub yourself. I don't see any point in rounding the results you push into @averages. You are only using this internally and why not calculate with the whole precision you have available - thus simplifying the code. You could still do the rounding when you print something out.

    A final remark to a differnt part of the code, this: my $i = sprintf("%d", scalar(@averages) / 2 ); could be better written as my $i = int( scalar(@averages)/2 ); Both do rounding towards zero.

    -- Hofmator

Re: Style and Coding criticism requested
by steves (Curate) on Feb 12, 2003 at 01:38 UTC

    This is well written for a student IMO. I'd agree that it's overly micro-commented, but I also recall that I did that in college because most professors preferred it to under commenting. I had a friend whose style I'm much closer to today -- well named variables, minimal line level comments, block comments to describe major areas, and overall package docs. He got points taken off for fewer line level comments which really is not fair (and a poor reflection on the prof). But if you're going for the grade you're more likely to do better with more than less.