Commit | Line | Data |
3fea05b9 |
1 | #============================================================= -*-Perl-*- |
2 | # |
3 | # Template::Plugin::Table |
4 | # |
5 | # DESCRIPTION |
6 | # Plugin to order a linear data set into a virtual 2-dimensional table |
7 | # from which row and column permutations can be fetched. |
8 | # |
9 | # AUTHOR |
10 | # Andy Wardley <abw@wardley.org> |
11 | # |
12 | # COPYRIGHT |
13 | # Copyright (C) 2000-2007 Andy Wardley. All Rights Reserved. |
14 | # |
15 | # This module is free software; you can redistribute it and/or |
16 | # modify it under the same terms as Perl itself. |
17 | # |
18 | #============================================================================ |
19 | |
20 | package Template::Plugin::Table; |
21 | |
22 | use strict; |
23 | use warnings; |
24 | use base 'Template::Plugin'; |
25 | use Scalar::Util 'blessed'; |
26 | |
27 | our $VERSION = 2.71; |
28 | our $AUTOLOAD; |
29 | |
30 | |
31 | #------------------------------------------------------------------------ |
32 | # new($context, \@data, \%args) |
33 | # |
34 | # This constructor method initialises the object to iterate through |
35 | # the data set passed by reference to a list as the first parameter. |
36 | # It calculates the shape of the permutation table based on the ROWS |
37 | # or COLS parameters specified in the $args hash reference. The |
38 | # OVERLAP parameter may be provided to specify the number of common |
39 | # items that should be shared between subseqent columns. |
40 | #------------------------------------------------------------------------ |
41 | |
42 | sub new { |
43 | my ($class, $context, $data, $params) = @_; |
44 | my ($size, $rows, $cols, $coloff, $overlap, $error); |
45 | |
46 | # if the data item is a reference to a Template::Iterator object, |
47 | # or subclass thereof, we call its get_all() method to extract all |
48 | # the data it contains |
49 | if (blessed($data) && $data->isa('Template::Iterator')) { |
50 | ($data, $error) = $data->get_all(); |
51 | return $class->error("iterator failed to provide data for table: ", |
52 | $error) |
53 | if $error; |
54 | } |
55 | |
56 | return $class->error('invalid table data, expecting a list') |
57 | unless ref $data eq 'ARRAY'; |
58 | |
59 | $params ||= { }; |
60 | return $class->error('invalid table parameters, expecting a hash') |
61 | unless ref $params eq 'HASH'; |
62 | |
63 | # ensure keys are folded to upper case |
64 | @$params{ map { uc } keys %$params } = values %$params; |
65 | |
66 | $size = scalar @$data; |
67 | $overlap = $params->{ OVERLAP } || 0; |
68 | |
69 | # calculate number of columns based on a specified number of rows |
70 | if ($rows = $params->{ ROWS }) { |
71 | if ($size < $rows) { |
72 | $rows = $size; # pad? |
73 | $cols = 1; |
74 | $coloff = 0; |
75 | } |
76 | else { |
77 | $coloff = $rows - $overlap; |
78 | $cols = int ($size / $coloff) |
79 | + ($size % $coloff > $overlap ? 1 : 0) |
80 | } |
81 | } |
82 | # calculate number of rows based on a specified number of columns |
83 | elsif ($cols = $params->{ COLS }) { |
84 | if ($size < $cols) { |
85 | $cols = $size; |
86 | $rows = 1; |
87 | $coloff = 1; |
88 | } |
89 | else { |
90 | $coloff = int ($size / $cols) |
91 | + ($size % $cols > $overlap ? 1 : 0); |
92 | $rows = $coloff + $overlap; |
93 | } |
94 | } |
95 | else { |
96 | $rows = $size; |
97 | $cols = 1; |
98 | $coloff = 0; |
99 | } |
100 | |
101 | bless { |
102 | _DATA => $data, |
103 | _SIZE => $size, |
104 | _NROWS => $rows, |
105 | _NCOLS => $cols, |
106 | _COLOFF => $coloff, |
107 | _OVERLAP => $overlap, |
108 | _PAD => defined $params->{ PAD } ? $params->{ PAD } : 1, |
109 | }, $class; |
110 | } |
111 | |
112 | |
113 | #------------------------------------------------------------------------ |
114 | # row($n) |
115 | # |
116 | # Returns a reference to a list containing the items in the row whose |
117 | # number is specified by parameter. If the row number is undefined, |
118 | # it calls rows() to return a list of all rows. |
119 | #------------------------------------------------------------------------ |
120 | |
121 | sub row { |
122 | my ($self, $row) = @_; |
123 | my ($data, $cols, $offset, $size, $pad) |
124 | = @$self{ qw( _DATA _NCOLS _COLOFF _SIZE _PAD) }; |
125 | my @set; |
126 | |
127 | # return all rows if row number not specified |
128 | return $self->rows() |
129 | unless defined $row; |
130 | |
131 | return () if $row >= $self->{ _NROWS } || $row < 0; |
132 | |
133 | my $index = $row; |
134 | |
135 | for (my $c = 0; $c < $cols; $c++) { |
136 | push(@set, $index < $size |
137 | ? $data->[$index] |
138 | : ($pad ? undef : ())); |
139 | $index += $offset; |
140 | } |
141 | return \@set; |
142 | } |
143 | |
144 | |
145 | #------------------------------------------------------------------------ |
146 | # col($n) |
147 | # |
148 | # Returns a reference to a list containing the items in the column whose |
149 | # number is specified by parameter. If the column number is undefined, |
150 | # it calls cols() to return a list of all columns. |
151 | #------------------------------------------------------------------------ |
152 | |
153 | sub col { |
154 | my ($self, $col) = @_; |
155 | my ($data, $size) = @$self{ qw( _DATA _SIZE ) }; |
156 | my ($start, $end); |
157 | my $blanks = 0; |
158 | |
159 | # return all cols if row number not specified |
160 | return $self->cols() |
161 | unless defined $col; |
162 | |
163 | return () if $col >= $self->{ _NCOLS } || $col < 0; |
164 | |
165 | $start = $self->{ _COLOFF } * $col; |
166 | $end = $start + $self->{ _NROWS } - 1; |
167 | $end = $start if $end < $start; |
168 | if ($end >= $size) { |
169 | $blanks = ($end - $size) + 1; |
170 | $end = $size - 1; |
171 | } |
172 | return () if $start >= $size; |
173 | return [ @$data[$start..$end], |
174 | $self->{ _PAD } ? ((undef) x $blanks) : () ]; |
175 | } |
176 | |
177 | |
178 | #------------------------------------------------------------------------ |
179 | # rows() |
180 | # |
181 | # Returns all rows as a reference to a list of rows. |
182 | #------------------------------------------------------------------------ |
183 | |
184 | sub rows { |
185 | my $self = shift; |
186 | return [ map { $self->row($_) } (0..$self->{ _NROWS }-1) ]; |
187 | } |
188 | |
189 | |
190 | #------------------------------------------------------------------------ |
191 | # cols() |
192 | # |
193 | # Returns all rows as a reference to a list of rows. |
194 | #------------------------------------------------------------------------ |
195 | |
196 | sub cols { |
197 | my $self = shift; |
198 | return [ map { $self->col($_) } (0..$self->{ _NCOLS }-1) ]; |
199 | } |
200 | |
201 | |
202 | #------------------------------------------------------------------------ |
203 | # AUTOLOAD |
204 | # |
205 | # Provides read access to various internal data members. |
206 | #------------------------------------------------------------------------ |
207 | |
208 | sub AUTOLOAD { |
209 | my $self = shift; |
210 | my $item = $AUTOLOAD; |
211 | $item =~ s/.*:://; |
212 | return if $item eq 'DESTROY'; |
213 | |
214 | if ($item =~ /^(?:data|size|nrows|ncols|overlap|pad)$/) { |
215 | return $self->{ $item }; |
216 | } |
217 | else { |
218 | return (undef, "no such table method: $item"); |
219 | } |
220 | } |
221 | |
222 | |
223 | |
224 | 1; |
225 | |
226 | __END__ |
227 | |
228 | =head1 NAME |
229 | |
230 | Template::Plugin::Table - Plugin to present data in a table |
231 | |
232 | =head1 SYNOPSIS |
233 | |
234 | [% USE table(list, rows=n, cols=n, overlap=n, pad=0) %] |
235 | |
236 | [% FOREACH item IN table.row(n) %] |
237 | [% item %] |
238 | [% END %] |
239 | |
240 | [% FOREACH item IN table.col(n) %] |
241 | [% item %] |
242 | [% END %] |
243 | |
244 | [% FOREACH row IN table.rows %] |
245 | [% FOREACH item IN row %] |
246 | [% item %] |
247 | [% END %] |
248 | [% END %] |
249 | |
250 | [% FOREACH col IN table.cols %] |
251 | [% col.first %] - [% col.last %] ([% col.size %] entries) |
252 | [% END %] |
253 | |
254 | =head1 DESCRIPTION |
255 | |
256 | The C<Table> plugin allows you to format a list of data items into a |
257 | virtual table. When you create a C<Table> plugin via the C<USE> directive, |
258 | simply pass a list reference as the first parameter and then specify |
259 | a fixed number of rows or columns. |
260 | |
261 | [% USE Table(list, rows=5) %] |
262 | [% USE table(list, cols=5) %] |
263 | |
264 | The C<Table> plugin name can also be specified in lower case as shown |
265 | in the second example above. You can also specify an alternative variable |
266 | name for the plugin as per regular Template Toolkit syntax. |
267 | |
268 | [% USE mydata = table(list, rows=5) %] |
269 | |
270 | The plugin then presents a table based view on the data set. The data |
271 | isn't actually reorganised in any way but is available via the C<row()>, |
272 | C<col()>, C<rows()> and C<cols()> as if formatted into a simple two dimensional |
273 | table of C<n> rows x C<n> columns. |
274 | |
275 | So if we had a sample C<alphabet> list contained the letters 'C<a>' to 'C<z>', |
276 | the above C<USE> directives would create plugins that represented the following |
277 | views of the alphabet. |
278 | |
279 | [% USE table(alphabet, ... %] |
280 | |
281 | rows=5 cols=5 |
282 | a f k p u z a g m s y |
283 | b g l q v b h n t z |
284 | c h m r w c i o u |
285 | d i n s x d j p v |
286 | e j o t y e k q w |
287 | f l r x |
288 | |
289 | We can request a particular row or column using the C<row()> and C<col()> |
290 | methods. |
291 | |
292 | [% USE table(alphabet, rows=5) %] |
293 | [% FOREACH item = table.row(0) %] |
294 | # [% item %] set to each of [ a f k p u z ] in turn |
295 | [% END %] |
296 | |
297 | [% FOREACH item = table.col(2) %] |
298 | # [% item %] set to each of [ m n o p q r ] in turn |
299 | [% END %] |
300 | |
301 | Data in rows is returned from left to right, columns from top to |
302 | bottom. The first row/column is 0. By default, rows or columns that |
303 | contain empty values will be padded with the undefined value to fill |
304 | it to the same size as all other rows or columns. |
305 | |
306 | For example, the last row (row 4) in the first example would contain the |
307 | values C<[ e j o t y undef ]>. The Template Toolkit will safely accept these |
308 | undefined values and print a empty string. You can also use the IF directive |
309 | to test if the value is set. |
310 | |
311 | [% FOREACH item = table.row(4) %] |
312 | [% IF item %] |
313 | Item: [% item %] |
314 | [% END %] |
315 | [% END %] |
316 | |
317 | You can explicitly disable the C<pad> option when creating the plugin to |
318 | returned shortened rows/columns where the data is empty. |
319 | |
320 | [% USE table(alphabet, cols=5, pad=0) %] |
321 | [% FOREACH item = table.col(4) %] |
322 | # [% item %] set to each of 'y z' |
323 | [% END %] |
324 | |
325 | The C<rows()> method returns all rows/columns in the table as a reference |
326 | to a list of rows (themselves list references). The C<row()> methods |
327 | when called without any arguments calls C<rows()> to return all rows in |
328 | the table. |
329 | |
330 | Ditto for C<cols()> and C<col()>. |
331 | |
332 | [% USE table(alphabet, cols=5) %] |
333 | [% FOREACH row = table.rows %] |
334 | [% FOREACH item = row %] |
335 | [% item %] |
336 | [% END %] |
337 | [% END %] |
338 | |
339 | The Template Toolkit provides the C<first>, C<last> and C<size> virtual |
340 | methods that can be called on list references to return the first/last entry |
341 | or the number of entries in a list. The following example shows how we might |
342 | use this to provide an alphabetical index split into 3 even parts. |
343 | |
344 | [% USE table(alphabet, cols=3, pad=0) %] |
345 | [% FOREACH group = table.col %] |
346 | [ [% group.first %] - [% group.last %] ([% group.size %] letters) ] |
347 | [% END %] |
348 | |
349 | This produces the following output: |
350 | |
351 | [ a - i (9 letters) ] |
352 | [ j - r (9 letters) ] |
353 | [ s - z (8 letters) ] |
354 | |
355 | We can also use the general purpose C<join> virtual method which joins |
356 | the items of the list using the connecting string specified. |
357 | |
358 | [% USE table(alphabet, cols=5) %] |
359 | [% FOREACH row = table.rows %] |
360 | [% row.join(' - ') %] |
361 | [% END %] |
362 | |
363 | Data in the table is ordered downwards rather than across but can easily |
364 | be transformed on output. For example, to format our data in 5 columns |
365 | with data ordered across rather than down, we specify C<rows=5> to order |
366 | the data as such: |
367 | |
368 | a f . . |
369 | b g . |
370 | c h |
371 | d i |
372 | e j |
373 | |
374 | and then iterate down through each column (a-e, f-j, etc.) printing |
375 | the data across. |
376 | |
377 | a b c d e |
378 | f g h i j |
379 | . . |
380 | . |
381 | |
382 | Example code to do so would be much like the following: |
383 | |
384 | [% USE table(alphabet, rows=3) %] |
385 | [% FOREACH cols = table.cols %] |
386 | [% FOREACH item = cols %] |
387 | [% item %] |
388 | [% END %] |
389 | [% END %] |
390 | |
391 | Output: |
392 | |
393 | a b c |
394 | d e f |
395 | g h i |
396 | j . . |
397 | . |
398 | |
399 | In addition to a list reference, the C<Table> plugin constructor may be passed |
400 | a reference to a L<Template::Iterator> object or subclass thereof. The |
401 | L<Template::Iterator> L<get_all()|Template::Iterator#get_all()> method is |
402 | first called on the iterator to return all remaining items. These are then |
403 | available via the usual Table interface. |
404 | |
405 | [% USE DBI(dsn,user,pass) -%] |
406 | |
407 | # query() returns an iterator |
408 | [% results = DBI.query('SELECT * FROM alphabet ORDER BY letter') %] |
409 | |
410 | # pass into Table plugin |
411 | [% USE table(results, rows=8 overlap=1 pad=0) -%] |
412 | |
413 | [% FOREACH row = table.cols -%] |
414 | [% row.first.letter %] - [% row.last.letter %]: |
415 | [% row.join(', ') %] |
416 | [% END %] |
417 | |
418 | =head1 AUTHOR |
419 | |
420 | Andy Wardley E<lt>abw@wardley.orgE<gt> L<http://wardley.org/> |
421 | |
422 | =head1 COPYRIGHT |
423 | |
424 | Copyright (C) 1996-2007 Andy Wardley. All Rights Reserved. |
425 | |
426 | This module is free software; you can redistribute it and/or |
427 | modify it under the same terms as Perl itself. |
428 | |
429 | =head1 SEE ALSO |
430 | |
431 | L<Template::Plugin> |
432 | |
433 | =cut |
434 | |
435 | # Local Variables: |
436 | # mode: perl |
437 | # perl-indent-level: 4 |
438 | # indent-tabs-mode: nil |
439 | # End: |
440 | # |
441 | # vim: expandtab shiftwidth=4: |