File Coverage

blib/lib/Text/SpanningTable.pm
Criterion Covered Total %
statement 126 129 97.6
branch 53 64 82.8
condition 9 16 56.2
subroutine 11 11 100.0
pod 8 8 100.0
total 207 228 90.7


line stmt bran cond sub pod time code
1             package Text::SpanningTable;
2             BEGIN {
3 2     2   4125064 $Text::SpanningTable::VERSION = '0.2';
4             }
5              
6 2     2   23 use warnings;
  2         5  
  2         61  
7 2     2   12 use strict;
  2         5  
  2         5395  
8              
9             # ABSTRACT: ASCII tables with support for column spanning.
10              
11             # this hash-ref holds the characters used to print the table decorations.
12             our $C = {
13             top => { # the top border, i.e. hr('top')
14             left => '.-',
15             border => '-',
16             sep => '-+-',
17             right => '-.',
18             },
19             middle => { # simple horizontal rule, i.e. hr('middle') or hr()
20             left => '+-',
21             border => '-',
22             sep => '-+-',
23             right => '-+',
24             },
25             dhr => { # double horizontal rule, i.e. hr('dhr') or dhr()
26             left => '+=',
27             border => '=',
28             sep => '=+=',
29             right => '=+',
30             },
31             bottom => { # bottom border, i.e. hr('bottom')
32             left => "'-",
33             border => '-',
34             sep => '-+-',
35             right => "-'",
36             },
37             row => { # row decoration
38             left => '| ',
39             sep => ' | ',
40             right => ' |',
41             },
42             };
43              
44             =head1 NAME
45              
46             Text::SpanningTable - ASCII tables with support for column spanning.
47              
48             =head1 VERSION
49              
50             version 0.2
51              
52             =head1 SYNOPSIS
53              
54             use Text::SpanningTable;
55              
56             # create a table object with four columns of varying widths
57             my $t = Text::SpanningTable->new(10, 20, 15, 25);
58              
59             # enable auto-newline adding
60             $t->newlines(1);
61            
62             # print a top border
63             print $t->hr('top');
64            
65             # print a row (with header information)
66             print $t->row('Column 1', 'Column 2', 'Column 3', 'Column 4');
67            
68             # print a double horizontal rule
69             print $t->dhr; # also $t->hr('dhr');
70              
71             # print a row of data
72             print $t->row('one', 'two', 'three', 'four');
73            
74             # print a horizontal rule
75             print $t->hr;
76            
77             # print another row, with one column that spans all four columns
78             print $t->row([4, 'Creedance Clearwater Revival']);
79            
80             # print a horizontal rule
81             print $t->hr;
82            
83             # print a row with the first column normally and another column
84             # spanning the remaining three columns
85             print $t->row(
86             'normal column',
87             [3, 'this column spans three columns and also wraps to the next line.']
88             );
89              
90             # finally, print the bottom border
91             print $t->hr('bottom');
92              
93             # the output from all these commands is:
94             .----------+------------------+-------------+-----------------------.
95             | Column 1 | Column 2 | Column 3 | Column 4 |
96             +==========+==================+=============+=======================+
97             | one | two | three | four |
98             +----------+------------------+-------------+-----------------------+
99             | Creedance Clearwater Revival |
100             +----------+------------------+-------------+-----------------------+
101             | normal | this column spans three columns and also wraps to the |
102             | | next line. |
103             '----------+------------------+-------------+-----------------------'
104              
105             =head1 DESCRIPTION
106              
107             C provides a mechanism for creating simple ASCII tables,
108             with support for column spanning. It is meant to be used with monospace
109             fonts such as common in terminals, and thus is useful for logging purposes.
110              
111             This module is inspired by L and can generally produce
112             the same output (except that C doesn't support column
113             spanning), but with a few key differences:
114              
115             =over
116              
117             =item * In C, you build your table in the object and
118             C it when you're done. In C, you can print
119             your table (or do whatever you want with the output) as it is being built.
120             If you don't need to have your tables in "real-time", you can just save the
121             output in a variable, but for convenience and compatibility with
122             C, this module provides a C method (which is
123             actually an alias for the C method) that returns the table's
124             output.
125              
126             =item * C takes care of the top and bottom borders of
127             the table by itself. Due to C's "real-time" nature,
128             this functionality is not provided, and you have to take care of that yourself.
129              
130             =item * C allows you to pass titles for a header column
131             when creating the table object. This module doesn't have that functionality,
132             you have to create header rows (or footer rows) yourself and how you see
133             fit.
134              
135             =item * C provides a second type of horizontal rules
136             (called 'dhr' for 'double horizontal rule') that can be used for header
137             and footer rows (or whatever you see fit).
138              
139             =item * C provides an option to define a callback
140             function that can be automatically invoked on the module's output when
141             calling C, C or C.
142              
143             =item * In C, the widths you define for the columns
144             are the widths of the data they can accommodate, i.e. without the borders
145             and padding. In C, the widths you define are WITH
146             the borders and padding. If you are familiar with the CSS and the box model,
147             then columns in C have C set to C,
148             while in C they have C set to C.
149             So take into account that the width of the column's data will be four
150             characters less than defined.
151              
152             =back
153              
154             Like C, the columns of the table will always be exactly
155             the same width as defined, i.e. they will not stretch to accommodate the
156             data passed to the cells. If a cell's data is too big, it will be wrapped
157             (with possible word-breaking using the '-' character), thus resulting in
158             more lines of text.
159              
160             =head1 METHODS
161              
162             =head2 new( [@column_widths] )
163              
164             Creates a new instance of C with columns of the
165             provided widths. If you don't provide any column widths, the table will
166             have one column with a width of 100 characters.
167              
168             =cut
169              
170             sub new {
171 3     3 1 501 my ($class, @cols) = @_;
172              
173 3         5 my $width; # total width of the table
174              
175             # default widths
176 3 100 100     14 @cols = (100) unless @cols and scalar @cols;
177              
178 3         7 foreach (@cols) {
179 8         12 $width += $_;
180             }
181              
182 3         21 return bless {
183             cols => \@cols,
184             width => $width,
185             newlines => 0,
186             output => [],
187             }, $class;
188             }
189              
190             =head2 newlines( [$boolean] )
191              
192             By default, trailing newlines will NOT be added automatically to the output generated
193             by this module (for example, when printing a horizontal rule, a newline
194             character will not be added). Pass a boolean value to this method to
195             enable/disable automatic newline creation. Returns the current value of
196             this attribute (after changing it if a boolean value has been passed).
197              
198             =cut
199              
200             sub newlines {
201 55 100   55 1 356 if (defined $_[1]) {
202 1         4 $_[0]->{newlines} = $_[1];
203             }
204              
205 55         226 return $_[0]->{newlines};
206             }
207              
208             =head2 exec( \&sub, [@args] )
209              
210             Define a callback function to be invoked whenever calling C, C
211             or C. Pass this method an anonymous subroutine (C<\&sub> above)
212             or a reference to a subroutine, and a list of parameters/arguments you
213             wish this subroutine to have (C<@args> above). When called, the subroutine
214             will receive, as arguments, the generated output, and C<@args>.
215              
216             So, for example, you can do:
217              
218             $t->exec(sub { my ($output, $log) = @_; $log->info($output); }, $log);
219            
220             This would result in C<< $log->info($output) >> being invoken whenever
221             calling C, C or C, with C<$output> being the output
222             these methods generated. See more info at the C's method documentation
223             below.
224              
225             =cut
226              
227             sub exec {
228 1     1 1 9 my $self = shift;
229              
230 1         3 $self->{exec} = shift;
231 1 50       7 $self->{args} = \@_ if scalar @_;
232             }
233              
234             =head2 hr( ['top'|'middle'|'bottom'|'dhr'] )
235              
236             Generates a horizontal rule of a certain type. Unless a specific type is
237             provided, 'middle' we be used. 'top' generates a top border for the table,
238             'bottom' generates a bottom border, and 'dhr' is the same as 'middle', but
239             generates a 'double horizontal rule' that is more pronounced and thus can
240             be used for headers and footers.
241              
242             This method will always result in one line of text.
243              
244             =cut
245              
246             sub hr {
247 13     13 1 738 my ($self, $type) = @_;
248              
249             # generate a simple horizontal rule by default
250 13   100     34 $type ||= 'middle';
251              
252             # start with the left decoration
253 13         30 my $output = $C->{$type}->{left};
254              
255             # print a border for every column in the table, with separator
256             # decorations between them
257 13         21 for (my $i = 0; $i < scalar @{$self->{cols}}; $i++) {
  56         130  
258 43         55 my $width = $self->{cols}->[$i] - 4;
259 43         80 $output .= $C->{$type}->{border} x$width;
260              
261             # print a separator unless this is the last column
262 43 100       35 $output .= $C->{$type}->{sep} unless $i == (scalar @{$self->{cols}} - 1);
  43         140  
263             }
264              
265             # right decoration
266 13         25 $output .= $C->{$type}->{right};
267              
268             # push this to the output buffer
269 13         14 push(@{$self->{output}}, $output);
  13         33  
270              
271             # are we adding newlines?
272 13 100       28 $output .= "\n" if $self->newlines;
273              
274             # if a callback function is defined, invoke it
275 13 100       32 if ($self->{exec}) {
276 8         11 my @args = ($output);
277 8 50       18 unshift(@args, @{$self->{args}}) if $self->{args};
  8         14  
278 8         18 $self->{exec}->(@args);
279             }
280              
281 13         57 return $output;
282             }
283              
284             =head2 dhr()
285              
286             Convenience method that simply calls C.
287              
288             =cut
289              
290             sub dhr {
291 1     1 1 6 shift->hr('dhr');
292             }
293              
294             =head2 row( @column_data )
295              
296             Generates a new row from an array holding the data for the row's columns.
297             At a maximum, the number of items in the C<@column_data> array will be
298             the number of columns defined when creating the object. At a minimum, it
299             will have one item. If the passed data doesn't fill the entire row, the
300             rest of the columns will be printed blank (so it is not structurally
301             incorrect to pass insufficient data).
302              
303             When a column doesn't span, simply push a scalar to the array. When it
304             does span, push an array-ref with two items, the first being the number
305             of columns to span, the second being the scalar data to print. Passing an
306             array-ref with 1 for the first item is the same as just passing the scalar
307             data (as the column will simply span itself).
308              
309             So, for example, if the table has nine columns, the following is a valid
310             value for C<@column_data>:
311              
312             ( 'one', [2, 'two and three'], 'four', [5, 'five through nine'] )
313              
314             The following is also valid:
315              
316             ( 'one', [5, 'two through six'] )
317              
318             Columns seven through nine in the above example will be blank, so it's the
319             same as passing:
320              
321             ( 'one', [5, 'two through six'], ' ', ' ', ' ' )
322              
323             If a column's data is longer than its width, the data will wrapped
324             and broken, which will result in the row being constructed from more than one
325             lines of text. Thus, as opposed to the C method, this method has
326             two options for a return value: in list context, it will return all the
327             lines constructing the row (with or without newlines at the end of each
328             string as per what was defined with the C); in scalar
329             context, however, it will return the row as a string containing newline
330             characters that separate the lines of text (once again, a trailing newline
331             will be added to this string only if a true value was passed to C).
332              
333             If a callback function has been defined, it will not be invoked with the
334             complete output of this row (i.e. with all the lines of text that has
335             resulted), but instead will be called once per each line of text. This is
336             what makes the callback function so useful, as it helps you cope with
337             problems resulting from all the newline characters separating these lines.
338             When the callback function is called on each line of text, the line will
339             only contain the newline character at its end if C has been
340             set to true.
341              
342             =cut
343              
344             sub row {
345 11     11 1 39 my ($self, @data) = @_;
346              
347 11         14 my @rows; # will hold a matrix of the table
348              
349 11         12 my $done = 0; # how many columns have we generated yet?
350              
351             # go over all columns provided
352 11         28 for (my $i = 0; $i < scalar @data; $i++) {
353             # is this a spanning column? what is the width of it?
354 24         24 my $width = 0;
355              
356 24         24 my $text = ''; # will hold column's text
357              
358 24 100       42 if (ref $data[$i] eq 'ARRAY') {
359             # this is a spanning column
360 6 50       15 $text .= $data[$i]->[1] if $data[$i]->[1];
361              
362 6         14 foreach (0 .. $data[$i]->[0] - 1) {
363             # $data[$i]->[0] is the number of columns this column spans
364 17         29 $width += $self->{cols}->[$done + $_];
365             }
366              
367             # subtract the number of columns this column spans
368             # minus 1, because two adjacent columns share the
369             # same separating border
370 6         10 $width -= $data[$i]->[0] - 1;
371            
372             # increase $done with the number of columns we have
373             # just parsed
374 6         8 $done += $data[$i]->[0];
375             } else {
376             # no spanning
377 18 50       40 $text .= $data[$i] if $data[$i];
378 18         25 $width = $self->{cols}->[$done];
379 18         20 $done++;
380             }
381              
382             # make sure the column's data is at least 4 characters long
383             # (because we're subtracting four from every column to make
384             # room for the borders and separators)
385 24 100       51 $text .= ' 'x(4 - length($text)) if length($text) < 4;
386            
387             # subtract four from the width, for the column's decorations
388 24         25 $width -= 4;
389              
390             # if the column's text is longer than the available width,
391             # we need to wrap it.
392 24         29 my $new_string = ''; # will hold parsed text
393 24 100       38 if (length($text) > $width) {
394 11   66     47 while (length($text) && length($text) > $width) {
395             # if the $width'th character of the string
396             # is a whitespace, just break it with a
397             # new line.
398            
399             # else if the $width'th - 1 character of the string
400             # is a whitespace, this is probably the start
401             # of a word, so add a whitespace and a newline.
402            
403             # else if the $width'th + 1 character is a whitespace,
404             # it is probably the end of a word, so just
405             # break it with a newline.
406            
407             # else we're in the middle of a word, so
408             # we need to break it with '-'.
409            
410            
411 43 100       201 if (substr($text, $width - 1, 1) =~ m/^\s$/) {
    100          
    100          
412 2         13 $new_string .= substr($text, 0, $width, '') . "\n";
413             } elsif (substr($text, $width - 2, 1) =~ m/^\s$/) {
414 8         39 $new_string .= substr($text, 0, $width - 1, '') . " \n";
415             } elsif (substr($text, $width, 1) =~ m/^\s$/) {
416 7         31 $new_string .= substr($text, 0, $width, '') . "\n";
417             } else {
418 26         124 $new_string .= substr($text, 0, $width - 1, '') . "-\n";
419             }
420             }
421 11 50       22 $new_string .= $text if length($text);
422             } else {
423 13         15 $new_string = $text;
424             }
425              
426             # if this row's data was split into more than one lines,
427             # we need to store these lines appropriately in our table's
428             # matrix (@rows).
429 24         64 my @fake_rows = split(/\n/, $new_string);
430 24         58 for (my $j = 0; $j < scalar @fake_rows; $j++) {
431 67 100       274 $rows[$j]->[$i] = ref $data[$i] eq 'ARRAY' ? [$data[$i]->[0], $fake_rows[$j]] : $fake_rows[$j];
432             }
433             }
434              
435             # suppose one column's data was wrapped into more than one lines
436             # of text. this means the matrix won't have data for all these
437             # lines in other columns that did not wrap (or wrapped less), so
438             # let's go over the matrix and fill missing cells with whitespace.
439 11         29 for (my $i = 1; $i < scalar @rows; $i++) {
440 34         36 for (my $j = 0; $j < scalar @{$self->{cols}}; $j++) {
  157         328  
441 123 100       204 next if $rows[$i]->[$j];
442            
443 80 100       157 if (ref $rows[$i - 1]->[$j] eq 'ARRAY') {
444 17         21 my $width = length($rows[$i - 1]->[$j]->[1]);
445 17         53 $rows[$i]->[$j] = [$rows[$i - 1]->[$j]->[0], ' 'x$width];
446             }
447             }
448             }
449              
450             # okay, now we go over the matrix and actually generate the
451             # decorated output
452 11         11 my @output;
453 11         24 for (my $i = 0; $i < scalar @rows; $i++) {
454 45         61 my $output = $C->{row}->{left};
455            
456 45         55 my $push = 0; # how many columns have we generated already?
457              
458             # print the columns
459 45         47 for (my $j = 0; $j < scalar @{$rows[$i]}; $j++) {
  150         280  
460 105         99 my $width = 0;
461 105         99 my $text;
462              
463 105 100       182 if (ref $rows[$i]->[$j] eq 'ARRAY') {
464             # a spanning column, calculate width and
465             # get the text
466 27         32 $text = $rows[$i]->[$j]->[1];
467 27         46 foreach (0 .. $rows[$i]->[$j]->[0] - 1) {
468 71         137 $width += $self->{cols}->[$push + $_];
469             }
470 27         40 $width -= $rows[$i]->[$j]->[0] - 1;
471             } else {
472             # normal column
473 78         91 $text = $rows[$i]->[$j];
474 78         115 $width = $self->{cols}->[$push];
475             }
476 105         173 $width -= 4;
477              
478             # is there any text for this column? if not just
479             # generate whitespace
480 105 100 66     412 $output .= $text && length($text) ? $text . ' 'x($width - length($text)) : ' 'x$width;
481              
482             # increase the number of columns we just processed
483 105 100       252 $push += ref $rows[$i]->[$j] eq 'ARRAY' ? $rows[$i]->[$j]->[0] : 1;
484              
485             # print a separator, unless this is the last column
486 105 100       91 $output .= $C->{row}->{sep} unless $push == (scalar @{$self->{cols}});
  105         308  
487             }
488              
489             # have we processed all columns? (i.e. has the user provided
490             # data for all the columns?) if not, generate empty columns
491 45         41 my $left = scalar @{$self->{cols}} - $push;
  45         70  
492            
493 45 100       72 if ($left) {
494 8         433 for (my $k = 1; $k <= $left; $k++) {
495 12         18 my $width = $self->{cols}->[$push++] - 4;
496 12         19 $output .= ' 'x$width;
497 12 100       35 $output .= $C->{row}->{sep} unless $k == $left;
498             }
499             }
500            
501 45         66 $output .= $C->{row}->{right};
502            
503 45         119 push(@output, $output);
504             }
505              
506             # save output in the object
507 11         11 push(@{$self->{output}}, @output);
  11         30  
508              
509             # invoke callback function, if any
510 11 100       26 if ($self->{exec}) {
511 7         8 my @args;
512 7 50       15 push(@args, @{$self->{args}}) if $self->{args};
  7         11  
513 7         8 foreach (@output) {
514 28 50 33     51 $_ .= "\n" if $self->newlines && !m/\n$/;
515 28         42 push(@args, $_);
516 28         71 $self->{exec}->(@args);
517 28         140 pop @args;
518             }
519             }
520              
521             # is the user expecting an array?
522 11 50       24 if (wantarray) {
523 0         0 foreach (@output) {
524 0 0 0     0 $_ .= "\n" if $self->newlines && !m/\n$/;
525             }
526 0         0 return @output;
527             } else {
528 11         29 my $output = join("\n", @output);
529 11 100       20 $output .= "\n" if $self->newlines;
530            
531 11         63 return $output;
532             }
533             }
534              
535             =head2 output()
536              
537             =head2 draw()
538              
539             Returns the entire output generated for the table up to the point of calling
540             this method. It should be stressed that this method does not "finalize"
541             the table by adding top and bottom borders or anything at all. Decoration
542             is done "real-time" and if you don't add top and bottom borders yourself
543             (with C and C, respectively), this method will
544             not do that for you. Returned output will or will not contain newlines as
545             per the value defined with C.
546              
547             Both the above methods do the same, C is provided as an alias for
548             compatibility with L.
549              
550             =cut
551              
552             sub output {
553 2     2 1 7 my $self = shift;
554              
555 2         3 my $output = join("\n", @{$self->{output}});
  2         7  
556 2 50       6 $output .= "\n" if $self->newlines;
557            
558 2         8 return $output;
559             }
560              
561             sub draw {
562 1     1 1 3 shift->output;
563             }
564              
565             =head1 AUTHOR
566              
567             Ido Perlmuter, C<< >>
568              
569             =head1 BUGS
570              
571             Please report any bugs or feature requests to C, or through
572             the web interface at L. I will be notified, and then you'll
573             automatically be notified of progress on your bug as I make changes.
574              
575             =head1 SUPPORT
576              
577             You can find documentation for this module with the perldoc command.
578              
579             perldoc Text::SpanningTable
580              
581             You can also look for information at:
582              
583             =over 4
584              
585             =item * RT: CPAN's request tracker
586              
587             L
588              
589             =item * AnnoCPAN: Annotated CPAN documentation
590              
591             L
592              
593             =item * CPAN Ratings
594              
595             L
596              
597             =item * Search CPAN
598              
599             L
600              
601             =back
602              
603             =head1 ACKNOWLEDGEMENTS
604              
605             Sebastian Riedel and Marcus Ramberg, authors of L, which
606             provided the inspiration of this module.
607              
608             =head1 LICENSE AND COPYRIGHT
609              
610             Copyright 2010 Ido Perlmuter.
611              
612             This program is free software; you can redistribute it and/or modify it
613             under the terms of either: the GNU General Public License as published
614             by the Free Software Foundation; or the Artistic License.
615              
616             See http://dev.perl.org/licenses/ for more information.
617              
618             =cut
619              
620             1;