#!perl -w require 5.004; =head1 NAME FP.pl - Will Mengarini - Quick function point calculation =head1 LAYOUT I prefer something similar to Knuth's "literate programming", in which documentation is intermingled with code. So what this module does is explained in a help message rather than POD documentation, & the function giving that message is the first code you'll see. That might make things difficult for a POD-translator to handle, but I think it's worth it to make reading the raw source code easier. Primarily I just use POD as a multiline comment delimiter. =cut sub usage { print < That's all so far. Example: Perl -S fp.pl HelloWorld.cpp ibm84 calculates & stores 1984 IBM function points for the program HelloWorld.cpp. EOT } =head1 DESIGN RATIONALE Storing the database out of band is best, so it can be used on other people's code in industrial contexts. This is even a good idea for nonindustrial code, since if it becomes freeware I don't want it crufted up. =cut # Selftest capabilities need to come before setting up strict & diagnostics. # The only selftesting in this script is of minor subs like validators (see # my ask.pl) & encryptors (see my ttyForm.pl). require 'selftest.pl'; require 'ask.pl'; require 'ttyForm.pl'; &maybeSelfTest; # If this script uses strict & diagnostics, it takes > 10 seconds to load # on a 486/50 running Windows 95; without them, it takes 1..2 seconds. # Here is a general approach to being able to toggle that from outside # the script without editing it. my $debug; #set in next BEGIN block BEGIN { #decide whether to set $debug & use {strict,diagnostics} if( defined $ENV{'PERLHACK'} ){ $_ = $0; s|.*[/\\]||; s|\.pl$||i; #Extract script name from file path if( $ENV{'PERLHACK'} =~ /\b$_\b/i ){ #If it's a word in $PERLHACK print "Running $_ with strict everything & diagnostics.\n"; $debug = 1; # Can't use "use" here because they'd generate a separate BEGIN block: require 'strict.pm'; import strict; require 'diagnostics.pm'; import diagnostics; }else{ $debug = 0; } undef $_; } } # Subs from packages ask & ttyForm: sub askYN; sub updateLoop; # The main code just applies the selected metric to the selected code unit. # There need to be 2 args, the code unit & the metric. The code unit is # first since it seems more likely that I'll want to run multiple metrics on # a single code unit than one metric on multiple units, at least while I'm # still hacking the metrics to find one I like. If I married one metric I'd # define a DOSKey alias that just ran FP with that metric. defined $ARGV[1] or usage and exit 1; my( $codeUnit, $metric ) = @ARGV[0..1]; # If we're using a database, we need to find the most recent entry in it that # pertains to this code unit & metric, & extract it. Then, whether or not # we're using a database, we need to create a new entry in database format, # because a screen display of that is an output of this script (& if there's # no database, it's the only output). sub findDatabase; sub getMostRecentEntry; sub makeNewEntry; my $database = $codeUnit eq '0' ? '' : findDatabase; # The path to the database if we're using one, but in effect also # a boolean indicating whether we're using a database. my $mostRecentEntry = getMostRecentEntry $codeUnit, $metric, $database; my $newEntry = makeNewEntry $codeUnit, $metric; # Next we just pick the relevant metric & calculate it. if( $metric eq 'ibm84' ){ sub ibm84; $newEntry .= ibm84 $mostRecentEntry; }else{ die "No \"$metric\" metric is defined.\n"; } # Finally, we display the calculated entry to the user, & ask whether # to append it to the database. print $newEntry,$/; sub putNewEntry; if( $database && askYN "Write this entry to database? " ){ putNewEntry $newEntry, $codeUnit, $metric, $database; } # The database must be located in $HOME, so that environment variable # must be defined for the database to be found. However, if the database # is known not to be wanted for this run (because the code unit is '0'), # the main code shouldn't call findDatabase, so $HOME won't be needed. sub findDatabase { my $HOME = $ENV{'HOME'} or die "Environment variable HOME not found.\n"; $HOME =~ s|([^/\\])$|$1/|; #"/" works on both Unix & Windows my $database = "${HOME}fp.dat"; if( -e $database && ! -f _ ){ die "\"$database\" exists & is not a normal file.\n"; }elsif( ! -f _ ){ if( askYN "$database doesn't exist; create? " ){ open DB, ">$database" or die "Unable to create $database.\n"; close DB; }else{ print "Aborting.\n"; exit 1; } } $database } # The trick to getMostRecentEntry is to pick off the *final* entry in # the database that matches the relevant criteria. It seems easiest to # read from the beginning, remembering the most recently found match. sub getMostRecentEntry { my( $codeUnit, $metric, $database ) = @_; die unless defined $database; return '' unless $database; open DB, $database or die "Can't open $database: $!"; my( $entry, $scratch ) = ( '', '' ); my $weAreReadingACandidateEntry = 0; while( ){ if( /^====+$/ ){ # This is a start of a new entry if( $weAreReadingACandidateEntry ){ # We were already reading a candidate entry, which now is finished $entry = $scratch; #overwriting any previous value in $entry $weAreReadingACandidateEntry = 0; #because the old entry is finished } $scratch = $_; }elsif( /^Code unit $codeUnit, metric $metric$/ ){ $weAreReadingACandidateEntry = 1; $scratch .= $_; }elsif( $weAreReadingACandidateEntry ){ $scratch .= $_; }else{ $scratch = ''; } } $entry = $scratch if $weAreReadingACandidateEntry; close DB; $entry } # The "database" is just a flat text file containing entries with headers # that identify the code unit, the metric that was applied to it, & when. # This allows using an editor to annotate the calculations; that capability # is important, since the metrics include subjective judgments. sub timestamp; sub makeNewEntry { my( $codeUnit, $metric ) = @_; die unless defined $metric; $codeUnit = "[anonymous]" unless $codeUnit; #so it replaces '0' "\n===============\nCode unit $codeUnit, metric $metric\n---------------\n" . "Calculated on " . timestamp } # It'd be possible to go thru the database for any previous entry pertaining # to this code unit & metric, & replace it with the new entry; but I'd rather # do that pruning in an editor, since I may want to keep multiple records # applying a particular metric to a particular code unit, with annotations # describing differences in subjective judgments. sub putNewEntry { my( $newEntry, $codeUnit, $metric, $database ) = @_; die unless defined $database; # In this incarnation I just catenate $newEntry onto the end of the # "database", so I don't need to know about $codeUnit or $metric. That # could change someday, so this calling protocol should be maintained. open DB, ">>$database" or die "Can't open $database: $!"; print DB $newEntry; close DB; } sub timestamp { my @dayName = qw(Sa Su Mo Tu We Th Fr); my @monthName = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); my ($sec,$min,$hour,$mday,$mon,$year,$wday) = localtime(time); sprintf "%02d:%02d:%02d %s %02d %s %02d\n", $hour, $min, $sec, $dayName[$wday], $mday, $monthName[$mon], $year; } # getValuesOf $whatToGet will extract from $entry the last line that has a # /^$whatToGet: / prefix, with that prefix removed; or '' if there is no such # line. The reason for going to the trouble of extracting the last item is # that I'm allowing for the possibility that I'll someday use a database # format in which multiple calculations of a metric go in one $entry. # To grok the code, remember that s/// returns true if it changes anything. sub getValuesOf { my( $whatToGet, $entry ) = @_; die unless defined $entry; my @matches = grep s/^$whatToGet: //, split /\n/, $entry; @matches ? $matches[$#matches] : '' } # ibm84 metric: described in Jones /Applied Software Measurement/ p64. # Here I implement it with 2 separate subs, to allow for the possibility # that I'll develop other metrics using one of those subs. sub ibm84Basics; sub ibm84Complexity; sub ibm84 { my( $mostRecentEntry ) = @_; my $defaults; #Scratch for 2 calculations my $result = ''; #User input & calculations from it, all in database format $defaults = getValuesOf 'ibm84Basics', $mostRecentEntry; my( $unadjustedFunctionPoints, $basicsString ) = ibm84Basics $defaults; $result .= $/ . 'ibm84Basics: ' . $basicsString; $defaults = getValuesOf 'ibm84Complexity', $mostRecentEntry; my( $complexityFactor, $complexityString ) = ibm84Complexity $defaults; $result .= $/ . 'ibm84Complexity: ' . $complexityString; my $functionPoints = $unadjustedFunctionPoints * $complexityFactor; $result .= $/ . "IBM 1984 Function Points: " . $functionPoints . $/; } sub isWhole; #validator sub as described in my ask.pl package sub sum; # ibm84Basics uses updateLoop from my ttyForm.pl package to present the # user with a purely text-mode form to fill out. sub ibm84Basics { my $defaults = shift || join '|', ('0') x 15; print "\nIBM 84 Function Point Basic Parameters---\n\n"; print "All values must be nonnegative integers.\n"; my $newValueString = updateLoop '|',$defaults,split( "\n\n", <