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

I have a script that must grade an exam in which there are several correct (and half correct) answers to a question. On this exam, one must match the spectral classification of a star with an image of a star (going by a comparison key). Spectral Classifications are classified by a letter-number combination, like "B1", "K8", and "G5" -- with the numbers ranging 0-9. The possible letters are: O, B, A, F, G, K and M (in order of ascension).

Now on to the code... Here is what I have determined will work to exam and grade each question, but it seems to me that there would be a better way.

sub gradeTest { my $answer, $runningTotal; $runningTotal = 0; $answer = lc(param(ans1)); # For answer 1, G6-G9 is full credit. # G0-G5 and K0-K5 are half credit if ($answer eq "g6" || $answer eq "g7" || $answer eq "g8" || $ans +wer eq "g9") { $runningTotal += 1; } elsif ($answer eq "g0" || $answer eq "g1" || $answer eq "g2" || + $answer eq "g3" || $answer eq "g4" || $answer eq "g5") { $runningTotal += .5; } elsif ($answer eq "k0" || $answer eq "k1" || $answer eq "k2" || + $answer eq "k3" || $answer eq "k4" || $answer eq "k5") { $runningTOtal += .5; } }

There are 12 questions on the test, so you can see that going through this 12 times is not optimal. Also, keep in mind that some answer ranges also span letters as well, like O9-B1, which, at least to me, seems to make condensing this more difficult.

Thanks for any suggestions.

Replies are listed 'Best First'.
Re: Efficient Rating of Test Answers
by enoch (Chaplain) on Aug 13, 2001 at 23:23 UTC
    Regex:
    sub gradeTest { my $answer, $runningTotal; $runningTotal = 0; $answer = lc(param(ans1)); # For answer 1, G6-G9 is full credit. # G0-G5 and K0-K5 are half credit $runningTotal += 1, return if $answer =~ /^g[6-9]$/; $runningTotal += .5, return if $answer =~ /^g[0-5]$/; $runningTotal += .5, return if $answer =~ /^k[0-5]$/; }
    Jeremy

      Why not put your last two cases together?

      $runningTotal += .5, return if $answer =~ /^[gk][0-5]$/; # instead of $runningTotal += .5, return if $answer =~ /^g[0-5]$/; $runningTotal += .5, return if $answer =~ /^k[0-5]$/;

      -- Hofmator

(jeffa) Re: Efficient Rating of Test Answers
by jeffa (Bishop) on Aug 13, 2001 at 23:24 UTC
    Ughh! No matter how you slice it, that's an icky problem. I try to avoid those nasty if-elsif-else statements with a hash table. japhy calls it a 'Dispatch Table', which sounds like a great name to me. The idea is that the keys in the hash point to subroutines:
    my %dispatch = ( g6 => \&add_one, g7 => \&add_one, g8 => \&add_one, g9 => \&add_one, g0 => \&add_half, g1 => \&add_half, g2 => \&add_half, g3 => \&add_half, g4 => \&add_half, g5 => \&add_half, k0 => \&add_half, k1 => \&add_half, k2 => \&add_half, k3 => \&add_half, k4 => \&add_half, k5 => \&add_half, ); sub add_one { my $total = shift; $total += 1; return $total; } sub add_half { my $total = shift; $total += 1; return $total; }
    And you access the table like so:
    my $total = 10; $total = $dispatch{'g6'}->($total); $total = $dispatch{'k5'}->($total); print $total, "\n";
    This is not a complete solution for you, but it is a start.

    ----------------------------------------------------
     perl -le '$x="jeff";$x++ for(0..4482550);print $x'
    ----------------------------------------------------
    

      You are describing the general concept of a 'Dispatch Table'. While very useful at times I think that's a little overkill here. Your subroutines do exactly the same thing - add a number - and only this number differs. So why not just store the number as proposed by VSarkiss. Your code would then simplify to

      my %lookup = ( # no longer a dispatch g6 => 1, g7 => 1, g8 => 1, g9 => 1, g0 => 0.5, # ... ); my $total = 10; $total += exists($lookup{'g6'})? $lookup{'g6'} : 0; print $total, "\n";
      I think I still prefer the regex solution by enoch in this easy case where the keys/answers are so 'handy' to match.

      -- Hofmator

        Very nice!

        How about generating the table with Perl instead of a text editor? I'll just borrow the regexes you and enoch provided, if you don't mind :D

        my %lookup; foreach my $alpha ('a'..'k') { foreach my $numb (0..9) { $_ = $alpha . $numb; $lookup{$_} = /^g[6-9]$/ ? 1 : /^[gk][0-5]$/ ? .5 : 0; } }
        however, this is not very efficient if your memory is tied behind your back. ;)

        ----------------------------------------------------
         perl -le '$x="jeff";$x++ for(0..4482550);print $x'
        ----------------------------------------------------
        
Re: Efficient Rating of Test Answers
by John M. Dlugosz (Monsignor) on Aug 13, 2001 at 23:26 UTC
    “The possible letters are: O, B, A, F, G, K and M (in order of ascension).” What happened to R and N?

    The easiest code I can think of is to map the designation to a scalar, and then use ordinary numeric ranges to compare.

    $answer =~ tr/OBAFGKM/1234567/; if ($answer >= 56 && $answer <= 59) ...
    Spanning letters is not an issue.

    Now, you have a lot of the same code but just changing the ranges. Make it into a table, like so:

    @results= ( undef, #index 0 unused [ #describe question 1 qw/ g0 k5 g6 g9/ ], # another row for each question.
    This lists the half-right range, then the full-right range. The code can extract these 4 values, convert them to numbers, test for full-right first (being a subset), then the larger half-right range, all with one block of code.

    —John

Re: Efficient Rating of Test Answers
by VSarkiss (Monsignor) on Aug 13, 2001 at 23:37 UTC

    I'm in the "make a hash table" camp. You're going to need to sit down and type the right answers per question, which are relatively arbitrary, so make data entry easy:

    my $credit = [ { (map {$_ => 1.0} qw(g6 g7 g8 g9)), (map {$_ => 0.5} qw(g0 g1 g2 g3 g4 g5 k0 k1 k2 k3 k4 k5)), }, ];
    So this is a reference to an array of hashes. The index to the array is the question number (alright, question number minus one), and the hash gives you the credit value of each answer. With exists, you can tell if the answer is wrong. Something like this:
    if (exists $credit->[$qnum]{$answer}) { print "Q $qnum: ", $credit->[$qnum]{$answer}, " points\n"; } else { print "wrong\n"; }
    If you don't care about warnings about undefined values, you don't have to have the "exists" test.

    HTH

Re: Efficient Rating of Test Answers
by runrig (Abbot) on Aug 14, 2001 at 01:18 UTC
    Here's a solution that lets you use your ranges. Note: I tried to use List::Util::first instead of grep, but there appears to be a bug with the 'first()' function.
    use strict; sub rank { local $_ = shift; tr/OBAFGKM/1-7/; $_; } # Enter questions and scores and answers # Enter answers as a range of values. my %scores = ( 1 => { 1 => { O9=>'B1', K1=>'K2', }, 0.5 => { B4=>'B5', B7=>'B7', }, }, 2 => { 1 => { K8=>'K9', }, 0.5 => { F1=>'F2', }, }, ); my %ranges; for my $question (keys %scores) { my $scores = $scores{$question}; my @ranges; for my $score (keys %$scores) { my $ranges = $scores->{$score}; push @ranges, map { [ $score, rank($_), rank($ranges->{$_}) ] } keys %$ranges; } @ranges = sort {$a->[1] cmp $b->[1]} @ranges; $ranges{$question} = \@ranges; } my %answers = (1 => 'K2', 2=>'F1'); my $total = 0; while (my ($qu, $ans) = each %answers) { $ans = rank($ans); my ($aref) = grep { $_->[1] le $ans and $ans le $_->[2] } @{$ranges{$qu}}; $total += $aref->[0] if defined $aref; } print $total,"\n";
THANK YOU!
by rvf (Novice) on Aug 14, 2001 at 22:46 UTC
    Great suggestions guys. This has got to be one of the best development communities around.