File Coverage

blib/lib/Text/FormatTable.pm
Criterion Covered Total %
statement 216 243 88.8
branch 68 88 77.2
condition 31 48 64.5
subroutine 23 23 100.0
pod 5 5 100.0
total 343 407 84.2


line stmt bran cond sub pod time code
1             package Text::FormatTable;
2              
3 1     1   7259 use Carp;
  1         3  
  1         82  
4 1     1   6 use strict;
  1         2  
  1         35  
5 1     1   5 use warnings;
  1         12  
  1         37  
6 1     1   6 use vars qw($VERSION);
  1         2  
  1         3323  
7              
8             $VERSION = '1.03';
9              
10             =head1 NAME
11              
12             Text::FormatTable - Format text tables
13              
14             =head1 SYNOPSIS
15              
16             my $table = Text::FormatTable->new('r|l');
17             $table->head('a', 'b');
18             $table->rule('=');
19             $table->row('c', 'd');
20             print $table->render(20);
21              
22             =head1 DESCRIPTION
23              
24             Text::FormatTable renders simple tables as text. You pass to the constructor
25             (I) a table format specification similar to LaTeX (e.g. C) and you
26             call methods to fill the table data and insert rules. After the data is filled,
27             you call the I method and the table gets formatted as text.
28              
29             Methods:
30              
31             =over 4
32              
33             =cut
34              
35             # Remove ANSI color sequences when calculating length
36             sub _uncolorized_length($)
37             {
38 130     130   153 my $str = shift;
39 130         192 $str =~ s/\e \[ [^m]* m//xmsg;
40 130         254 return length $str;
41             }
42              
43             # minimal width of $1 if word-wrapped
44             sub _min_width($)
45             {
46 23     23   22 my $str = shift;
47 23         43 my $min;
48 23         58 for my $s (split(/\s+/,$str)) {
49 37         56 my $l = _uncolorized_length $s;
50 37 100 100     138 $min = $l if not defined $min or $l > $min;
51             }
52 23 100       69 return $min ? $min : 1;
53             }
54              
55             # width of $1 if not word-wrapped
56             sub _max_width($)
57             {
58 23     23   42 my $str = shift;
59 23         36 my $len = _uncolorized_length $str;
60 23 100       66 return $len ? $len : 1;
61             }
62              
63             sub _max($$)
64             {
65 46     46   71 my ($a,$b) = @_;
66 46 100 66     191 return $a if defined $a and (not defined $b or $a >= $b);
      66        
67 30         46 return $b;
68             }
69              
70             # word-wrap multi-line $2 with width $1
71             sub _wrap($$)
72             {
73 23     23   26 my ($width, $text) = @_;
74 23         57 my @lines = split(/\n/, $text);
75 23         34 my @w = ();
76 23         31 for my $l (@lines) {
77 22         22 push @w, @{_wrap_line($width, $l)};
  22         40  
78             }
79 23         64 return \@w;
80             }
81              
82             sub _wrap_line($$)
83             {
84 22     22   29 my ($width, $text) = @_;
85 22         32 my $width_m1 = $width-1;
86 22         35 my @t = ($text);
87 22         24 while(1) {
88 35         50 my $t = pop @t;
89 35         57 my $l = _uncolorized_length $t;
90 35 100       214 if($l <= $width){
    50          
    0          
91             # last line is ok => done
92 22         44 push @t, $t;
93 22         129 return \@t;
94             }
95             elsif($t =~ /^(.{0,$width_m1}\S)\s+(\S.*?)$/) {
96             # farest space < width
97 13         34 push @t, $1;
98 13         30 push @t, $2;
99             }
100             elsif($t =~ /(.{$width,}?\S)\s+(\S.*?)$/) {
101             # nearest space > width
102 0 0       0 if ( _uncolorized_length $1 > $width_m1 )
103             {
104             # hard hyphanation
105 0         0 my $left = substr($1,0,$width);
106 0         0 my $right= substr($1,$width);
107              
108 0         0 push @t, $left;
109 0         0 push @t, $right;
110 0         0 push @t, $2;
111             }
112             else
113             {
114 0         0 push @t, $1;
115 0         0 push @t, $2;
116             }
117             }
118             else {
119             # hard hyphanation
120 0         0 my $left = substr($t,0,$width);
121 0         0 my $right= substr($t,$width);
122              
123 0         0 push @t, $left;
124 0         0 push @t, $right;
125 0         0 return \@t;
126             }
127             }
128 0         0 return \@t;
129             }
130              
131             # render left-box $2 with width $1
132             sub _l_box($$)
133             {
134 20     20   35 my ($width, $text) = @_;
135 20         31 my $lines = _wrap($width, $text);
136 20         34 map { $_ .= ' 'x($width-_uncolorized_length($_)) } @$lines;
  24         45  
137 20         36 return $lines;
138             }
139              
140             # render right-box $2 with width $1
141             sub _r_box($$)
142             {
143 3     3   6 my ($width, $text) = @_;
144 3         5 my $lines = _wrap($width, $text);
145 3         6 map { $_ = (' 'x($width-_uncolorized_length($_)).$_) } @$lines;
  11         18  
146 3         5 return $lines;
147             }
148              
149             # Algorithm of:
150             # http://ei5nazha.yz.yamagata-u.ac.jp/~aito/w3m/eng/STORY.html
151              
152             sub _distribution_f($)
153             {
154 10     10   10 my $max_width = shift;
155 10         17 return log($max_width);
156             }
157              
158             sub _calculate_widths($$)
159             {
160 4     4   5 my ($self, $width) = @_;
161 4         6 my @widths = ();
162             # calculate min and max widths for each column
163 4         4 for my $r (@{$self->{data}})
  4         12  
164             {
165 13 100 100     52 $r->[0] eq 'data' or $r->[0] eq 'head' or next;
166 8         7 my $cn=0;
167 8         10 my ($max, $min) = (0,0);
168            
169 8         8 for my $c (@{$r->[1]}) {
  8         13  
170            
171 23 50       52 if ( $self->{fixed_widths}[$cn] )
172             {
173             # fixed width
174 0         0 $widths[$cn][0] = $self->{fixed_widths}[$cn];
175 0         0 $widths[$cn][1] = $self->{fixed_widths}[$cn];
176             }
177             else
178             {
179 23         57 $widths[$cn][0] = _max($widths[$cn][0], _min_width $c);
180 23         55 $widths[$cn][1] = _max($widths[$cn][1], _max_width $c);
181             }
182 23         56 $cn++;
183             }
184             }
185              
186             # calculate total min and max width
187 4         14 my ($total_min, $total_max) = (0,0);
188 4         7 for my $c (@widths) {
189 11         14 $total_min += $c->[0];
190 11         15 $total_max += $c->[1];
191             }
192             # extra space
193 19 100       89 my $extra_width += scalar grep {$_->[0] eq '|' or $_->[0] eq ' '}
  4         9  
194 4         5 (@{$self->{format}});
195 4         9 $total_min += $extra_width;
196 4         4 $total_max += $extra_width;
197              
198             # if total_max <= screen width => use max as width
199 4 100       10 if($total_max <= $width) {
200 3         5 my $cn = 0;
201 3         6 for my $c (@widths) {
202 8         14 $self->{widths}[$cn]=$c->[1];
203 8         13 $cn++;
204             }
205 3         15 $self->{total_width} = $total_max;
206             }
207             else {
208 1         2 my @dist_width;
209 1         1 ITERATION: while(1) {
210 2         3 my $total_f = 0.0;
211 2         3 my $fixed_width = 0;
212 2         3 my $remaining=0;
213 2         3 for my $c (@widths) {
214 6 100       11 if(defined $c->[2]) {
215 1         3 $fixed_width += $c->[2];
216             }
217             else {
218 5         19 $total_f += _distribution_f($c->[1]);
219 5         7 $remaining++;
220             }
221             }
222 2         3 my $available_width = $width-$extra_width-$fixed_width;
223             # enlarge width if it isn't enough
224 2 100       5 if($available_width < $remaining*5) {
225 1         1 $available_width = $remaining*5;
226 1         2 $width = $extra_width+$fixed_width+$available_width;
227             }
228 2         2 my $cn=-1;
229 2         3 COLUMN: for my $c (@widths) {
230 6         5 $cn++;
231 6 100       10 next COLUMN if defined $c->[2]; # skip fixed-widths
232 5         9 my $w = _distribution_f($c->[1]) * $available_width / $total_f;
233 5 100       12 if($c->[0] > $w) {
234 1         2 $c->[2] = $c->[0];
235 1         2 next ITERATION;
236             }
237 4 50       8 if($c->[1] < $w) {
238 0         0 $c->[2] = $c->[1];
239 0         0 next ITERATION;
240             }
241 4         13 $dist_width[$cn] = int($w);
242             }
243 1         2 last;
244             }
245 1         1 my $cn = 0;
246 1         2 for my $c (@widths) {
247 3 100       10 $self->{widths}[$cn]=defined $c->[2] ? $c->[2] : $dist_width[$cn];
248 3         6 $cn++;
249             }
250             }
251             }
252              
253             sub _render_rule($$)
254             {
255 5     5   5 my ($self, $char) = @_;
256 5         7 my $out = '';
257 5         8 my ($col,$data_col) = (0,0);
258 5         5 for my $c (@{$self->{format}}) {
  5         44  
259 28 100 66     659 if($c->[0] eq '|') {
    100 66        
    50 33        
260 3 100       9 if ($char eq '-') { $out .= '+' }
  1 50       1  
261 0         0 elsif($char eq ' ') { $out .= '|' }
262 2         3 else { $out .= $char }
263             }
264             elsif($c->[0] eq ' ') {
265 10         11 $out .= $char;
266             }
267             elsif( $c->[0] eq 'l'
268             or $c->[0] eq 'L'
269             or $c->[0] eq 'r'
270             or $c->[0] eq 'R'
271             ) {
272 15         53 $out .= ($char)x($self->{widths}[$data_col]);
273 15         13 $data_col++;
274             }
275 28         39 $col++;
276             }
277 5         21 return $out."\n";
278             }
279              
280             sub _render_data($$)
281             {
282 8     8   15 my ($self,$data) = @_;
283              
284 8         7 my @rdata; # rendered data
285              
286             # render every column and find out number of lines
287 8         13 my ($col, $data_col) = (0,0);
288 8         10 my $lines=0;
289 8         19 my @rows_in_column;
290 8         9 for my $c (@{$self->{format}}) {
  8         15  
291 41 100 66     200 if( ($c->[0] eq 'l') or ($c->[0] eq 'L') ) {
    100 66        
292 20         43 my $lb = _l_box($self->{widths}[$data_col], $data->[$data_col]);
293 20         27 $rdata[$data_col] = $lb;
294 20         25 my $l = scalar @$lb ;
295 20 100       40 $lines = $l if $lines < $l;
296 20         23 $rows_in_column[$data_col] = $l;
297 20         26 $data_col++;
298             }
299             elsif( ($c->[0] eq 'r') or ($c->[0] eq 'R' ) ) {
300 3         9 my $rb = _r_box($self->{widths}[$data_col], $data->[$data_col]);
301 3         4 $rdata[$data_col] = $rb;
302 3         4 my $l = scalar @$rb ;
303 3 50       7 $lines = $l if $lines < $l;
304 3         4 $rows_in_column[$data_col] = $l ;
305 3         4 $data_col++;
306             }
307 41         61 $col++;
308             }
309              
310             # render each line
311 8         12 my $out = '';
312 8         18 for my $l (0..($lines-1)) {
313 16         30 my ($col, $data_col) = (0,0);
314 16         19 for my $c (@{$self->{format}}) {
  16         31  
315 89 100 33     385 if($c->[0] eq '|') {
    100 66        
    50          
    50          
316 11         11 $out .= '|';
317             }
318             elsif($c->[0] eq ' ') {
319 31         42 $out .= ' ';
320             }
321             elsif( $c->[0] eq 'L' or $c->[0] eq 'R')
322             {
323             # bottom align
324 0         0 my $start_print = $lines - $rows_in_column[$data_col];
325            
326 0 0 0     0 if ( defined $rdata[$data_col][$l-$start_print]
327             and $l >= $start_print
328             )
329             {
330 0         0 $out .= $rdata[$data_col][$l-$start_print];
331             }
332             else
333             {
334 0         0 $out .= ' 'x($self->{widths}[$data_col]);
335             }
336 0         0 $data_col++;
337             }
338             elsif($c->[0] eq 'l' or $c->[0] eq 'r') {
339             # top align
340 47 100       78 if(defined $rdata[$data_col][$l]) {
341 35         44 $out .= $rdata[$data_col][$l];
342             }
343             else {
344 12         23 $out .= ' 'x($self->{widths}[$data_col]);
345             }
346 47         53 $data_col++;
347             }
348 89         104 $col++;
349             }
350 16         30 $out .= "\n";
351             }
352 8         35 return $out;
353             }
354              
355             sub _parse_format($$)
356             {
357 4     4   6 my ($self, $format) = @_;
358 4         25 my @f = split(//, $format);
359 4         6 my @format = ();
360 4         7 my @width = ();
361            
362 4         6 my ($col,$data_col) = (0,0);
363 4         5 my $wid;
364 4         11 for my $f (@f) {
365 19 50       54 if ( $f =~ /(\d+)/)
366             {
367 0         0 $wid .= $f;
368 0         0 next;
369             }
370 19 100 66     122 if($f eq 'l' or $f eq 'L' or $f eq 'r' or $f eq 'R') {
    50 100        
      66        
      66        
371 11         24 $format[$col] = [$f, $data_col];
372 11         15 $width[$data_col] = $wid;
373 11         9 $wid = undef;
374 11         12 $data_col++;
375             }
376             elsif($f eq '|' or $f eq ' ') {
377 8         16 $format[$col] = [$f];
378             }
379             else {
380 0         0 croak "unknown column format: $f";
381             }
382 19         25 $col++;
383             }
384 4         12 $self->{format}=\@format;
385 4         7 $self->{fixed_widths}=\@width;
386 4         5 $self->{col}=$col;
387 4         14 $self->{data_col}=$data_col;
388             }
389              
390             =item B(I<$format>)
391              
392             Create a Text::FormatTable object, the format of each column is specified as a
393             character of the $format string. The following formats are defined:
394              
395             =over 4
396              
397             =item l
398              
399             Left-justified top aligned word-wrapped text.
400              
401             =item L
402              
403             Left-justified bottom aligned word-wrapped text.
404              
405             =item r
406              
407             Right-justified top aligned word-wrapped text.
408              
409             =item R
410              
411             Right-justified bottom aligned word-wrapped text.
412              
413             =item 10R, 20r, 15L, 12l,
414              
415             Number is fixed width of the column.
416             Justified and aligned word-wrapped text (see above).
417              
418             =item ' '
419              
420             A space.
421              
422             =item |
423              
424             Column separator.
425              
426             =back
427              
428             =cut
429              
430             sub new($$)
431             {
432 4     4 1 355 my ($class, $format) = @_;
433 4 50       11 croak "new() requires one argument: format" unless defined $format;
434 4         17 my $self = { col => '0', row => '0', data => [] };
435 4         10 bless $self, $class;
436 4         12 $self->_parse_format($format);
437 4         17 return $self;
438             }
439              
440             # remove head and trail space
441             sub _preprocess_row_data($$)
442             {
443 8     8   12 my ($self,$data) = @_;
444 8         9 my $cn = 0;
445 8         17 for my $c (0..($#$data)) {
446 23         39 $data->[$c] =~ s/^\s+//m;
447 23         47 $data->[$c] =~ s/\s+$//m;
448             }
449             }
450              
451             =item B(I<$col1>, I<$col2>, ...)
452              
453             Add a header row using $col1, $col2, etc. as cell contents. Note that, at the
454             moment, header rows are treated like normal rows.
455              
456             =cut
457              
458             sub head($@)
459             {
460 4     4 1 30 my ($self, @data) = @_;
461 4 50       14 scalar @data == $self->{data_col} or
462             croak "number of columns must be $self->{data_col}";
463 4         11 $self->_preprocess_row_data(\@data);
464 4         23 $self->{data}[$self->{row}++] = ['head', \@data];
465             }
466              
467             =item B(I<$col1>, I<$col2>, ...)
468              
469             Add a row with $col1, $col2, etc. as cell contents.
470              
471             =cut
472              
473             sub row($@)
474             {
475 4     4 1 18 my ($self, @data) = @_;
476 4 50       10 scalar @data == $self->{data_col} or
477             croak "number of columns must be $self->{data_col}";
478            
479 4         8 $self->_preprocess_row_data(\@data);
480 4         26 $self->{data}[$self->{row}++] = ['data', \@data];
481             }
482              
483             =item B([I<$char>])
484              
485             Add an horizontal rule. If $char is specified it will be used as character to
486             draw the rule, otherwise '-' will be used.
487              
488             =cut
489              
490             sub rule($$)
491             {
492 5     5 1 20 my ($self, $char) = @_;
493 5 100       12 $char = '-' unless defined $char;
494 5         19 $self->{data}[$self->{row}++] = ['rule', $char];
495             }
496              
497             =item B([I<$screen_width>])
498              
499             Return the rendered table formatted with $screen_width or 79 if it is not
500             specified.
501              
502             =cut
503              
504             sub render($$)
505             {
506 4     4 1 15 my ($self, $width) = @_;
507            
508 4 100       10 $width = 79 unless defined $width;
509 4         9 $self->_calculate_widths($width);
510            
511 4         5 my $out = '';
512 4         6 for my $r (@{$self->{data}}) {
  4         8  
513 13 100       39 if($r->[0] eq 'rule') {
    100          
    50          
514 5         14 $out .= $self->_render_rule($r->[1]);
515             }
516             elsif($r->[0] eq 'head') {
517 4         10 $out .= $self->_render_data($r->[1]);
518             }
519             elsif($r->[0] eq 'data') {
520 4         11 $out .= $self->_render_data($r->[1]);
521             }
522             }
523 4         14 return $out;
524             }
525              
526             1;
527              
528             =back
529              
530             =head1 SEE ALSO
531              
532             Text::ASCIITable
533              
534             =head1 COPYRIGHT
535              
536             Copyright (c) 2001-2004 Swiss Federal Institute of Technology, Zurich.
537             (c) 2009 Trey Harris
538             All Rights Reserved.
539              
540             This module is free software; you can redistribute it and/or
541             modify it under the same terms as Perl itself.
542              
543             =head1 CODE REPOSITORY
544              
545             Git - http://github.com/treyharris/Text-FormatTable/tree/master
546              
547             =head1 AUTHOR
548              
549             S>
550              
551             Maintained by S>
552              
553             Fixed column width and bottom alignment written by
554             S>
555              
556             =cut
557              
558             # vi: et sw=4