File Coverage

blib/lib/RapidApp/Spreadsheet/ExcelTableWriter.pm
Criterion Covered Total %
statement 24 109 22.0
branch 0 36 0.0
condition 0 3 0.0
subroutine 8 22 36.3
pod 6 12 50.0
total 38 182 20.8


line stmt bran cond sub pod time code
1             package RapidApp::Spreadsheet::ExcelTableWriter;
2              
3             =head1 ExcelTableWriter
4              
5             Convenience object for writing a table into an Excel worksheet.
6              
7             ExcelTableWriter does not manage the excel file, and instead takes parameters of the
8             workbook and worksheet objects to use. This allows quite a bit of flexibility.
9              
10             my $xls= Excel::Writer::XLSX->new($fh);
11            
12             my $tw= RapidApp::Spreadsheet::ExcelTableWriter->new(
13             wbook => $xls,
14             wsheet => $xls->add_worksheet("MyData"),
15             columns => [ 'Foo', 'Bar', 'Baz' ]
16             );
17            
18             my $tw= RapidApp::Spreadsheet::ExcelTableWriter->new(
19             wbook => $xls,
20             wsheet => $xls->add_worksheet("MyData"),
21             columns => [
22             { name => 'foo_1', label => 'Foo', isString => 0 },
23             { name => 'bar', label => 'Bar', format => $xls->add_format(bold => 1) },
24             { name => 'baz', label => 'BAAAAZZZZZ!' },
25             ],
26             headerFormat => $xls->add_format(bold => 1, underline => 1, italic => 1),
27             );
28            
29             $tw->writePreamble("Some descriptive text at the top of the file");
30             $tw->writePreamble;
31             $tw->writeHeaders; # optional so long as writeRow gets called
32            
33             $tw->writeRow(1, 'John Doe', '1234 Reading Rd');
34             $tw->writeRow( [ 2, 'Bob Smith', '1234 Eagle Circle');
35             $tw->writeRow( { foo_1 => 3, bar => 'Rand AlThor', baz => 'Royal Palace, Cairhien' } );
36            
37             $tw->autosizeColumns;
38              
39             =cut
40              
41 4     4   28 use strict;
  4         12  
  4         122  
42 4     4   21 use warnings;
  4         9  
  4         100  
43 4     4   26 use Moose;
  4         8  
  4         54  
44              
45 4     4   27812 use Spreadsheet::ParseExcel;
  4         148647  
  4         141  
46 4     4   1899 use RapidApp::Spreadsheet::ExcelTableWriter::ColDef;
  4         15  
  4         980  
47              
48             has 'wbook' => ( is => 'ro', isa => 'Excel::Writer::XLSX::Workbook', required => 1 );
49             has 'wsheets' => ( is => 'ro', isa => 'ArrayRef', required => 1 );
50             has 'columns' => ( is => 'rw', isa => 'ArrayRef', required => 1 );
51             has 'rowStart' => ( is => 'rw', isa => 'Int', required => 1, default => 0 );
52             has 'colStart' => ( is => 'rw', isa => 'Int', required => 1, default => 0 );
53             has 'headerFormat' => ( is => 'rw', lazy_build => 1 );
54             has 'ignoreUnknownRowKeys' => ( is => 'rw', isa => 'Bool', default => 0 );
55              
56             sub _build_headerFormat {
57 0     0     my $self= shift;
58 0           return $self->wbook->add_format(bold => 1, bottom => 1);
59             }
60              
61             sub colCount {
62 0     0 0   my $self= shift;
63 0           return scalar(@{$self->columns});
  0            
64             }
65              
66             around 'BUILDARGS' => sub {
67             my $orig= shift;
68             my $class= shift;
69             my $args= $class->$orig(@_);
70             if (defined $args->{wsheet}) {
71             $args->{wsheets}= [ $args->{wsheet} ];
72             delete $args->{wsheet};
73             }
74             return $args;
75             };
76              
77             sub numWsRequired($) {
78 0     0 0   my ($unused, $numCols)= @_;
79 4     4   37 use integer;
  4         8  
  4         40  
80 0           return ($numCols+255) / 256;
81             }
82              
83             sub BUILD {
84 0     0 0   my $self= shift;
85            
86 0           my $numWsNeeded= $self->numWsRequired(scalar(@{$self->columns}));
  0            
87 0           $numWsNeeded <= scalar(@{$self->wsheets})
88 0 0         or die "Not enough worksheets allocated for ExcelTableWriter (got ".scalar(@{$self->wsheets}).", require $numWsNeeded)";
  0            
89            
90 0           for (my $i= 0; $i < scalar(@{$self->columns}); $i++) {
  0            
91 0           my $val= $self->columns->[$i];
92             # convert hashes into the proper object
93 0 0         ref $val eq 'HASH' and
94             $self->columns->[$i]= RapidApp::Spreadsheet::ExcelTableWriter::ColDef->new($val);
95             # convert scalars into names (and labels)
96 0 0         ref $val eq '' and
97             $self->columns->[$i]= RapidApp::Spreadsheet::ExcelTableWriter::ColDef->new(name => $val);
98             }
99             }
100              
101             =head2 curRow
102              
103             Returns the next row that will be written by a call to writePreamble, writeHeadrs, or writeRow.
104              
105             This value is read-only
106              
107             =cut
108            
109             sub curRow {
110 0     0 1   my $self= shift;
111 0 0         defined $self->{_curRow} and return $self->{_curRow};
112 0           return $self->rowStart;
113             }
114              
115             has '_documentStarted' => ( is => 'rw' );
116             has '_dataStarted' => ( is => 'rw' );
117              
118             =head2 excelColIdxToLetter
119              
120             print RapidApp::Spreadsheet::ExcelTableWriter->excelColIdxToLetter(35);
121             # prints AM
122             print $tableWriter->excelColIdxToLetter(0);
123             # prints A
124              
125             =cut
126              
127 4     4   1028 use Spreadsheet::ParseExcel::Utility 'int2col';
  4         16  
  4         475  
128              
129             sub excelColIdxToLetter($) {
130 0     0 1   my ($ignored, $colNum)= @_;
131 0           return int2col($colNum);
132             }
133              
134             sub sheetForCol {
135 0     0 0   my ($self, $colIdx)= @_;
136 4     4   35 use integer;
  4         8  
  4         17  
137 0           $colIdx+= $self->colStart;
138 0           return $self->wsheets->[$colIdx / 256], $colIdx%256;
139             }
140              
141             sub _applyColumnFormats {
142 0     0     my $self= shift;
143            
144 0           for (my $i=0; $i < $self->colCount; $i++) {
145 0           my $fmt= $self->columns->[$i]->format;
146 0 0         my $wid= $self->columns->[$i]->width eq 'auto'? undef : $self->columns->[$i]->width;
147            
148 0           my ($wsheet, $sheetCol)= $self->sheetForCol($i);
149 0           $wsheet->set_column($sheetCol, $sheetCol, $wid, $fmt);
150             }
151             }
152              
153             sub prepareDocument {
154 0     0 0   my $self= shift;
155 0 0         !$self->_documentStarted or die 'column formats can only be applied before the first "write"';
156            
157 0           $self->_applyColumnFormats();
158 0           $self->_documentStarted(1);
159             }
160              
161             =head2 writePreamble
162              
163             writePreamble writes each of its arguments into an Excel cell from left to right, and then
164             increments the current row.
165              
166             The only purpose of this routine is to conveniently increment the starting row while writing
167             various bits of text at the start of the worksheet.
168              
169             =cut
170              
171             sub writePreamble {
172 0     0 1   my ($self, @args)= @_;
173 0 0         !$self->_dataStarted or die 'Preamble must come before headers and data';
174            
175 0 0         $self->_documentStarted or $self->prepareDocument;
176 0           for (my $i=0; $i < scalar(@args); $i++) {
177 0           my ($ws, $wsCol)= $self->sheetForCol($i);
178 0           $ws->write($self->curRow, $wsCol, $args[$i]);
179             }
180 0           $self->{_curRow}++;
181             }
182              
183             =head2 writeHeaders
184              
185             writeHeaders takes no parameters and returns nothing. It simply writes out the column header row
186             in the current headerFormat, and changes the state of the object to "writing rows".
187              
188             writeheaders can only be called once. No more writePreamble calls can be made after writeHeaders.
189              
190             =cut
191              
192             sub writeHeaders {
193 0     0 1   my $self= shift;
194 0 0         !$self->_dataStarted or die 'Headers cannot be written twice';
195            
196 0 0         $self->_documentStarted or $self->prepareDocument;
197 0           for (my $i=0; $i < $self->colCount; $i++) {
198 0           my ($ws, $wsCol)= $self->sheetForCol($i);
199 0           $ws->write_string($self->curRow, $wsCol, $self->columns->[$i]->label, $self->headerFormat);
200 0           $self->columns->[$i]->updateWidest(length($self->columns->[$i]->label)*1.2);
201             }
202 0           $self->_dataStarted(1);
203 0           $self->{_curRow}++;
204             }
205              
206              
207              
208              
209             =head2 writeRow
210              
211             $tableWriter->writeRow( \@rowdata );
212             $tableWriter->writeRow( { col1_name => col1_val, col2_name => col2_val ... } );
213             $tableWriter->writeRow( @rowData );
214              
215             =over
216              
217             =item Arguments: \@rowdata or \%rowhash or @rowdata
218              
219             =item Returns: true
220              
221             =back
222              
223             The most optimal parameter is an array of elements in the same order as the columns were defined.
224              
225             Alternatively, a hash can be used, with the name of the columns as keys.
226              
227             If the first parameter is not a array/hash reference, the argument array is treated as the data array.
228              
229             =cut
230             our $writeRowFormat; #<-- quick/dirty global var for 'format' (see $workbook->add_format in Excel::Writer::XLSX)
231             sub writeRow {
232 0     0 1   my $self= shift;
233 0           my $rowData;
234 0 0         if (ref $_[0] eq 'ARRAY') {
    0          
235 0           $rowData= $_[0];
236             } elsif (ref $_[0] eq 'HASH') {
237 0           $rowData= $self->rowHashToArray($_[0]);
238             } else {
239 0           $rowData= [ @_ ];
240             }
241            
242 0 0         $self->_dataStarted or $self->writeHeaders;
243            
244 0           for (my $i=0; $i < $self->colCount; $i++) {
245 0           my ($ws, $wsCol)= $self->sheetForCol($i);
246            
247 0           my @args = ($self->curRow, $wsCol, $rowData->[$i]);
248 0 0         push @args, $writeRowFormat if ($writeRowFormat);
249            
250 0           $ws->write(@args);
251            
252             # -- this logic is dumb and doesn't work right. 'write' already does smart setting of the
253             # type. (commented out by HV on 2012-05-26)
254             #if ($self->columns->[$i]->isString) {
255             # $ws->write_string(@args);
256             #} else {
257             # $ws->write(@args);
258             #}
259             # --
260            
261 0 0         $self->columns->[$i]->updateWidest(length $rowData->[$i]) if (defined $rowData->[$i]);
262             }
263 0           $self->{_curRow}++;
264             }
265              
266             sub rowHashToArray {
267 0     0 0   my ($self, $hash)= @_;
268 0           my $result= [];
269 0           my $seen= 0;
270 0           for my $col (@{$self->columns}) {
  0            
271 0 0         exists $hash->{$col->name} and $seen++;
272 0           push @$result, $hash->{$col->name};
273             }
274            
275             # elaborate error check, to be helpful....
276 0 0 0       if (!$self->ignoreUnknownRowKeys && scalar(keys(%$hash)) != $seen) {
277 0           my %tmphash= %$hash;
278 0           map { delete $tmphash{$_->name} } @{$self->columns};
  0            
  0            
279 0           warn "Unused keys in row hash: ".join(',',keys(%tmphash));
280             }
281 0           return $result;
282             }
283              
284             =head2 autosizeColumns
285              
286             $tableWriter->writeRow
287             $tableWriter->writeRow
288             $tableWriter->writeRow
289             ...
290             $tableWriter->autosizeColumns
291              
292             =over
293              
294             =item Arguments: none
295              
296             =item Returns: none
297              
298             AutosizeColumns should be called after all data has been written. As each row is written, a
299             max width is updated per column. Calling autosizeColumns sets the excel column width to these
300             maximum values.
301              
302             =back
303              
304             =cut
305             sub autosizeColumns {
306 0     0 1   my $self= shift;
307 0           for (my $i=0; $i < $self->colCount; $i++) {
308 0 0         if ($self->columns->[$i]->width eq 'auto') {
309 0           my ($ws, $wsCol)= $self->sheetForCol($i);
310 0           $ws->set_column($wsCol, $wsCol, $self->columns->[$i]->widest+.5);
311             }
312             }
313             }
314              
315             1;