reuse relative value calculation
[barcat.git] / barcat
diff --git a/barcat b/barcat
index 6ab610db6282e26f15996fb88605e96f2a5669ba..c3c0dfd86faf8d56c773ca67a47a2062ab48dcb3 100755 (executable)
--- a/barcat
+++ b/barcat
@@ -6,7 +6,7 @@ use List::Util qw( min max sum );
 use open qw( :std :utf8 );
 use re '/msx';
 
 use open qw( :std :utf8 );
 use re '/msx';
 
-our $VERSION = '1.08';
+our $VERSION = '1.09';
 
 my %opt;
 if (@ARGV) {
 
 my %opt;
 if (@ARGV) {
@@ -14,15 +14,21 @@ require Getopt::Long;
 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
 GetOptions(\%opt,
        'ascii|a!',
 Getopt::Long->import('2.33', qw( :config gnu_getopt ));
 GetOptions(\%opt,
        'ascii|a!',
-       'color|c!',
-       'C' => sub { $opt{color} = 0 },
+       'color|C!',
+       'M' => sub { $opt{color} = 0 },
        'field|f=s' => sub {
                eval {
                        local $_ = $_[1];
        'field|f=s' => sub {
                eval {
                        local $_ = $_[1];
-                       $opt{anchor} = /\A[0-9]+\z/ ? qr/(?:\S*\h+){$_}\K/ : qr/$_/;
+                       s/\A[0-9]+\z/(?:\\S*\\h+){$_}\\K/;
+                       s{\A[+]([0-9]*)\z}{
+                               (!!$1 && '(?:\d+\D+\b){'.$1.'}\K') . '\s* (?=\d)'
+                       }e;
+                       $opt{anchor} = qr/$_/;
                } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
        },
        'human-readable|H!',
                } or die $@ =~ s/(?:\ at\ \N+)?\Z/ for option $_[0]/r;
        },
        'human-readable|H!',
+       'sexagesimal!',
+       'reformat!',
        'interval|t:i',
        'trim|length|l=s' => sub {
                my ($optname, $optval) = @_;
        'interval|t:i',
        'trim|length|l=s' => sub {
                my ($optname, $optval) = @_;
@@ -48,6 +54,7 @@ GetOptions(\%opt,
                        " (range expected)\n"
                );
        },
                        " (range expected)\n"
                );
        },
+       'log|e!',
        'header!',
        'markers|m=s',
        'graph-format=s' => sub {
        'header!',
        'markers|m=s',
        'graph-format=s' => sub {
@@ -111,7 +118,7 @@ $opt{trim}   *= $opt{width} / 100 if $opt{trimpct};
 $opt{units}   = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
        if $opt{'human-readable'};
 $opt{anchor} //= qr/\A/;
 $opt{units}   = [split //, ' kMGTPEZYyzafpn'.($opt{ascii} ? 'u' : 'μ').'m']
        if $opt{'human-readable'};
 $opt{anchor} //= qr/\A/;
-$opt{'value-length'} = 6 if $opt{units};
+$opt{'value-length'} = 4 if $opt{units};
 $opt{'value-length'} = 1 if $opt{unmodified};
 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
 $opt{'value-length'} = 1 if $opt{unmodified};
 $opt{'signal-stat'} //= exists $SIG{INFO} ? 'INFO' : 'QUIT';
 $opt{markers} //= '=avg >31.73v <68.27v +50v |0';
@@ -123,9 +130,11 @@ $opt{hidemin} = ($opt{hidemin} || 1) - 1;
 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
        and undef $opt{interval};
 
 $opt{input} = (@ARGV && $ARGV[0] =~ m/\A[-0-9]/) ? \@ARGV : undef
        and undef $opt{interval};
 
-$opt{'sum-format'} = sub { sprintf '%.8g', $_[0] };
 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
 $opt{'calc-format'} = sub { sprintf '%*.*f', 0, 2, $_[0] };
-$opt{'value-format'} = $opt{units} && sub {
+$opt{'value-format'} = $opt{sexagesimal} ? sub {
+       my $s = abs($_[0]) + .5;
+       sprintf('%s%d:%02d:%02d', $_[0] < 0 && '-', $s/3600, $s/60%60, $s%60);
+} : $opt{units} && sub {
        my $unit = (
                log(abs $_[0] || 1) / log(10)
                - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
        my $unit = (
                log(abs $_[0] || 1) / log(10)
                - 3 * (abs($_[0]) < .9995)   # shift to smaller unit if below 1
@@ -142,7 +151,8 @@ $opt{'value-format'} = $opt{units} && sub {
                $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
                        $opt{units}->[$unit/3]  # suffix
        );
                $#{$opt{units}} * 1.5 < abs $unit ? sprintf('e%d', $unit) :
                        $opt{units}->[$unit/3]  # suffix
        );
-};
+} and $opt{reformat}++;
+$opt{'value-format'} ||= sub { sprintf '%.8g', $_[0] };
 
 
 my (@lines, @values, @order);
 
 
 my (@lines, @values, @order);
@@ -189,10 +199,6 @@ while (defined ($_ = $opt{input} ? shift @{ $opt{input} } : readline)) {
                and $. % $opt{interval} == 0;
 }
 
                and $. % $opt{interval} == 0;
 }
 
-if ($opt{'zero-missing'}) {
-       push @values, (0) x 10;
-}
-
 $SIG{INT} = 'DEFAULT';
 
 sub color {
 $SIG{INT} = 'DEFAULT';
 
 sub color {
@@ -225,12 +231,13 @@ my $maxval = $opt{maxval} // (
 ) // 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
 my $range = $maxval - $minval;
 ) // 0;
 my $minval = $opt{minval} // min $order[-1] // (), 0;
 my $range = $maxval - $minval;
+$range &&= log $range if $opt{log};
 my $lenval = $opt{'value-length'} // max map { length } @order;
 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
        max map { length $values[$_] && length $lines[$_] }
                0 .. min $#lines, $opt{hidemax} || ();  # left padding
 my $size   = defined $opt{width} && $range &&
 my $lenval = $opt{'value-length'} // max map { length } @order;
 my $len    = defined $opt{trim} && $opt{trim} <= 0 ? -$opt{trim} + 1 :
        max map { length $values[$_] && length $lines[$_] }
                0 .. min $#lines, $opt{hidemax} || ();  # left padding
 my $size   = defined $opt{width} && $range &&
-       ($opt{width} - $lenval - $len - !!$opt{indicators}) / $range;  # bar multiplication
+       ($opt{width} - $lenval - $len - !!$opt{indicators});  # bar multiplication
 
 my @barmark;
 if ($opt{markers} and $size > 0) {
 
 my @barmark;
 if ($opt{markers} and $size > 0) {
@@ -241,7 +248,9 @@ if ($opt{markers} and $size > 0) {
                                return sum(@order) / @order;
                        }
                        elsif ($func =~ /\A([0-9.]+)v\z/) {
                                return sum(@order) / @order;
                        }
                        elsif ($func =~ /\A([0-9.]+)v\z/) {
-                               die "Invalid marker $char: percentile $1 out of bounds\n" if $1 > 100;
+                               $1 <= 100 or die(
+                                       "Invalid marker $char: percentile $1 out of bounds\n"
+                               );
                                my $index = $#order * $1 / 100;
                                return ($order[$index] + $order[$index + .5]) / 2;
                        }
                                my $index = $#order * $1 / 100;
                                return ($order[$index] + $order[$index + .5]) / 2;
                        }
@@ -257,8 +266,9 @@ if ($opt{markers} and $size > 0) {
                        next;
                };
                $pos -= $minval;
                        next;
                };
                $pos -= $minval;
+               $pos &&= log $pos if $opt{log};
                $pos >= 0 or next;
                $pos >= 0 or next;
-               color(36) for $barmark[$pos * $size] = $char;
+               color(36) for $barmark[$pos / $range * $size] = $char;
        }
 
        state $lastmax = $maxval;
        }
 
        state $lastmax = $maxval;
@@ -266,10 +276,10 @@ if ($opt{markers} and $size > 0) {
                print ' ' x ($lenval + $len);
                printf color(90);
                printf '%-*s',
                print ' ' x ($lenval + $len);
                printf color(90);
                printf '%-*s',
-                       ($lastmax - $minval) * $size + .5,
-                       '-' x (($values[$nr - 1] - $minval) * $size);
+                       ($lastmax - $minval) * $size / $range + .5,
+                       '-' x (($values[$nr - 1] - $minval) * $size / $range);
                print color(92);
                print color(92);
-               say '+' x (($range - $lastmax) * $size + .5);
+               say '+' x (($range - $lastmax) * $size / $range + .5);
                print color(0);
                $lastmax = $maxval;
        }
                print color(0);
                $lastmax = $maxval;
        }
@@ -278,14 +288,19 @@ if ($opt{markers} and $size > 0) {
 say(
        color(31), sprintf('%*s', $lenval, $minval),
        color(90), '-', color(36), '+',
 say(
        color(31), sprintf('%*s', $lenval, $minval),
        color(90), '-', color(36), '+',
-       color(32), sprintf('%*s', $size * $range - 3, $maxval),
+       color(32), sprintf('%*s', $size - 3, $maxval),
        color(90), '-', color(36), '+',
        color(0),
 ) if $opt{header};
 
 while ($nr <= $limit) {
        my $val = $values[$nr];
        color(90), '-', color(36), '+',
        color(0),
 ) if $opt{header};
 
 while ($nr <= $limit) {
        my $val = $values[$nr];
-       my $rel = length $val && $range && min(1, ($val - $minval) / $range);
+       my $rel;
+       if (length $val) {
+               $rel = $val - $minval;
+               $rel &&= log $rel if $opt{log};
+               $rel = min(1, $rel / $range) if $range; # 0..1
+       }
        my $color = !length $val || !$opt{palette} ? undef :
                $val == $order[0] ? $opt{palette}->[-1] : # max
                $val == $order[-1] ? $opt{palette}->[0] : # min
        my $color = !length $val || !$opt{palette} ? undef :
                $val == $order[0] ? $opt{palette}->[-1] : # max
                $val == $order[-1] ? $opt{palette}->[0] : # min
@@ -305,8 +320,9 @@ while ($nr <= $limit) {
        print $indicator if defined $indicator;
 
        if (length $val) {
        print $indicator if defined $indicator;
 
        if (length $val) {
-               $val = $opt{'value-format'} ? $opt{'value-format'}->($val) :
-                       sprintf "%*s", $lenval, $val;
+               $val = sprintf("%*s", $lenval,
+                       $opt{reformat} ? $opt{'value-format'}->($val) : $val
+               );
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
                color($color) for $val;
        }
        my $line = $lines[$nr] =~ s/\n/$val/r;
@@ -315,8 +331,10 @@ while ($nr <= $limit) {
                next;
        }
        printf '%-*s', $len + length($val), $line;
                next;
        }
        printf '%-*s', $len + length($val), $line;
-       print $barmark[$_] // $opt{'graph-format'}
-               for 1 .. $size && (($values[$nr] || 0) - $minval) * $size + .5;
+       if ($rel and $size) {
+               print $barmark[$_] // $opt{'graph-format'}
+                       for 1 .. $rel * $size + .5;
+       }
        say '';
 }
 continue {
        say '';
 }
 continue {
@@ -335,19 +353,19 @@ sub show_stat {
                        $linemin += @lines;
                        $linemax = @lines - $linemax;
                }
                        $linemin += @lines;
                        $linemax = @lines - $linemax;
                }
-               printf '%.8g of ', $opt{'sum-format'}->(
+               printf '%.8g of ', $opt{'value-format'}->(
                        sum(grep {length} @values[$linemin .. $linemax]) // 0
                );
        }
        if (@order) {
                my $total = sum @order;
                        sum(grep {length} @values[$linemin .. $linemax]) // 0
                );
        }
        if (@order) {
                my $total = sum @order;
-               printf '%s total', color(1) . $opt{'sum-format'}->($total) . color(0);
+               printf '%s total', color(1) . $opt{'value-format'}->($total) . color(0);
                printf ' in %d values', scalar @order;
                printf ' over %d lines', scalar @lines if @order != @lines;
                printf(' (%s min, %s avg, %s max)',
                printf ' in %d values', scalar @order;
                printf ' over %d lines', scalar @lines if @order != @lines;
                printf(' (%s min, %s avg, %s max)',
-                       color(31) . ($opt{'value-format'} || sub {$_[0]})->($order[-1]) . color(0),
-                       color(36) . ($opt{'value-format'} || $opt{'calc-format'})->($total / @order) . color(0),
-                       color(32) . ($opt{'value-format'} || sub {$_[0]})->($order[0]) . color(0),
+                       color(31) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[-1]) . color(0),
+                       color(36) . ($opt{reformat} ? $opt{'value-format'} : $opt{'calc-format'})->($total / @order) . color(0),
+                       color(32) . ($opt{reformat} ? $opt{'value-format'} : sub {$_[0]})->($order[0]) . color(0),
                );
        }
        say '';
                );
        }
        say '';
@@ -369,18 +387,21 @@ Usage:                                               /\_/\
                                                     (u   u)
 Options:
   -a, --[no-]ascii         Restrict user interface to ASCII characters
                                                     (u   u)
 Options:
   -a, --[no-]ascii         Restrict user interface to ASCII characters
-  -c, --[no-]color         Force colored output of values and bar markers
-  -f, --field=(N|REGEXP)   Compare values after a given number of whitespace
+  -C, --[no-]color         Force colored output of values and bar markers
+  -f, --field=([+]N|REGEXP)
+                           Compare values after a given number of whitespace
                            separators
       --header             Prepend a chart axis with minimum and maximum
                            values labeled
   -H, --human-readable     Format values using SI unit prefixes
                            separators
       --header             Prepend a chart axis with minimum and maximum
                            values labeled
   -H, --human-readable     Format values using SI unit prefixes
+      --sexagesimal        Convert seconds to HH:MM:SS time format
   -t, --interval[=(N|-LINES)]
                            Output partial progress every given number of
                            seconds or input lines
   -l, --length=[-]SIZE[%]  Trim line contents (between number and bars)
   -L, --limit[=(N|-LAST|START-[END])]
                            Stop output after a number of lines
   -t, --interval[=(N|-LINES)]
                            Output partial progress every given number of
                            seconds or input lines
   -l, --length=[-]SIZE[%]  Trim line contents (between number and bars)
   -L, --limit[=(N|-LAST|START-[END])]
                            Stop output after a number of lines
+  -e, --log                Logarithmic (exponential) scale instead of linear
       --graph-format=CHAR  Glyph to repeat for the graph line
   -m, --markers=FORMAT     Statistical positions to indicate on bars
       --min=N, --max=N     Bars extend from 0 or the minimum value if lower
       --graph-format=CHAR  Glyph to repeat for the graph line
   -m, --markers=FORMAT     Statistical positions to indicate on bars
       --min=N, --max=N     Bars extend from 0 or the minimum value if lower
@@ -430,15 +451,15 @@ Restrict user interface to ASCII characters,
 replacing default UTF-8 by their closest approximation.
 Input is always interpreted as UTF-8 and shown as is.
 
 replacing default UTF-8 by their closest approximation.
 Input is always interpreted as UTF-8 and shown as is.
 
-=item -c, --[no-]color
+=item -C, --[no-]color
 
 Force colored output of values and bar markers.
 Defaults on if output is a tty,
 disabled otherwise such as when piped or redirected.
 
 Force colored output of values and bar markers.
 Defaults on if output is a tty,
 disabled otherwise such as when piped or redirected.
-Can also be disabled by setting I<-C>
+Can also be disabled by setting I<-M>
 or the I<NO_COLOR> environment variable.
 
 or the I<NO_COLOR> environment variable.
 
-=item -f, --field=(<number> | <regexp>)
+=item -f, --field=([+]<number> | <regexp>)
 
 Compare values after a given number of whitespace separators,
 or matching a regular expression.
 
 Compare values after a given number of whitespace separators,
 or matching a regular expression.
@@ -449,6 +470,7 @@ A string can indicate the starting position of a value
 (such as I<-f:> if preceded by colons),
 or capture the numbers itself,
 for example I<-f'(\d+)'> for the first digits anywhere.
 (such as I<-f:> if preceded by colons),
 or capture the numbers itself,
 for example I<-f'(\d+)'> for the first digits anywhere.
+A shorthand for this is I<+0>, or I<+N> to find the Nth number.
 
 =item --header
 
 
 =item --header
 
@@ -461,6 +483,10 @@ turning long numbers like I<12356789> into I<12.4M>.
 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
 Short integers are aligned but kept without decimal point.
 
 Also changes an exponent I<1.602176634e-19> to I<160.2z>.
 Short integers are aligned but kept without decimal point.
 
+=item --sexagesimal
+
+Convert seconds to HH:MM:SS time format.
+
 =item -t, --interval[=(<seconds> | -<lines>)]
 
 Output partial progress every given number of seconds or input lines.
 =item -t, --interval[=(<seconds> | -<lines>)]
 
 Output partial progress every given number of seconds or input lines.
@@ -486,6 +512,11 @@ A specific range can be given by two values.
 All input is still counted and analyzed for statistics,
 but disregarded for padding and bar size.
 
 All input is still counted and analyzed for statistics,
 but disregarded for padding and bar size.
 
+=item -e, --log
+
+Logarithmic (I<e>xponential) scale instead of linear
+to compare orders of magnitude.
+
 =item --graph-format=<character>
 
 Glyph to repeat for the graph line.
 =item --graph-format=<character>
 
 Glyph to repeat for the graph line.
@@ -572,7 +603,9 @@ Reserved space for numbers.
 =item -w, --width=<columns>
 
 Override the maximum number of columns to use.
 =item -w, --width=<columns>
 
 Override the maximum number of columns to use.
-Appended graphics will extend to fill up the entire screen.
+Appended graphics will extend to fill up the entire screen,
+otherwise determined by the environment variable I<COLUMNS>
+or by running the C<tput> command.
 
 =item -h, --usage
 
 
 =item -h, --usage
 
@@ -599,6 +632,20 @@ Compare file sizes (with human-readable numbers):
 
     du -d0 -b * | barcat -H
 
 
     du -d0 -b * | barcat -H
 
+Same from formatted results, selecting the first numeric value:
+
+    tree -s --noreport | barcat -H -f+
+
+Compare media metadata, like image size or play time:
+
+    exiftool -T -p '$megapixels ($imagesize) $filename' * | barcat
+
+    exiftool -T -p '$duration# $avgbitrate# $filename' * | barcat --sexagesimal
+
+    find -type f -print0 | xargs -0 -L1 \
+    ffprobe -show_format -of json -v error |
+    jq -r '.format|.duration+" "+.bit_rate+" "+.filename' | barcat --sex
+
 Memory usage of user processes with long names truncated:
 
     ps xo rss,pid,cmd | barcat -l40
 Memory usage of user processes with long names truncated:
 
     ps xo rss,pid,cmd | barcat -l40
@@ -621,19 +668,26 @@ Number of HTTP requests per day:
 
     cat httpd/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
 
 
     cat httpd/access.log | cut -d\  -f4 | cut -d: -f1 | uniq -c | barcat
 
-Any kind of database query with counts, preserving returned alignment:
+Any kind of database query results, preserving returned alignment:
 
 
-    echo 'SELECT count(*),schemaname FROM pg_tables GROUP BY 2' |
+    echo 'SELECT sin(value * .1) FROM generate_series(0, 30) value' |
     psql -t | barcat -u
 
     psql -t | barcat -u
 
-In PostgreSQL from within the client:
+In PostgreSQL from within the client; a fancy C<\dt+> perhaps:
+
+    > SELECT schemaname, relname, pg_total_relation_size(relid)
+      FROM pg_statio_user_tables ORDER BY idx_blks_hit
+      \g |barcat -uHf+
+
+Same thing in SQLite (requires the sqlite3 client):
 
 
-    > SELECT sin(generate_series(0, 3, .1)) \g |barcat
+    > .once |barcat -Hf+
+    > SELECT name, sum(pgsize) FROM dbstat GROUP BY 1;
 
 Earthquakes worldwide magnitude 1+ in the last 24 hours:
 
     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
 
 Earthquakes worldwide magnitude 1+ in the last 24 hours:
 
     curl https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/1.0_day.csv |
-    column -tns, | barcat -f4 -u -l80%
+    column -ts, -n | barcat -f4 -u -l80%
 
 External datasets, like movies per year:
 
 
 External datasets, like movies per year:
 
@@ -657,6 +711,11 @@ Total population history in XML from the World Bank:
     xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
     barcat -f1 -H
 
     xmlstarlet sel -t -m '*/*' -v wb:date -o ' ' -v wb:value -n |
     barcat -f1 -H
 
+Population and other information for all countries:
+
+    curl http://download.geonames.org/export/dump/countryInfo.txt |
+    grep -v '^#\s' | column -ts$'\t' -n | barcat -f+2 -e -u -l150 -s
+
 And of course various Git statistics, such commit count by year:
 
     git log --pretty=%ci | cut -b-4 | uniq -c | barcat
 And of course various Git statistics, such commit count by year:
 
     git log --pretty=%ci | cut -b-4 | uniq -c | barcat