File Coverage

blib/lib/Excel/Table.pm
Criterion Covered Total %
statement 245 253 96.8
branch 80 102 78.4
condition 4 6 66.6
subroutine 30 30 100.0
pod 9 10 90.0
total 368 401 91.7


line stmt bran cond sub pod time code
1             package Excel::Table;
2             #
3             # Excel::Table.pm - spreadsheet table processing class.
4             #
5             # This program is free software; you can redistribute it and/or modify
6             # it under the terms of the GNU General Public License as published
7             # by the Free Software Foundation; either version 2 of the License,
8             # or any later version.
9             #
10             # This program is distributed in the hope that it will be useful,
11             # but WITHOUT ANY WARRANTY; without even the implied warranty of
12             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13             # General Public License for more details.
14             #
15             # You should have received a copy of the GNU General Public License
16             # along with this program; if not, write to the Free Software
17             # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18             #
19             =head1 NAME
20              
21             Excel::Table
22              
23             =head1 SYNOPSIS
24              
25             use Excel::Table;
26              
27             my $xs = Excel::Table->new('dir' => '/cygdrive/c/Users/self/Desktop');
28              
29             for ($xs->list_workbooks) {
30             print "workbook [$_]\n";
31             }
32              
33             $xs->open('mybook.xls');
34              
35             my $wb1 = $xs->open_re('foo*bar*');
36              
37             for my $worksheet ($wb1->worksheets) {
38             print "worksheet: " . $worksheet->get_name() . "\n";
39             }
40              
41             $xs->null("this is a null value");
42             $xs->force_null(1);
43              
44             $xs->rowid(0);
45              
46             $xs->trim(0);
47              
48             my @data = $xs->extract('Sheet1');
49              
50             for (@data) {
51             printf "rowid [%s] title [%s] max_width [%d] value [%s]\n",
52             $_->[0],
53             $xs->titles->[0],
54             $xs->widths->[0],
55             $data{$_}->[0];
56             }
57              
58             @data = $xs->extract_hash('Sheet1');
59              
60             @data = $xs->select("column1,column2,column3", 'Sheet1');
61              
62             @data = $xs->select_hash("column1,column2,column3", 'Sheet1');
63              
64             printf "columns %d rows %d title_row %d\n",
65             $xs->columns, $xs->rows, $xs->title_row;
66              
67             printf "regexp [%s] pathname [%s] sheet_name [%s]\n",
68             $xs->regexp, $xs->pathname, $xs->sheet_name;
69              
70             printf "colid2title(0) = [%s]\n", $xs->colid2title(0);
71              
72             printf "title2colid('Foo') = %d\n", $xs->title2colid('Foo');
73              
74             =head1 DESCRIPTION
75              
76             Excel::Table.pm - spreadsheet table processing. Retrieves worksheets as
77             if they are structured tables array-format.
78              
79             =over 4
80              
81             =item 1a. OBJ->dir(EXPR)
82              
83             Override the directory location in which to look for workbooks.
84             Defaults to "." (i.e. the current working directory).
85             This location is critical to the B, B,
86             and B methods.
87              
88             =item 1b. OBJ->list_workbooks
89              
90             Returns an array of workbook files in the directory defined by the
91             B property.
92              
93             =item 2a. OBJ->open(EXPR)
94              
95             Parses the filename specified by EXPR. The B property
96             will designate the search path.
97             Once opened, via this method (or B) the
98             workbook is available for use by the B method.
99              
100             =item 2b. OBJ->open_re(EXPR)
101              
102             This will search for a file which has a filename matching the regexp EXPR.
103             A warning will be issued if multiple matches are found, only the first will
104             be opened.
105              
106             =item 3. OBJ->regexp
107              
108             Returns the regexp used to search for the workbook on the filesystem.
109              
110             =item 4. OBJ->pathname
111              
112             Returns the pathname of the opened workbook.
113              
114             =item 5a. OBJ->extract(EXPR,[TITLE_ROW])
115              
116             This will extract all data from the worksheet named EXPR. Data is extracted
117             into an array and returned. Format of data is per below:
118              
119             [ value1, value2, value3, ... ],
120             [ value1, value2, value3, ... ],
121             [ value1, value2, value3, ... ],
122             ...
123              
124             The object OBJ will be populated with various properties to assist you to
125             access the data in the array, including column titles and widths.
126              
127             A worksheet object is temporarily created in order to populate the array.
128             Once a worksheet is extracted, the associated worksheet object is destroyed.
129             This routine can be called again on any worksheet in the workbook.
130              
131             If the TITLE_ROW argument is specified, then the B property will
132             also be updated prior to extraction.
133              
134             =item 5b. OBJ->extract_hash(EXPR,[TITLE_ROW])
135              
136             Per the B method, but returns an array of hashes, with the hash
137             keys corresponding to the titles.
138              
139             =item 5c. OBJ->select(CLAUSE,EXPR,[TITLE_ROW])
140              
141             Similar to the B method, this will extract all rows from the worksheet EXPR, constraining the columns to those specified by the B argument,
142             which is a comma-separated string, e.g. "column1,column2,column3".
143              
144             As with the B method, the B and B properties will
145             be revised.
146              
147             =item 5d. OBJ->select_hash(CLAUSE,EXPR,[TITLE_ROW])
148              
149             Per the B
150              
151             =item 6. OBJ->columns or OBJ->rows
152              
153             Returns the number of columns or rows available in the sheet extracted via the
154             B method.
155              
156             =item 7a. OBJ->force_null
157              
158             Flag which determines if whitespace fields should be
159             replaced by specific text (see OBJ->null).
160              
161             =item 7b. OBJ->null
162              
163             String to replace whitespace fields with. Defaults to "(null)".
164              
165             =item 8. OBJ->rowid
166              
167             Flag which determines whether a pseudo-column "rowid" is included in each
168             tuple. The value will take the form "999999999" Defaults to FALSE.
169              
170             =item 9. OBJ->sheet_name
171              
172             Returns the sheet_name against which data was extracted via B.
173              
174             =item 10. OBJ->trim
175              
176             Flag which determines if trailing whitespace fields should be trimmed.
177              
178             =item 11a. OBJ->title_row
179              
180             Returns the title row of the worksheet (defaults to zero), following extract.
181              
182             =item 11b. OBJ->titles
183              
184             Returns an array of title fields, the title row number having been defined
185             as OBJ->title_row.
186              
187             =item 11c. OBJ->colid2title(colid)
188              
189             Converts the column number (colid) to a string column title (i.e.
190             the offset within the title_row array).
191             If no match, then returns undef.
192              
193             =item 11d. OBJ->title2colid(REGEXP)
194              
195             Returns the column number of the title identified by REGEXP.
196             If no match, then returns undef.
197              
198             =item 12. OBJ->widths
199              
200             Returns an array of maximum lengths of any (non-title) data in each column.
201              
202             =back
203              
204             =cut
205              
206 8     8   820691 use strict;
  8         22  
  8         315  
207 8     8   43 use warnings;
  8         15  
  8         220  
208              
209 8     8   49 use Data::Dumper;
  8         19  
  8         562  
210 8     8   12533 use Spreadsheet::ParseExcel 0.57;
  8         680410  
  8         259  
211 8     8   7962 use Spreadsheet::XLSX;
  8         751607  
  8         266  
212 8     8   115 use File::Basename;
  8         17  
  8         575  
213              
214 8     8   47 use Carp qw(cluck confess); # only use stack backtrace within class
  8         15  
  8         432  
215 8     8   39 use Log::Log4perl qw/ get_logger /;
  8         17  
  8         93  
216              
217 8     8   450 use vars qw/ @EXPORT $VERSION /;
  8         14  
  8         485  
218              
219             $VERSION = "1.020"; # update this on new release
220              
221             #@ISA = qw(Exporter);
222             #@EXPORT = qw();
223              
224             # package constants
225 8     8   106 use constant S_RID => "rowid";
  8         17  
  8         496  
226 8     8   40 use constant S_NULL => "(null)";
  8         11  
  8         365  
227 8         505 use constant EXT_EXCEL => qw/
228             \.xls \.xla \.xlb \.xlc \.xld \.xlk \.xll \.xlm \.xlt
229             \.xlv \.xlw \.xls \.xlt
230 8     8   34 /; # known extensions for EXCEL file
  8         14  
231              
232             # need the Spreadsheet::XLSX module for the following:
233 8         40357 use constant EXT_EXCEL_2007 => qw/
234             \.xlsx \.xlsm \.xlsb \.xltm \.xlam
235 8     8   38 /; # known extensions for EXCEL 2007 file
  8         13  
236              
237              
238             # package globals
239              
240             our $AUTOLOAD;
241              
242              
243             # package locals
244             my $n_Objects = 0; # counter of objects created.
245              
246             my %attribute = (
247             _n_objects => \$n_Objects,
248             _xl_vers => undef,
249             columns => undef,
250             dir => ".",
251             _log => get_logger("Excel::Table"),
252             null => S_NULL,
253             pathname => undef,
254             regexp => undef,
255             force_null => 0,
256             rows => undef,
257             rowid => 0,
258             sheet_name => undef,
259             title_row => 0, # if title row is zero, first data row is 1
260             titles => undef,
261             trim => 0,
262             widths => undef,
263             workbook => undef,
264             );
265              
266              
267             #INIT { };
268              
269              
270             sub AUTOLOAD {
271 18959     18959   715694 my $self = shift;
272 18959 50       37970 my $type = ref($self) or croak("self is not an object");
273              
274 18959         23430 my $name = $AUTOLOAD;
275 18959         57843 $name =~ s/.*://; # strip fully−qualified portion
276              
277 18959 50       46620 unless (exists $self->{_permitted}->{$name} ) {
278 0         0 confess "no attribute [$name] in class [$type]";
279             }
280              
281 18959 100       29374 if (@_) {
282 328         984 return $self->{$name} = shift;
283             } else {
284 18631         70189 return $self->{$name};
285             }
286             }
287              
288              
289             sub new {
290 23     23 0 42381 my ($class) = shift;
291             #my $self = $class->SUPER::new(@_);
292 23         423 my $self = { _permitted => \%attribute, %attribute };
293              
294 23         71 ++ ${ $self->{_n_objects} };
  23         62  
295              
296 23         63 bless ($self, $class);
297              
298 23         73 my %args = @_; # start processing any parameters passed
299 23         37 my ($method,$value); # start processing any parameters passed
300 23         119 while (($method, $value) = each %args) {
301              
302 31 50       76 confess "SYNTAX new(method => value, ...) value not specified"
303             unless (defined $value);
304              
305 31         207 $self->_log->debug("method [self->$method($value)]");
306              
307 31         385 $self->$method($value);
308             }
309            
310 23         87 return $self;
311             }
312              
313              
314             sub _determine_xl_vers {
315 264     264   394 my ($self,$pn)=@_;
316 264 50       553 $self->_log->logcroak("SYNTAX: _determine_xl_vers(path)")
317             unless defined ($pn);
318             # return version string or undef for given pathname
319 264         258 my $extension;
320             my @extensions;
321 264         323 my $retval = undef;
322              
323 264         1064 $self->_log->debug("pn [$pn]");
324              
325 264         2362 @extensions = EXT_EXCEL;
326 264         27715 (undef,undef,$extension) = fileparse($pn,@extensions);
327             #$self->_log->debug(sprintf " extension [%s] \@extensions [%s]", $extension, Dumper(\@extensions));
328              
329 264 100       746 $retval = 'xl2003' if ($extension ne "");
330              
331 264         854 @extensions = EXT_EXCEL_2007;
332 264         12866 (undef,undef,$extension) = fileparse($pn,@extensions);
333              
334 264 100       759 $retval = 'xl2007' if ($extension ne "");
335              
336 264 100       528 if (defined $retval) {
337 56         235 $self->_log->debug("pn [$pn] returning [$retval]");
338             }
339              
340 264         1693 return $retval;
341             }
342              
343              
344             sub list_workbooks {
345 13     13 1 1519 my $self = shift;
346              
347 13         65 my $dn = $self->dir;
348 13         21 my ($dh,$fn);
349 0         0 my @workbooks;
350              
351 13         54 $self->_log->debug("dn [$dn]");
352              
353 13 50       946 opendir($dh, $dn) || $self->_log->logcroak("opendir($dn)");
354              
355 13         257 while ($fn = readdir($dh)) {
356 234         2008 my $pn = File::Spec->catfile($dn, $fn);
357              
358             # need to remember just the filename here, not the path because
359             # open will use the self->dir property to make the path
360 234 100       634 push @workbooks, $fn
361             if (defined($self->_determine_xl_vers($pn)));
362             }
363              
364 13         293 closedir $dh;
365              
366 13         68 $self->_log->debug(sprintf '@workbooks [%s]', Dumper(\@workbooks));
367              
368 13         1603 return @workbooks;
369             }
370              
371              
372             sub open {
373 30     30 1 10481 my ($self,$fn)=@_;
374 30 50       113 $self->_log->logcroak("SYNTAX: open(file)") unless defined ($fn);
375 30         290 my $pn = File::Spec->catfile($self->dir, $fn);
376              
377             # to look for the file in the cwd, is not good behaviour,
378             # so dir must be explicit, thus the default to "."
379              
380 30 50       974 if (-f $pn) {
381 30         154 $self->pathname($pn);
382             } else {
383 0         0 $self->_log->logcroak("no such path [$pn]");
384             }
385              
386 30         205 $self->_log->debug("parsing [$pn]");
387              
388 30         321 $self->{'_xl_vers'} = $self->_determine_xl_vers($pn);
389              
390 30         51 my $parser;
391              
392 30 100       242 if ($self->_xl_vers eq 'xl2007') {
393 13         192 $parser = Spreadsheet::XLSX->new($pn);
394              
395 13 50       463227 $self->_log->logcroak("Spreadsheet::XLSX->new($pn) failed")
396             unless defined $parser;
397              
398 13         144 $self->workbook($parser);
399             } else {
400 17         196 $parser = Spreadsheet::ParseExcel->new();
401 17         38517 $self->workbook($parser->Parse($pn));
402              
403 17 50       795 $self->_log->logcroak("Parse() failed, error: " . $self->workbook->error())
404             unless defined $self->workbook;
405             }
406              
407              
408 30         189 return $self->workbook;
409             }
410              
411              
412             sub open_re {
413 7     7 1 4455 my $self = shift;
414 7 50       28 if (@_) { $self->regexp(shift); } else { $self->_log->logcroak("SYNTAX: open_re(regexp)"); }
  7         45  
  0         0  
415 7         67 my $re = $self->regexp;
416 7         16 my $matches = 0;
417 7         11 my $wb = undef;
418              
419 7         34 $self->_log->debug(sprintf "regexp [%s]", $self->regexp);
420 7         69 for ( $self->list_workbooks ) {
421 14         58 $self->_log->debug(" file [$_]");
422 14 100       171 if ($_ =~ /$re/) {
423 10         38 $self->_log->debug(" FOUND [$_]");
424 10 100       90 $wb = $_ unless ($matches++); # remember first occurence
425             }
426             }
427              
428 7 100       22 unless (defined $wb) {
429 1         5 $self->_log->logcarp("could not find file matching [$re]");
430 1         598 return undef;
431             }
432              
433 6 100       32 $self->_log->logwarn("non-unique match on [$re]")
434             if ($matches > 1);
435              
436 6         1837 return $self->open($wb);
437             }
438              
439              
440             sub _prepend_rowid {
441 110     110   161 my ($self, $ra_columns, $id)=@_;
442              
443 110 100       569 my $rowid = ($id == $self->title_row) ? S_RID : sprintf "%09d", $id;
444              
445 110         275 push @$ra_columns, $rowid;
446              
447 110         147 return $rowid;
448             }
449              
450              
451             sub extract {
452 36     36 1 6207 my $self = shift;
453 36 50       132 if (@_) { $self->sheet_name(shift); }
  36         215  
454 36 50       112 if (@_) { $self->title_row(shift); }
  0         0  
455              
456 36 50 33     154 $self->_log->logcroak("SYNTAX: extract(sheet_name,title_row)")
457             unless (defined $self->sheet_name && defined $self->title_row);
458              
459 36         184 $self->_log->debug(sprintf "opening [%s]", $self->sheet_name);
460              
461 36         535 my $ws = $self->workbook->worksheet($self->sheet_name);
462              
463 36         585 my ($minr, $maxr) = $ws->row_range();
464 36         449 my ($minc, $maxc) = $ws->col_range();
465              
466 36         410 $self->rows($maxr);
467 36         188 $self->columns($maxc + 1);
468              
469 36 100       140 $self->title_row($minr) # fix minimum row
470             if ($self->title_row < $minr);
471              
472 36         141 $self->_log->debug(sprintf "sheet_name [%s] minr [%d] maxr [%d] minc [%d] maxc [%d]",
473             $self->sheet_name, $minr, $maxr, $minc, $maxc);
474              
475 36         301 my ($subr,$subc,$value);
476 0         0 my @data;
477 0         0 my (@columns,@widths);
478              
479 36         152 for ($subr = $self->title_row; $subr <= $maxr; $subr++) {
480              
481 368 100       1239 $self->_prepend_rowid(\@columns, $subr)
482             if ($self->rowid);
483              
484 368         1008 for ($subc = $minc; $subc <= $maxc; $subc++) {
485              
486 3584         15546 my $cell = $ws->get_cell($subr, $subc);
487              
488 3584 100       35848 if (defined $cell) {
489 3552 100       11931 $value = ($self->trim) ? $self->_trim_whitespace($cell->value) : $cell->value;
490             } else {
491 32         58 $value = undef;
492             }
493              
494 3584 100       18566 $value = $self->_resolve_null($value, $self->null)
495             if ($self->force_null);
496              
497 3584         10202 push @columns, $value;
498             }
499              
500             # adjust widths, including rowid column
501 368         410 $subc = 0;
502 368         504 for $value (@columns) {
503              
504             # calculate width, ignoring title_row
505              
506 3694 100       12139 if ($subr == $self->title_row) {
507 346         504 $widths[$subc] = 0;
508             } else {
509 3348 100 100     13760 $widths[$subc] = length($value)
510             if (defined($value) &&
511             length($value) > $widths[$subc]);
512             }
513              
514 3694         5347 $subc++;
515             }
516              
517 368         1233 $self->_log->debug(sprintf '@columns [%s]', Dumper(\@columns));
518 368         33456 $self->_log->debug(sprintf '@widths [%s]', Dumper(\@widths));
519            
520 368 100       28904 if ($subr == $self->title_row) {
521 36         221 $self->titles([ @columns ]);
522             } else {
523 332         1103 push @data, [ @columns ];
524             }
525 368         1352 @columns = ();
526             }
527 36         316 $self->widths([ @widths ]);
528              
529 36         108 @widths = $ws = ();
530              
531 36         344 return @data;
532             }
533              
534              
535             sub colid2title {
536 4     4 1 12 my ($self,$colid)=@_;
537              
538 4 50       16 $self->_log->logcroak("SYNTAX: colid2title2(colid)")
539             unless (defined $colid);
540              
541 4         29 $self->_log->debug("colid [$colid]");
542              
543             return undef
544 4 50       44 if ($colid < 0);
545              
546             return undef
547 4 100       6 unless ($colid < scalar @{ $self->titles });
  4         18  
548              
549 2         11 return $self->titles->[$colid];
550             }
551              
552              
553             sub title2colid {
554 44     44 1 92 my ($self,$title)=@_;
555              
556 44 50       99 $self->_log->logcroak("SYNTAX: title2colid(title)")
557             unless (defined $title);
558              
559 44         159 $self->_log->debug("title [$title] ");
560              
561 44         273 my $tmax = scalar @{ $self->titles };
  44         159  
562              
563 44         128 for (my $tsub = 0; $tsub < $tmax; $tsub++) {
564 294 100       1101 if ($self->titles->[$tsub] =~ /$title/) {
565 30         112 $self->_log->debug("match at colid $tsub");
566 30         245 return $tsub;
567             }
568             }
569 14         62 $self->_log->debug("NO MATCH");
570              
571 14         116 return undef;
572             }
573              
574              
575             sub _trim_whitespace {
576 1526     1526   7113 my ($self,$s_value)=@_;
577              
578 1526 50       2796 if (defined $s_value) {
579 1526         4755 $self->_log->debug("s_value [$s_value]");
580              
581 1526         10551 $s_value =~ s/^[[:cntrl:][:space:]]+//; # trim leading
582 1526         2540 $s_value =~ s/[[:cntrl:][:space:]]+$//; # trim trailing
583              
584 1526         5012 $self->_log->debug("after s_value [$s_value]");
585             }
586              
587 1526         10757 return $s_value;
588             }
589              
590              
591             sub _resolve_null {
592 440     440   591 my ($self, $s_value, $s_null)=@_;
593              
594 440 100       1347 $self->_log->debug(sprintf "s_value [%s] s_null [%s]",
    50          
595             (defined $s_value) ? $s_value : "not defined",
596             (defined $s_null) ? $s_null : "not defined",
597             );
598              
599 440 100       2677 if (defined $s_value) {
600 436 100       807 $s_value = $s_null
601             if ($s_value eq "");
602             } else {
603 4         6 $s_value = $s_null;
604             }
605              
606 440         753 return $s_value;
607             }
608              
609              
610             sub _array_to_hash {
611 4     4   8 my $self = shift;
612 4         8 my @data;
613              
614 4         8 for my $row (@_) {
615 40         138 $self->_log->debug(sprintf '$row [%s]', Dumper($row));
616              
617 40         2510 my %data;
618 40         51 my $unique = 0;
619 40         59 my $m_value = scalar(@$row);
620              
621 40         89 for (my $ss_value = 0; $ss_value < $m_value; $ss_value++) {
622              
623 260         829 my $column = $self->titles->[$ss_value];
624 260         369 my $value = $row->[$ss_value];
625              
626 260 100       460 my $key = (exists $data{$column}) ? $column . $unique++ : $column;
627 260         808 $data{$key} = $value;
628             }
629              
630 40         126 $self->_log->debug(sprintf 'data [%s]', Dumper(\%data));
631              
632 40         3006 push @data, { %data };
633              
634 40         141 %data = ();
635             }
636              
637 4         26 return @data;
638             }
639              
640              
641             sub extract_hash {
642 2     2 1 5 my $self = shift;
643              
644 2 50       7 $self->_log->logcroak("SYNTAX: extract_hash(sheet_name,[title_row])")
645             unless (defined @_);
646              
647 2         10 return $self->_array_to_hash($self->extract(@_));
648             }
649              
650              
651             sub select_hash {
652 2     2 1 9780 my $self = shift;
653 2         6 my $clause = shift;
654              
655 2 50       8 $self->_log->logcroak("SYNTAX: select_hash(clause,[sheet_name,title_row])")
656             unless (defined @_);
657              
658 2         10 return $self->_array_to_hash($self->select($clause, @_));
659             }
660              
661              
662             sub select {
663 14     14 1 11407 my $self = shift;
664 14         27 my $clause = shift;
665              
666 14 50       49 $self->_log->logcroak("SYNTAX: select(clause,[sheet_name,title_row])")
667             unless (defined $clause);
668              
669 14         58 my @pre = $self->extract(@_);
670 14         34 my (@post, @id);
671 0         0 my (@columns, @widths);
672              
673 14 100       58 $clause = join(',', S_RID, $clause)
674             if ($self->rowid);
675              
676 14         85 for my $column (split(/,/, $clause)) {
677 40         3212 $self->_log->debug("column [$column]");
678              
679 40         306 my $id = $self->title2colid($column);
680              
681 40 100       92 if (defined $id) {
682 28         45 push @id, $id;
683 28         35 push @columns, $column;
684 28         99 push @widths, $self->widths->[$id];
685             } else {
686 12         50 $self->_log->logwarn("invalid column [$column]");
687             }
688             }
689              
690 14         972 $self->_log->debug(sprintf '@id [%s]', Dumper(\@id));
691              
692 14 100       987 my $f_no_columns = ($self->rowid) ? 1 : 0;
693              
694 14 100       50 unless (scalar(@columns) == $f_no_columns) { # no columns, thus no rows
695 10         23 for my $row (@pre) {
696 100         626 $self->_log->debug(sprintf 'row [%s]', Dumper($row));
697              
698 100         7604 my @wanted = ();
699              
700 100         164 for my $id (@id) {
701 260         453 push @wanted, $row->[$id];
702             }
703              
704 100         380 $self->_log->debug(sprintf '@wanted [%s]', Dumper(\@wanted));
705              
706 100 50       11277 push @post, [ @wanted ]
707             if (scalar(@wanted)); # account for null case
708             }
709             }
710              
711 14         64 $self->_log->debug(sprintf '@columns [%s]', Dumper(\@columns));
712 14         791 $self->_log->debug(sprintf '@widths [%s]', Dumper(\@widths));
713              
714 14         799 $self->titles([ @columns ]);
715 14         75 $self->widths([ @widths ]);
716              
717 14         237 return @post;
718             }
719              
720              
721             DESTROY {
722 23     23   81642 my $self = shift;
723 23         38 -- ${ $self->{_n_objects} };
  23         3014  
724             };
725              
726             #END { }
727              
728             1;
729              
730             __END__