# Print a text table of a given list of arrays. # # Usage: &text_table(\@header_fields, @rows) # If the @header_fields ref is undefined, we don't use header fields. # @rows contains either array refs (which are printed literally) or # hash refs (for which the @header_fields are used to determine which # values to print). # # Copyright 2004, Mark T. Abbott use strict; my $g_n_sep_spaces = 2; ### Begin main code. ### sub text_table { my ($header, @rows) = @_; my @print_fields; if ( ref($header) eq 'ARRAY' ) { @print_fields = @{$header}; } elsif ( ref($rows[0]) eq 'HASH' ) { @print_fields = sort keys %{$rows[0]} } else { @print_fields = undef; } my %field_length; map { $field_length{$_} = length($_) } @print_fields; my @clean_rows; foreach my $r (@rows) { my @row; if ( ref($r) eq 'ARRAY' ) { @row = @{$r}; } elsif ( ref($r) eq 'HASH' ) { @row = map { ${$r}{$_} } @print_fields; } else { next; } push @clean_rows, \@row; for (0..$#print_fields) { my $l = length($row[$_]); ($field_length{$print_fields[$_]}) = (sort {$b<=>$a} ($field_length{$print_fields[$_]}||0, $l)); } } # Find the proper field lengths for printing the rows. my $cols = &cols; while ( 1 ) { my $l = -$g_n_sep_spaces; map { $l += $field_length{$_} + $g_n_sep_spaces } @print_fields; last if $l <= $cols; # Choose a field whose length to reduce. my @reducible_fields = grep { $field_length{$_} > length($_) } reverse @print_fields; my %penalty_upon_reducing = (); foreach my $f ( @reducible_fields ) { my %fl = %field_length; --$fl{$f}; $penalty_upon_reducing{$f} = &field_length_penalty(\%fl, \@print_fields, \@clean_rows); } my ($reducee) = sort { $penalty_upon_reducing{$a} <=> $penalty_upon_reducing{$b} } @reducible_fields; $field_length{$reducee}--; } my $row_pattern = sprintf( join(' 'x$g_n_sep_spaces, map { '%%%ds' } @print_fields ), map { $field_length{$_} } @print_fields ); #warn $row_pattern; my $output = ''; foreach my $r (@clean_rows) { my @row = @{$r}; if ($r == $clean_rows[0]) { $output .= "\n"; $output .= sprintf $row_pattern, map { uc } @print_fields; $output .= "\n"; } my $line = sprintf $row_pattern, map { &shrink_to($field_length{$print_fields[$_]}, $row[$_]) } (0..$#print_fields); die "line exceeds screen width of $cols: $line" if length($line)>$cols; $output .= $line; $output .= "\n"; } $output .= "\n"; return $output; } ### Primary routines. ### sub cols { chomp (my $size = `/bin/stty size`); my ($w, $c) = ($size =~ m#(\d+)\s+(\d+)#); return $c||80; } sub field_length_penalty { my ($flref, $pfref, $rref) = @_; my %field_length = %{$flref}; my @print_fields = @{$pfref}; my @rows = @{$rref}; my $penalty = 0; foreach my $r (@rows) { my @row = @{$r}; foreach my $i (0..$#print_fields) { my $f = $print_fields[$i]; my $under = length($row[$i]) - $field_length{$f}; #print join("\t", $f, $t{$f}, $field_length{$f}, length($t{$f}), $under), "\n"; if ( $under > 0 ) { # Take the ellipsis ('..') into account. $under += 2; # The potential penalties are broken up by type # so they can be tweaked/configured/enabled # more easily. Currently, using C alone seems # to work best, because it suppresses the long # Album field most. # A. Penalty is the number of interruptions. #$penalty += 1; # B. Penalty is the number of characters lost. #$penalty += $under; # C. Penalty is the percent of info lost. #$penalty += 100 * $under/length($row[$i]); # D. Penalty is the percent of info lost, # squared (better to lose half of two columns # than all of one). $penalty += ($under/length($row[$i])) ** 2; } } } return $penalty; } sub shrink_to { my ($l, $s) = @_; my $over = length($s)-$l; return $s if $over<=0; my $insert = '..'; #warn "$l, $s, $over\n"; substr($s, int($l/2), $over+length($insert), $insert); return $s; } 1;