Solutions and Discussion for Perl Quiz of the Week #6 (Expert Edition)

Mark Jason Dominus <[email protected]> Tue, 26 Nov 2002 22:15:41 -0500
Newsgroups gmane.comp.lang.perl.qotw.quiz-of-the-week
Organization Plover Systems
Message-ID <[email protected]>
Sample solutions and discussion
Perl Expert Quiz of The Week #6 (20021120)

        Write a generic survey or quiz program, "survey".

        The program will read a configuration file in this format:

                How old are you?  0-12; 13-19; 20-24; 25-34; 35-44; 45 or older
                What is your favorite color?  red; blue; black; other
                ...

        Each question is on a separate line; the question is
        everything up to and including the first '?', and the allowed
        responses to the question are the following items separated by
        ';'.  If the last response ends with ";;" instead of ";", any
        data following the ";;" is taken to be configuration options
        for that question, separated by spaces.  For example:

            How old are you?  0-12; 13-19; 20-24; 25-34; 35-44; 45 or older;; OPTIONAL
            What is your favorite color?  red; blue; black;; ALLOW_OTHER

        It is up to you to decide what options, if any, should be
        supported.

        The program will then prompt the user with the questions, and
        read and validte the responses.  When the user has finished
        answering the questions, the program should record the
        responses in a file.

        There should be some way to run the program to get it to
        disgorge a report about the aggregate survey responses.  The
        report should include: The number of people completing the
        survey; the number providing each response to each question,
        and the percentage breakdown for the responses to each
        question.

        The program could use the terminal as an interface, or it
        could have a Tk interface, or could be a CGI program, or
        whatever you prefer.


----------------------------------------------------------------

Not much to say this week.  There was no discussion of the problem on
the -discuss list, and only three people sent sample solutions.  All
of these seemed straightforward, so I'll show Alan Cameron's, for two
reasons: First, it's relatively short, and second, it isn't cluttered
up with a lot of HTML, because it uses a plain terminal interface.

================================================================
#!/usr/bin/perl

# For Quiz of the Week #6, Expert
# Author: Alan Cameron
#
# run as './survey' to run the survey
# run as './survey report' to get a report on the answers
# $Qfile and $Afile can be changed to whatever you want.

use strict;

# For saving of the answer file
use Storable;

# Filenames for the question and answer file
my $Qfile = "quiz-questions";
my $Afile = "cameron-data";

# Array of questions in the following format
# @questions = (
#	[
#	 "Question 1",
#	 "OPTIONS",
#	 ["answer1","answer2",...]
#	],
#	[
#	 "Question 2",
#	 "OPTIONS",
#	 ["answer1","answer2",...]
#    ...
#	]
# )
my @questions;

# Array of answers to each question.
# Elements are the number of the answer chosen
my @answers;

# Read in the list of questions from a given file
# stores questions in global array @questions
# skips blank lines, and lines beginning with #
# also removes any white space before and after the list of answers
sub read_qfile {
	my $file = shift;
	my $fh;
	open $fh, "$file" or die "Couldn't open $file: $!";

	while (<$fh>) {
		next if /^\s*$/;
		next if /^#/;
		chomp;
		my ($ques, $ans) = /^(.*?\?)\s*(.*)$/;
		my $opt;
		($ans, $opt) = split /;;/, $ans;
		my @ans = split /;/, $ans;
		s/^\s*//, s/\s*$// for @ans;
		push @questions, [$ques, $opt, \@ans];
	}
}

# Adds the results of @answers to the Answer file
sub write_afile {
	my $file = shift;
	my @ans;
	# Either read in the current answer file, or create a new
	# structure to hold the answers.
	if (-e $file) {
		@ans = @{ +retrieve($file) };
	} else {
		foreach (@questions) {
			push @ans, [ (0) x scalar @{$_->[2]} ];
		}
		unshift @ans, 0;
	}
	my $num = shift @ans;
	for (my $i = 0;$i < @answers; $i++) {
		$ans[$i]->[$answers[$i]]++
	}
	unshift @ans, ++$num;
	store \@ans, $file;
}

# Report on the number of answers for each question
# Output is formatted for a max of 3 digits in the number
# of answers per question, if you have more than 999 answers
# to a particular question, the output won't look as nice
sub report_ans {
	my $file = shift;
	my @ans;
	my $total;
	if (-e $file) {
		@ans = @{ +retrieve($file) };
	} else {
		die "No Current Answer file to report.\n";
	}
	my $num = shift @ans;
	print "Number of questionees: $num\n";
	# For each question, iterate through the list of possible
	# answers, and print the number of each.
	for (my $i = 0;$i < @questions; $i++) {
		print $questions[$i][0];
		print " (", $questions[$i][1], ")" if $questions[$i][1];
		print "\n";
		$total += $_ for @{$ans[$i]};
		$total = 1 unless $total;
		for (my $j = 0; $j < @{$questions[$i][2]}; $j++) {
			$ans[$i][$j] = 0 unless defined $ans[$i][$j];
			printf "%3d (%5.1f%%)  %s\n",
			  $ans[$i][$j], ($ans[$i][$j] / $total)*100, $questions[$i][2][$j];
		}
		print "\n";
		$total = 0;
	}
}

# returns the number of the chosen answer
# passed in 2 args. a string containing the options,
# and an arrayref to a list of answers
sub get_answer {
	my $opt = shift;
	my @ans = @{ +shift };
	my $in;
	my $i;

	for ($i = 0; $i < @ans; $i++) {
		print $i+1, " $ans[$i]\n";
	}

	while(1) {
		print "Answer (1-$i): ";
		{ local $^W=0;
		$in = int <STDIN>;
		}
		if ($in < 1 || $in > $i) {
			print "Invalid answer, try again\n";
			redo;
		}
		last;
	}
	print "\n";
	return $in - 1;

}

# Get the list of questions
read_qfile $Qfile;

# If all you want is a report, print that and exit
if ($ARGV[0] =~ /report/i) {
	report_ans $Afile;
	exit 0;
}

# Ask each question, and record the response
foreach my $ques (@questions) {
	my ($q, $opt, $ans) = @$ques;
	print "$q\n";
	push @answers, get_answer($opt, $ans);
}

# Write out the answers to the answer file
write_afile $Afile;

================================================================

Mr. Cameron's program used the 'Storable' module to record the answer
data in a file.  Two other solutions used plain text, serializing the
data manually; my example program used DB_File.

1. When I posed this question, I hoped someone would build a generic
   survey kit that had a pluggable interface.  Nobody did do this,
   although Alan Cameron's came close: His program has a fairly clear
   separation between the interface parts (report_ans, get_answer) and
   the data parts (read_qfile, write_afile).

2. John Toomey's and James Gray's programs support the useful options
   OPTIONAL and ALLOW_OTHER; Mr. Gray's also supports ALL_THAT_APPLY.
   Alan Cameron and I took the easy way out and had the programs
   ignore the input.

3. Everyone's program suffers from potential race conditions, except
   mine (I think.)  Oops!

   I've found in the past that in addition to recording aggregate
   data, it's a good idea to also record every detail of every survey
   that is submitted, including the date and source IP address,
   because you never know what information you might want to extract
   later.  ("How many 18-25-year-olds said their favorite color was
   blue?")  Also in case you mess up the file locking and the
   aggregate date becomes corrupt.  A plain text file that gets a
   survey response appended to it is unlikely to become corrupt.

4. My program and Cameron's were much smaller than the others.  I
   thought, "Oh, we must have an unfair advantage, because we left out
   the optional options."  So I added OPTIONAL and ALLOW_OTHER to my
   program, and it grew from 80 to 99 lines.  Then I realized I hadn't
   deleted the prose introductions from gray.pl and toomey.pl.  (My
   line-counting progrram ignores comments, but not email headers or
   prose.)  It's been that kind of a day.  Here are the totals:

                                OPTIONAL   ALLOW_OTHER   ALL_THAT_APPLY

        cameron.pl       80    
        gray.pl         116     Yes        Yes           Yes
        mjd.pl           80     
        mjd2.pl          99     Yes        Yes
        toomey.pl       115     Yes        Yes


   So where's the code difference?  toomey.pl seems to have a lot of
   repeated code.  gray.pl has some additional code to handle
   ALL_THAT_APPLY, but also uses a lot of space generating HTML
   output.  mjd2.pl uses the CGI.pm built-in functions, which appears
   to be more compact, at least in this case.

5. All five programs are at http://perl.plover.com/qotw/misc/e006/ .

I hope the next quiz will arouse more interest.  Thanks to everyone
who participated.  New quizzes tomorrow.