Sunday, September 27, 2009

Statistic Calculator: User friendly cosole

[Original Spanish source]
One of the nice features for console applications is the ability to edit the command line and reusing previous commands, when these two characteristics meet the application is much friendlier. So let's add these features to the calculator of the last article.
This is another job where CPAN shows why it is Perl' s best feature, I'll use Term::ReadLine, a unified interface for console reading, this library allows the use of several backends that implement it's functionality, and for our example to work I installed Term::ReadLine::Perl, but I suppose that Term::ReadLine::Gnu would work just as well. Both are interfaces of the GNU readline(3) library used by many applications, including bash.
To add a bash like interface to the calculator, we just need to change lines 27 and 28 by yhis:
28     my $command = $term->readline("Listo> ") // last;
you must declare the use of the module, and initialize the terminal creating the $term object that will allow us to invoke the readline method:
 7 use Term::ReadLine;
 9 my $term = new Term::ReadLine 'Statistic Calculator';
I used Term::ReadLine and not any of the specific variants, because this one is responsible for selecting and loading a variant automatically, but also allows the user to have the control if necessary. The library has functions that allow you to decorate the prompt, autocompleting and some other goodies, CPAN again saves the day by just reading a module's documentation.
I will fix some other annoying things that go through (read: bugs), first the most annoying but the easier to solve: an empty command gives an error message because an empty string is not matched by any when clause, to solve it, I  just added:
when ("")  {  } # si el comando es vacĂ­o no hacer nada
and it's ready.
A harder problem are commands that return undef, such as "clear", and produces a warning thanks to the implicit use warnings by Modern::Perl:
Listo> clear
Use of uninitialized value in concatenation (.) or string at line 32.
clear =

To solve this problem I'm going to make a subroutine to apply functions and print results, so the dispatch cycle remains as clear a possible, so this:
32   when (%FUNCS)  { say "$command = " . $s->$command }
will become this:
32   when (%FUNCS)  { apply $command }
then we add the "apply" subroutine, that receives a command, executes it, and gets the result, but it returns without saying anything unless the result is defined and not an empty string (notice the remarkable similarity between last sentence the Perl one):
26     return unless defined $result and $result ne "";
The calculator is now more user friendly, but still has some problems, if you try to execute "trimmed_mean", you'll notice lots of warnings, the manual ("man") describes the cause, "trimmed_mean" function receives parameters, but our program doesn't know how to handle this, so in next article I will fix this, and also make it display complex return values, such arrays and hashes.
Now our full program looks like this:
 1 #!/usr/bin/perl
 3 use Modern::Perl;
 4 use Scalar::Util qw( looks_like_number );
 5 use Statistics::Descriptive;
 6 use Pod::Perldoc;
 7 use Term::ReadLine;
 9 my $term = new Term::ReadLine 'Statistic Calculator';
11 my %FUNCS = map { $_ => 1 } qw( sum mean count variance standard_deviation
12     min mindex max maxdex sample_range median harmonic_mean geometric_mean
13     mode trimmed_mean clear );
15 my @COMMANDS = qw( exit quit help man );
17 sub help { say "Comandos: " . join( ", ", sort @COMMANDS, keys %FUNCS ) }
19 sub man { Pod::Perldoc->new(args => \@_)->process }
21 my $s = Statistics::Descriptive::Full->new();
23 sub apply {
24     my $command = shift;
25     my $result = $s->$command;
26     return unless defined $result and $result ne "";
27     say "$command = $result";
28 }
30 while (defined(my $command = $term->readline("Listo> "))) {
31     $command =~ s/^\s+//; $command =~ s/\s+$//;
32     given ($command) {
33         when ( looks_like_number($_) ) { $s->add_data($command) }
34         when (%FUNCS)                  { apply $command }
35         when ("man")                   { man "Statistics::Descriptive" }
36         when ( [ "exit", "quit" ] )    { last }
37         when ("help")                  { help }
38         when ("")                      { }
39         default                        { say "Error: tipee 'help' para ayuda" }
40     }
41 }

No comments:

Post a Comment