Another Example: Stats with a Menu

Let's modify the stats example once more in this lesson to take advantage of just about everything you've learned so far in this book. I've modified stats such that instead of reading the values, printing everything, and then exiting, the script prints a menu of operations. You have a choice of the sort of operations you want to perform on the numbers in the data set.

Unlike the names.pl script from Day 8, however, which used a large while loop and a number of if tests to handle the menu, this one uses subroutines. In addition, it moves a lot of the calculations we performed in earlier versions of this script into the subroutine that actually uses those calculations, so that only the work that needs to be done gets done.

Because this script is rather long, instead of showing you the whole thing, and then analyzing it, I'm going to walk through each important part and show you bits of the overall code to point out what I did when I wrote this version of the stats script. At the end of this section, in Listing 11.2, I'll show you all the code.

Let's start with the main body of code in this script. Previous versions of stats used a rather large set of global variables, which we defined and initialized at the start of the script. This version uses only two: the array of numbers for the input, and a variable to keep track of whether to quit the script or not. All the other variables are my variables, local to the various subroutines that need them.

The script starts with a call to a subroutine called &getinput(). This subroutine, which we'll look at in a bit, reads in the input from the input files and stores it in the @nums array. This version of &getinput() is significantly smaller than the one we've used in previous versions of stats—this one simply reads in the numbers. It doesn't keep track of frequencies, sums, or anything else, and it uses the $_ variable instead of a temporary input variable. It does, however, include the line that sorts the numbers in the final @nums array, and it discards any lines of input that contain characters other than numbers.

sub getinput {
    while (<>) {
        chomp;
        next if (/D/);
        push @nums, $_;
    }
    @nums = sort { $a <=> $b } @nums;
}

After reading the input, the core of this menu-driven version of stats is a while loop that prints the menu and executes different subroutines based on the part of the menu that was selected. That while loop looks like this:

&getinput();
while ($choice !~ /q/i) {
    $choice = &printmenu();
    SWITCH: {
        $choice =~ /^1/ && do { &printdata(); last SWITCH; };
        $choice =~ /^2/ && do { &countsum(); last SWITCH; };
        $choice =~ /^3/ && do { &maxmin(); last SWITCH; };
        $choice =~ /^4/ && do { &meanmed(); last SWITCH; };
        $choice =~ /^5/ && do { &printhist(); last SWITCH; };
    }
}

Look! A switch! This is one way to accomplish a switch-like statement in Perl, using pattern matching, do statements, and a label. For each value of $choice, 1 to 5, this statement will call the right subroutine and then call last to fall through the labeled block. Note that the labeled block isn't a loop, but you can still use last to exit out of it.

The value of $choice gets set via the &printmenu() subroutine (we'll look at that one in a bit). Note that the while loop here keeps printing the menu and repeating operations until the &printmenu() subroutine returns the value 'q' (or 'Q'), in which case the while loop stops, and the script exits.

The &printmenu() subroutine simply prints the menu of options, accepts input, verifies it, and then returns that value:

sub printmenu {
    my $in = "";
    print "Please choose one (or Q to quit): 
";
    print "1. Print data set
";
    print "2. Print count and sum of numbers
";
    print "3. Print maximum and minimum numbers
";
    print "4. Print mean and median numbers
";
    print "5. Print a histogram of the frequencies.
";
    while () {
        print "
Your choice --> ";
        chomp($in = <STDIN>);
        if ($in =~ /^d$/ || $in =~ /^q$/i) {
            return $in;
        } else {
            print "Not a choice.  1-5 or Q, please,
";
        }
    }
}

Let's work down the list of choices the menu gives us. The first choice is simply to print out the data set, which uses the &printdata() subroutine. &printdata() looks like this:

sub printdata {
    my $i = 1;
    print "Data Set: 
";
    foreach my $num (@nums) {
        print "$num ";
        if ($i == 10) {
            print "
";
            $i = 1;
        } else { $i++; }
    }
    print "

";
}

This subroutine simply iterates over the array of numbers and prints them. But there's one catch: it prints them ten per line for better formatting. That's what that $i variable does; it simply keeps track of how many numbers have been printed, and prints a new line after ten of them.

Note one other point—even though the $num variable is implicitly local to the foreach loop, you still have to declare it as my variable in order to get past use strict (which is at the beginning of this script even though I didn't show it to you).

The second menu choice prints the count and sum of the data set. The &countsum() subroutine looks like this:

sub countsum {
    print "Number of elements: ", scalar(@nums), "
";
    print "Sum of elements: ", &sumnums(), "

";
}

This subroutine, in turn, calls the &sumnums() to generate a sum of all the elements:

sub sumnums {
    my $sum = 0;
    foreach my $num  (@nums) {
        $sum += $num;
    }
    return $sum;
}

In previous versions of this stats script, we simply generated the sum as part of reading in the input from the file. In this example, we've postponed generating that sum until now. You could make the argument that this is less efficient—particularly since the average uses the sum as well—but it does allow us to put off some of the data processing until it's actually required.

Our third choice is the maximum and minimum value in the data set. We don't actually have to calculate these at all; because the data set is sorted, the minimum and maximum values are the first and last elements of the @nums array, respectively:

sub maxmin {
    print "Maximum number: $nums[0]
";
    print "Minimum number: $nums[$#nums]

";
}

Fourth choice is the mean and median values, which we can get as we did in previous stats scripts:

sub meanmed {
    printf("Average (mean): %.2f
", &sumnums() / scalar(@nums));
    print "Median: $nums[@nums / 2]

";
}

Which brings us to the last subroutine in this script, &printhist(), which calculates and prints the histogram of the values. As with previous versions, this part of the script is the most complex. This version, however, collects everything relating to the histogram into this one place, instead having bits of it spread all over the script. That means more local variables for this subroutine than for others, and more processing of the data that has to take place before we can print anything. But it also means that the data input isn't slowed down calculating values that won't be used until later, if at all, and there isn't a hash of the frequencies hanging around and taking up space as the script runs. Here's the &printhist() subroutine:

sub printhist {
    my %freq = ();
    my $maxfreq = 0;
    my @keys = ();
    my $space = 0;
    my $totalspace = 0;
    my $num;

    # build frequency hash, set maxfreq
    foreach $num (@nums) {
        $freq{$num} ++;
        if ($maxfreq < $freq{$num} ) { $maxfreq = $freq{$num} }
    }

    # print hash
    @keys = sort { $a <=> $b } keys %freq;
    for (my $i = $maxfreq; $i > 0; $i--) {
        foreach $num (@keys) {
            $space = (length $num);
            if ($freq{$num} >= $i) {
                print( (" " x $space) . "*");
            } else {
                print " " x (($space) + 1);
            }
            if ($i == $maxfreq) { $totalspace += $space + 1; }
        }
        print "
";
    }
    print "-" x $totalspace;
    print "
 @keys

";
}

A careful look will show that beyond collecting all the frequency processing into this one subroutine, little else has changed. Putting it into a subroutine simply makes the process and its data more self-contained.

Listing 11.2 shows the full script (all the individual parts put together):

Listing 11.2. The statsmenu.pl Script
#!/usr/ bin/perl -w
use strict;

my @nums = (); # array of numbers;
my $choice = "";

# main script
&getinput();
while ($choice !~ /q/i) {
    $choice = &printmenu();
  SWITCH: {
      $choice =~ /^1/ && do { &printdata(); last SWITCH; };
      $choice =~ /^2/ && do { &countsum(); last SWITCH; };
      $choice =~ /^3/ && do { &maxmin(); last SWITCH; };
      $choice =~ /^4/ && do { &meanmed(); last SWITCH; };
      $choice =~ /^5/ && do { &printhist(); last SWITCH; };
  }
}

# read in the input from the files, sort it once its done
sub getinput {
    while (<>) {
        chomp;
        next if (/D/);
        push @nums, $_;
    }
    @nums = sort { $a <=> $b } @nums;
}
# our happy menu to be repeated until Q
sub printmenu {
    my $in = "";
    print "Please choose one (or Q to quit): 
";
    print "1. Print data set
";
    print "2. Print count and sum of numbers
";
    print "3. Print maximum and minimum numbers
";
    print "4. Print mean and median numbers
";
    print "5. Print a histogram of the frequencies.
";
    while () {
        print "
Your choice --> ";
        chomp($in = <STDIN>);
        if ($in =~ /^d$/ || $in =~ /^q$/i) {
            return $in;
        } else {
            print "Not a choice.  1-5 or Q, please,
";
        }
    }
}
# print out the data set, ten numbers per line
sub printdata {
    my $i = 1;
    print "Data Set: 
";
    foreach my $num (@nums) {
        print "$num ";
        if ($i == 10) {
            print "
";
            $i = 1;
        } else { $i++; }
    }
    print "

";
}

# print the number of elements and the sum
sub countsum {
    print "Number of elements: ", scalar(@nums), "
";
    print "Sum of elements: ", &sumnums(), "

";
}

# find the sum
sub sumnums {
    my $sum = 0;
    foreach my $num  (@nums) {
        $sum += $num;
    }
    return $sum;
}

# print the max and minimum values
sub maxmin {
    print "Maximum number: $nums[0]
";
    print "Minimum number: $nums[$#nums]

";
}

# print the mean and median
sub meanmed {
    printf("Average (mean): %.2f
", &sumnums() / scalar(@nums));
    print "Median: $nums[@nums / 2]

";
}

# print the histogram.  Build hash of frequencies & prints.
sub printhist {
    my %freq = ();
    my $maxfreq = 0;
    my @keys = ();
    my $space = 0;
    my $totalspace = 0;
    my $num;
    # build frequency hash, set maxfreq
    foreach $num (@nums) {
        $freq{$num} ++;
        if ($maxfreq < $freq{$num} ) { $maxfreq = $freq{$num} }
    }

    # print hash
    @keys = sort { $a <=> $b } keys %freq;
    for (my $i = $maxfreq; $i > 0; $i--) {
        foreach $num (@keys) {
            $space = (length $num);
            if ($freq{$num} >= $i) {
                print( (" " x $space) . "*");
            } else {
                print " " x (($space) + 1);
            }
            if ($i == $maxfreq) { $totalspace += $space + 1; }
        }
        print "
";
    }
    print "-" x $totalspace;
    print "
 @keys

";
}
					

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset