For the moment, I have taken over maintenance of
cow of doom's node tracker.
This can also be found at
http://home.cinci.rr.com/dancers/code/e2info.pl
/msg avjewe or email ajewell@cinci.rr.com with comments, questions, fixes and such.
#!/usr/local/bin/perl -w
# e2info.pl - gathers user info from everything2.com.
# Copyright (C) 2000,2001 Will Woods (a.k.a. Cow Of Doom)
# Copyright (C) 2002 Andy JJewell (ajewell@cinci.rr.com)
# Distributed under the terms of the GNU General Public License,
# included here by reference.
#
# send comments, questions, and stories to the address above.
# history:
# v1.0.22 27 january 2003. misc initialization fixes for the first run.
# v1.0.21 3 December 2002. Slight output change to show +/- in total as well as delta
# v1.0.20 23 November 2002. Bug fixes to yesterdays changes. Got some signs wrong in the output.
# v1.0.19 22 November 2002. Better formatting. Show new votes when rep uchanged.
# v1.0.18 14 October 2002. Updated documentation, added --help
# v1.0.17 14 October 2002. General tidying of output. Added "ext" option.
# v1.0.16 27 September 2002. Subtract Nuke Requests and such. Add vote stats.
# v1.0.15 18 September 2002. Appends summary, with date to .e2history
# v1.0.14 8 September 2002. Now tracks merit, devotion and other stats.
# v1.0.13 28 August 2002. Now tracks number of messages.
# v1.0.12 31 July 2002. Changes in formats of a couple of different things.
# v1.0.11 23 May 2002. Yet another slight change in the login format.
# v1.0.10 19 March 2002. Another slight change in the login format.
# v1.0.9 18 Oct 2001. Integrated dotc's fix for fractional votes.
# v1.0.8 12 Sept 2001. Fixed new bug parsing saved data file.
# v1.0.7: 12 Sept 2001. show (+x/-y) with reputation.
# change "update" to behave like "both".
# v1.0.6: 22 Aug 2001. Show number of C!s as well as change.
# v1.0.5: 21 Aug 2001. Generalize type handling.
# Types outside of the basic four no longer crash.
# Added "use strict" and fixed as necessry.
# v1.0.4: 7 Aug 2001. Taken over by avjewe.
# Updated for various format changes, multiple cools
# v1.0.3: The format of the Login screen changed, so no one
# could log in. Fixed.
# v1.0.2: I was miscalculating Writeup Node-fu (WNF). fixed.
# v1.0.1: Fixed problem that would happen if file was saved with blanks
# at the beginning of each line (e.g. copy-and-paste from Netscape)
# Fixed bug where new nodes would not be displayed in Rep/Cool list
# v1.0.0: (initial release)
use strict;
$0="$0"; # Perl magic to clean the commandline from the process list
my $version = '1.0.21';
use LWP::UserAgent; # these are both part of libwww-perl, available
use HTTP::Cookies; # at your friendly local CPAN mirror
my $baseurl="http://www.everything2.com/index.pl";
my $datafile="$ENV{HOME}/.e2info"; # save data here
my $historyfile="$ENV{HOME}/.e2history"; # save here
my $meritHist="$ENV{HOME}/.meritHist"; # save here
my $totalHist="$ENV{HOME}/.totalHist"; # save here
my $badLogin="$ENV{HOME}/.badLogin"; # save here
my $ext = 0;
$|=1;
my $ua = LWP::UserAgent->new();
$ua->env_proxy();
my $cookies = HTTP::Cookies->new();
$ua->cookie_jar($cookies);
$ARGV[0] = "" if (@ARGV == 0);
if ($ARGV[0] eq '--help') {
HELP();
exit(0);
}
if ((@ARGV < 2) || (@ARGV > 3)) {
USAGE();
exit(1);
}
sub HELP {
USAGE();
print "
Provides these statistics :
Nodes : Number of write-ups by you
XP : Your current XP total
Cools : Sum of the cools of all your write-ups
Max Rep : The Maximum reputaion of any of your write-ups
Min Rep : The Minimum reputaion of any of your write-ups
Total Rep : Sum of the reputaion of all your write-ups
Node Fu : XP / Nodes
WNF : Writeup Node Fu (Total Rep + Cools*10) / Nodes + 1
Cool Ratio : Cools / Nodes
Messages : Number of messages in your in box
Average Rep: Arithmetic mean of the reps of all your writeups
Median Rep : The rep of the middle node, after sorting by rep
Merit : Arithmetic mean of the reps of the middle half of your write-ups
Devotion : Merit * Nodes
Merit Range: The rep range involved in your Merit
Up votes : Total up votes you've received
Down votes : Total down votes you've received
Votes : Total up or down votes you've received
Max Cools : Number of cools on your most-cooled wu
Max Votes : Number of votes on your most-voted writeup
Popularity : Up votes / Votes
a breakdown of your write-ups by type
If you specify 'ext' on the command line, you get these Top 5's :
Highest Rep
Lowest Rep
Most Votes
Fewest Votes
Most Cools
";
}
sub USAGE {
print "
E2 Node Tracker by Cow Of Doom and avjewe, version $version
USAGE: $0 <username> <password> [update|check|both|ext]
Displays various statistics about you and your writeups, and
shows you what has changed since your last run
By default, the new stats are written to ~/.e2info
If 'check' is specified, ~/.e2info is not updated
If 'ext' is specified, the report also contains the
top 5 write-ups of yours based on each of a number
of critera.
Also appends a line to ~/.e2history with each run, with tab separated columns :
date, XP, node, cools, total rep, messages, merit, devotion, mean rep, median rep,
upvotes, downvotes, maxcools, maxvotes
For more help, use --help
";
print '
Feedback to ajewell@cinci.rr.com or avjewe at E2
';
}
my $update = 0;
my $postupdate = 0;
if (@ARGV == 3) {
if (lc($ARGV[2]) eq "update") {
$postupdate = 1;
}
elsif (lc($ARGV[2]) eq "both") {
$postupdate = 1;
}
elsif (lc($ARGV[2]) eq "ext") {
$ext = 5;
$postupdate = 1;
}
elsif (lc($ARGV[2]) =~ m/^ext(\d+)$/) {
$ext = $1;
$postupdate = 1;
}
elsif (lc($ARGV[2]) ne "check") {
USAGE();
}
}
print "Logging in...";
(my $homenode = &login(@ARGV))
or die "failed";
# get the XP count from the user's homenode.
print "ok.\nGetting homenode...";
my %info;
my $snork = &getnode($homenode);
($info{xp}) = ($snork =~
m|HREF="/index.pl\?.*createtime%20DESC.*</a>/(-?\d+)|)
or die "failed";
# get the User Search XML page, and array-ify it
print "ok.\nDoing user search...";
my $ffoo = getnode(762826);
my @data = split(/\n/,$ffoo) # 762826 = User Search XML Ticker
or die "failed";
print "ok.\n";
foreach ('nodes') {$info{$_} = 0;} # initialize counters
my %node;
my %types;
# Read the info out of the User Search page.
my @reps;
my @wudata;
foreach (@data) { # loop over each line in the page
if (/^<writeup/g) { # if this line is about a writeup..
my %n;
while (/ (\w+)=\"(.*?)\"/gc) { $n{$1}=$2; } # get node info
my ($name, $type) = />(.*) \(([a-z]+)\)<\/writeup>/gc;
if (($name eq "E2 Nuke Request") or
($name eq "Edit these E2 titles") or
($name eq "Nodeshells marked for destruction") or
($name eq "Broken Nodes")) {
$info{xp}--;
next;
}
$n{name} = $name;
$n{type} = $type;
$n{votes} = $n{downvotes} + $n{upvotes};
push(@wudata, \%n);
$types{$type} = 0 unless defined($types{$type});
$types{$type}++; $info{nodes}++;
push(@reps, $n{reputation});
$info{totalrep} += $n{reputation};
$info{cools} += $n{cooled};
$info{downvotes} += $n{downvotes};
$info{upvotes} += $n{upvotes};
my $totVotes = $n{downvotes}+$n{upvotes};
if ($info{nodes} == 1) {$info{maxrep}=$info{minrep}=$n{reputation}}
if ($info{nodes} == 1) {$info{maxvotes}=$totVotes}
if ($info{nodes} == 1) {$info{maxcools}=$n{cooled}}
if ($n{reputation} > $info{maxrep}) {$info{maxrep}=$n{reputation}}
if ($n{reputation} < $info{minrep}) {$info{minrep}=$n{reputation}}
if ($totVotes > $info{maxvotes}) {$info{maxvotes} = $totVotes}
if ($n{cooled} > $info{maxcools}) {$info{maxcools} = $n{cooled}}
$node{$n{node_id}} = [$n{reputation},$n{cooled},$name,$n{upvotes},$n{downvotes}];
}
}
my @rep2 = sort {$a <=> $b} @reps;
my $sz = 0 + @rep2;
my $stt = int(($sz)/4);
my $stp = int(($sz*3)/4 + 0.5);
my $tot = 0;
my $tot2 = 0;
my @counts;
for (my $i=0; $i<500; ++$i) {
push @counts, 0;
}
for (my $i=$stt; $i<$stp; ++$i) {
my $rep = $rep2[$i];
$tot += $rep;
$counts[$rep]++ if ($rep >= 0);
}
open(DATAFILE, ">>$meritHist") or die "Couldn't open $meritHist: $!";
print DATAFILE scalar gmtime;
for (my $i=0; $i<50; ++$i) {
print DATAFILE "\t$i:$counts[$i]", if ($counts[$i]);
}
print DATAFILE "\n";
close(DATAFILE);
for (my $i=0; $i<500; ++$i) {
$counts[$i] = 0;
}
my $minrep = $rep2[0];
for (my $i=0; $i<$sz; ++$i) {
my $rep = $rep2[$i];
$tot2 += $rep;
$counts[$rep - $minrep]++;
}
open(DATAFILE, ">>$totalHist") or die "Couldn't open $totalHist: $!";
print DATAFILE scalar gmtime;
for (my $i=0; $i<500; ++$i) {
my $rep = $i + $minrep;
print DATAFILE "\t$rep:$counts[$i]" if ($counts[$i]);
}
print DATAFILE "\n";
close(DATAFILE);
my $foo = $stp-$stt;
my $median = $rep2[$sz/2];
my $average = $tot2/$sz;
my $merit = $tot/($stp-$stt);
my $devotion = int($merit * @rep2 + 0.5);
$info{average} = $average;
$info{median} = $median;
$info{merit} = $merit;
$info{devotion} = $devotion;
my $minmerit = $rep2[$stt];
my $maxmerit = $rep2[$stp-1];
undef(@data); # free the memory used by @data
if ($info{nodes}) {
$info{wnf} = (($info{totalrep}+(10*$info{cools}))/$info{nodes})+1;
$info{nodefu} = $info{xp}/$info{nodes};
$info{coolratio} = ($info{cools}*100)/$info{nodes};
}
my ($oir, $onr) = &readdatafile($datafile);
my %oldinfo;
my %oldnode;
if ($oir == 0) {
$update=1;
print "missing. Will be created.\n";
} else {
%oldinfo = %$oir, %oldnode = %$onr;
}
sub Update {
open(DATAFILE,">$datafile") or die "Couldn't open $datafile: $!";
print DATAFILE "$info{xp}:$info{nodes}:$info{cools}:$info{totalrep}:$info{messages}:",
"$info{merit}:$info{devotion}:$info{average}:$info{median}:$info{upvotes}:$info{downvotes}:",
"$info{maxcools}:$info{maxvotes}\n";
foreach (sort {$b<=>$a} keys(%node)) {
print DATAFILE "$_:",join(":",@{$node{$_}}),"\n";
}
close(DATAFILE);
open(DATAFILE, ">>$historyfile") or die "Couldn't open $datafile: $!";
my $foo = scalar gmtime;
print DATAFILE "$foo\t$info{xp}\t$info{nodes}\t$info{cools}\t$info{totalrep}\t$info{messages}\t",
"$info{merit}\t$info{devotion}\t$info{average}\t$info{median}\t$info{upvotes}\t$info{downvotes}\t",
"$info{maxcools}\t$info{maxvotes}\n";
close(DATAFILE);
}
if ($update) {
Update();
exit(0); # no point in printing stats right after an update.
}
sub MakeEven {
my ($aref) = @_;
my $len = 0;
foreach (@$aref) {
$len = length($_) if (length($_) > $len)
}
foreach (@$aref) {
if (m/--$/) {
$_ .= "-" x ($len - length($_));
}
else {
$_ .= " " x ($len - length($_));
}
}
};
$oldinfo{votes} = $oldinfo{upvotes} + $oldinfo{downvotes};
$info{votes} = $info{upvotes} + $info{downvotes};
$info{popularity} = 0;
$oldinfo{popularity} = 0;
$info{popularity} = $info{upvotes} * 100 / $info{votes} if ($info{votes});
$oldinfo{popularity} = $oldinfo{upvotes} * 100 / $oldinfo{votes} if ($oldinfo{votes});
my $line='-'x65;
printf("
E2 USER INFO: last update %s
-----------------------------------------------------------------\n",
scalar(localtime((stat($datafile))[9])));
my @c1;
my @c2;
my @c3;
push @c1, "Nodes: " . infodiff('nodes');
push @c2, "XP: " . infodiff('xp');
push @c3, "Cools: " . infodiff('cools');
push @c1, "Max Rep: " . infodiff('maxrep');
push @c2, "Min Rep: " . infodiff('minrep');
push @c3, "Total Rep: " . infodiff('totalrep');
push @c1, "Node Fu: " . infodiff_fp('nodefu');
push @c2, "WNF: " . infodiff_fp('wnf');
push @c3, "Cool Ratio: " . infodiff_fp('coolratio', 'yes');
push @c1, "Messages: " . infodiff('messages');
push @c2, "Average Rep: " . infodiff_fp('average');
push @c3, "Median rep: " . infodiff('median');
push @c1, "Merit: " . infodiff_fp('merit');
push @c2, "Devotion: " . infodiff('devotion');
push @c3, "Merit Range: $minmerit to $maxmerit";
push @c1, "Up votes: " . infodiff('upvotes');
push @c2, "Down votes: " . infodiff('downvotes');
push @c3, "Votes : " . infodiff('votes');
push @c1, "Max Cools: " . infodiff('maxcools');
push @c2, "Max Votes : " . infodiff('maxvotes');
push @c3, "Popularity: " . infodiff_fp('popularity', 'yes');
MakeEven(\@c1);
MakeEven(\@c2);
for (my $i=0; $i<7; ++$i) {
print "$c1[$i] $c2[$i] $c3[$i]\n";
}
print "\n";
if ($info{nodes}) {
foreach (keys %types) {
printf("%s: %3.1f%% ",$_,(100 * $types{$_})/($info{nodes}));
}
}
print("\n$line\n");
my $head="
Created/Nuked/Renamed:
Change Title\n$line\n";
my $didhead = 0;
my @nodes;
foreach (uniq(sort({$b<=>$a} keys(%node),keys(%oldnode)))) {
if (!exists($oldnode{$_})) {
if (!$didhead) {print $head; $didhead = 1;}
printf("Created | %s\n",$node{$_}->[2]);
$oldnode{$_} = [0,0,$node{$_}->[2]];
push @nodes, $_;
} elsif (!exists($node{$_})) {
if (!$didhead) {print $head; $didhead = 1;}
printf("Nuked | %s\n",$oldnode{$_}->[2]);
} else {
if ($node{$_}->[0] != $oldnode{$_}->[0]) {push (@nodes, $_);}
if ($node{$_}->[1] != $oldnode{$_}->[1]) {push (@nodes, $_);}
if ($node{$_}->[3] != $oldnode{$_}->[3]) {push (@nodes, $_);}
if ($node{$_}->[4] != $oldnode{$_}->[4]) {push (@nodes, $_);}
if ($node{$_}->[2] ne $oldnode{$_}->[2]) {
if (!$didhead) {print $head; $didhead = 1;}
printf("Renamed | %s->%s\n",$oldnode{$_}->[2],$node{$_}->[2]);
}
}
}
my $change;
if ($didhead) {print $line, "\n"; $change = 1;}
$head="
Reputation Changes / Cools:\n";
$didhead = 0;
my (@a1, @a2, @a3, @a4, @a5);
foreach (uniq(@nodes)) {
if (!$didhead) {
$didhead = 1;
print $head;
push @a1, "Rep";
push @a1, "---";
push @a2, "+/-";
push @a2, "---";
push @a3, "C!";
push @a3, "--";
push @a4, "+/-";
push @a4, "---";
push @a5, "Title";
push @a5, "-----";
}
$oldnode{$_}->[3] = 0 unless defined($oldnode{$_}->[3]);
$oldnode{$_}->[4] = 0 unless defined($oldnode{$_}->[4]);
my $d = $node{$_}->[0]-$oldnode{$_}->[0];
my $d1 = $node{$_}->[3]-$oldnode{$_}->[3];
my $d2 = $node{$_}->[4]-$oldnode{$_}->[4];
my $dcool = $node{$_}->[1] - $oldnode{$_}->[1];
if ($node{$_}->[3] && $node{$_}->[4]) {
push @a1, sprintf "%+i (%+i/%+i)", $node{$_}->[0], $node{$_}->[3], -$node{$_}->[4];
}
else {
push @a1, sprintf "%+i", $node{$_}->[0];
}
if ($d1 and $d2) {
push @a2, sprintf "%+i (%+i/%+i)", $d, $d1, -$d2;
}
else {
push @a2, sprintf "%+i", $d;
}
push @a3, sprintf "%i", $node{$_}->[1];
if ($dcool) {
push @a4, sprintf "%+i", $dcool;
}
else {
push @a4, "";
}
push @a5, $node{$_}->[2];
}
if ($didhead) {
MakeEven(\@a1);
MakeEven(\@a2);
MakeEven(\@a3);
MakeEven(\@a4);
for (my $i=0; $i < @a1; ++$i) {
print "$a1[$i] $a2[$i] $a3[$i] $a4[$i] $a5[$i]\n";
}
print "\n";
}
elsif (!$change) {
print "No nodes changed.\n";
}
sub PrintOne {
my $node = $_[0];
print "$node->{reputation} (+$node->{upvotes}/-$node->{downvotes})";
print " $node->{cooled} C! $node->{name}\n";
}
if ($postupdate) {
Update();
}
if ($ext) {
my @wu = sort {$a->{reputation} <=> $b->{reputation}} @wudata;
print "\nTop $ext Lowest Rep : \n";
for (my $i=0; $i<$ext; ++$i) {
PrintOne($wu[$i]);
}
print "\nTop $ext Highest Rep : \n";
for (my $i=0; $i<$ext; ++$i) {
PrintOne($wu[-$i-1]);
}
@wu = sort {$a->{votes} <=> $b->{votes}} @wudata;
print "\nTop $ext Fewest Votes : \n";
for (my $i=0; $i<$ext; ++$i) {
PrintOne($wu[$i]);
}
print "\nTop $ext Most Votes : \n";
for (my $i=0; $i<$ext; ++$i) {
PrintOne($wu[-$i-1]);
}
@wu = sort {$a->{cooled} <=> $b->{cooled}} @wudata;
print "\nTop $ext Most Cools : \n";
for (my $i=0; $i<$ext; ++$i) {
PrintOne($wu[-$i-1]);
}
print "\n";
@wu = sort {$a->{downvotes} <=> $b->{downvotes}} @wudata;
print "\nTop $ext Most Downvotes : \n";
for (my $i=0; $i<$ext; ++$i) {
PrintOne($wu[-$i-1]);
}
print "\n";
}
#----- end of main program ------------------------------
#----- subroutines --------------------------------------
sub uniq {
# takes a list argument
# assumes the list is sorted (use sort() if not)
# returns that list with duplicates removed
# examples: @foo=uniq(@sorted); @foo=uniq(sort(@random));
my (@list, @result);
@list = @_; @result = ();
foreach (@list) {
if ((!@result) || ($result[-1] != $_)) {push(@result,$_);}
}
return(@result);
}
sub getnode {
# takes one argument: $node_id
# assumes that $ua is a valid HTTP::UserAgent object
# returns the contents of the page in a scalar variable
# example: $page = getnode($node_id);
my $req = HTTP::Request->new('GET', "$baseurl?node_id=$_[0]");
return($ua->request($req)->content());
}
sub login {
# takes two arguments: $username, $password
# assumes that $ua is a valid HTTP::UserAgent object
# returns node_id of homenode on success, 0 on failure
# example: $homenode = login($username, $password);
my $homenode = 0;
my $req = HTTP::Request->new('POST', "$baseurl?node_id=109");
$req->content_type('application/x-www-form-urlencoded');
$req->content("op=login&node_id=109&user=$_[0]&passwd=$_[1]");
my $response = $ua->request($req);
if ($response->content() =~ m|Log Out.*\n.*node_id=(\d+)|i) {
$homenode = $1;
}
else {
open(FILE, ">$badLogin");
print FILE $response->content();
close(FILE);
}
if ($response->content() =~ m|you have <A [^>]+>(\d+)</a> messages|i) {
$info{messages} = $1;
}
else {
$info{messages} = 0;
}
return($homenode);
}
sub readdatafile {
# takes one argument: $filename
# gets stored user info from $filename.
# returns a list of two hash references.
# example: ($inforef, $noderef) = getinfo($filename);
if (! -f $_[0]) {
return(0);
} else {
my (%info, %node);
open(DATA,$_[0]);
($info{xp}, $info{nodes}, $info{cools}, $info{totalrep}, $info{messages}, $info{merit},
$info{devotion}, $info{average}, $info{median}, $info{upvotes}, $info{downvotes},
$info{maxcools},$info{maxvotes}) = split(/:/,<DATA>);
$info{xp} = 0 unless defined($info{xp}) && ($info{xp}) =~ m/^\d+$/;
$info{cools} = 0 unless defined($info{cools}) && ($info{cools}) =~ m/^\d+$/;
$info{totalrep} = 0 unless defined($info{totalrep}) && ($info{totalrep}) =~ m/^\d+$/;
$info{nodes} = 0 unless defined($info{nodes}) && ($info{nodes}) =~ m/^\d+$/;
$info{messages} = 0 unless defined($info{messages}) && ($info{messages}) =~ m/^\d+$/;
$info{merit} = 0 unless defined($info{merit}) && ($info{merit}) =~ m/^[\d.]+$/;
$info{devotion} = 0 unless defined($info{devotion}) && ($info{devotion}) =~ m/^[\d.]+$/;
$info{average} = 0 unless defined($info{average}) && ($info{average}) =~ m/^[\d.]+$/;
$info{median} = 0 unless defined($info{median}) && ($info{median}) =~ m/^[\d.]+$/;
$info{upvotes} = 0 unless defined($info{upvotes}) && ($info{upvotes}) =~ m/^\d+$/;
$info{downvotes} = 0 unless defined($info{downvotes}) && ($info{downvotes}) =~ m/^\d+$/;
$info{maxcools} = 0 unless defined($info{maxcools}) && ($info{maxcools}) =~ m/^\d+$/;
$info{maxvotes} = 0 unless defined($info{maxvotes}) && ($info{maxvotes}) =~ m/^\d+$/;
chomp($info{xp}, $info{nodes}, $info{cools}, $info{totalrep}, $info{messages}, $info{merit},
$info{devotion}, $info{average}, $info{median}, $info{upvotes}, $info{downvotes},
$info{maxcools},$info{maxvotes});
if ($info{nodes}) {
$info{wnf} = (($info{totalrep}+(10*$info{cools}))/$info{nodes})+1;
$info{nodefu} = $info{xp}/$info{nodes};
$info{coolratio} = ($info{cools}*100)/$info{nodes};
}
while (<DATA>) {
chomp;
if (/^(\d+):(-?\d+):(\d+):(.+):(\d+):(\d+)$/) {
@{$node{$1}} = ($2,$3,$4,$5,$6);
}
elsif (/^(\d+):(-?\d+):(\d+):(.*)$/) {
@{$node{$1}} = ($2,$3,$4,0,0);
}
else {
print STDERR "You Suck!\n";
}
if ($.<=2) {$info{maxrep}=$info{minrep}=$2}
if ($2 > $info{maxrep}) {$info{maxrep}=$2}
if ($2 < $info{minrep}) {$info{minrep}=$2}
}
close(DATA);
return(\%info,\%node);
}
}
sub infodiff {
# takes one argument. returns a string that shows the value of that
# piece of the %info hash, and the change in the value, if any.
# example: infodiff('xp') could return "132", "133 (+1)", etc.
my $arg = $_[0];
my $str = $info{$arg};
$oldinfo{$arg} = 0 unless defined($oldinfo{$arg});
if ($oldinfo{$arg} != $info{$arg}) {
$str .= " (".($info{$arg}>$oldinfo{$arg}?'+':'');
$str .= ($info{$arg}-$oldinfo{$arg}).")";
}
return($str);
}
sub infodiff_fp {
# takes one argument. returns a string that shows the value of that
# piece of the %info hash, and the change in the value, if any.
# example: infodiff('xp') could return "132", "133 (+1)", etc.
my $arg = $_[0];
my $perc = $_[1];
my $str = sprintf "%1.2f", $info{$arg};
if (defined($perc)) {$str .= '%';}
$oldinfo{$arg} = 0 unless defined($oldinfo{$arg});
my $diff = $info{$arg} - $oldinfo{$arg};
if (($diff > 0.001) || ($diff < -0.001)) {
$str .= " (";
$str .= "+" if ($diff > 0);
$str .= sprintf "%1.3f%s)", $diff, defined($perc) ? "%" : "";
}
return($str);
}