Details | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
324 | agaran | 1 | # Text::Aligner - Align text in columns |
2 | package Text::Aligner; |
||
3 | use strict; |
||
4 | |||
5 | use warnings; |
||
6 | |||
7 | BEGIN { |
||
8 | use Exporter (); |
||
9 | use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); |
||
10 | $VERSION = 0.03; |
||
11 | @ISA = qw (Exporter); |
||
12 | #Give a hoot don't pollute, do not export more than needed by default |
||
13 | @EXPORT = qw (); |
||
14 | @EXPORT_OK = qw ( align); |
||
15 | %EXPORT_TAGS = (); |
||
16 | } |
||
17 | |||
18 | # this is a non-method, and currently the only user interface |
||
19 | sub align ($@) { |
||
20 | my $ali = Text::Aligner->new( shift); |
||
21 | $ali->alloc( map ref eq 'SCALAR' ? $$_ : $_, @_); |
||
22 | if ( defined wantarray ) { |
||
23 | my @just = map $ali->justify( ref eq 'SCALAR' ? $$_ : $_), @_; |
||
24 | return @just if wantarray; |
||
25 | return join "\n", @just, ''; |
||
26 | } else { |
||
27 | for ( @_ ) { |
||
28 | $_ = $ali->justify( $_) for ref eq 'SCALAR' ? $$_ : $_; # one-shot |
||
29 | } |
||
30 | } |
||
31 | } |
||
32 | |||
33 | ### class Text::Aligner |
||
34 | |||
35 | sub _new { # internal creator |
||
36 | my $class = shift; |
||
37 | my ( $width, $pos) = @_; # both method-or-coderef (this is very general) |
||
38 | bless { |
||
39 | width => $width, |
||
40 | pos => $pos, |
||
41 | left => Text::Aligner::MaxKeeper->new, |
||
42 | right => Text::Aligner::MaxKeeper->new, |
||
43 | }, $class; |
||
44 | } |
||
45 | |||
46 | # create an aligner |
||
47 | sub new { |
||
48 | my ( $class, $spec) = @_; |
||
49 | $spec ||= 0; # left alignment is default |
||
50 | my $al; |
||
51 | if ( !ref( $spec) and $spec =~ s/^auto/num/ ) { |
||
52 | $al = Text::Aligner::Auto->_new( $spec); |
||
53 | } else { |
||
54 | $al = $class->_new( _compile_alispec( $spec)); |
||
55 | } |
||
56 | $al; |
||
57 | } |
||
58 | |||
59 | # return left and right field widths for an object |
||
60 | sub _measure0 { |
||
61 | my $al = shift; |
||
62 | my $obj = shift; |
||
63 | $obj = '' unless defined $obj; |
||
64 | my ( $w, $p); |
||
65 | if ( ref $obj ) { |
||
66 | ( $w, $p) = ( $obj->$al->{ width}->(), $obj->$al->{ pos}->() ); |
||
67 | } else { |
||
68 | ( $w, $p) = ( $al->{ width}->( $obj), $al->{ pos}->( $obj) ); |
||
69 | } |
||
70 | $_ ||= 0 for $w, $p; |
||
71 | ( $p, $w - $p); |
||
72 | } |
||
73 | |||
74 | # return left and right field widths for an object |
||
75 | sub _measure { |
||
76 | my $al = shift; |
||
77 | my $obj = shift; |
||
78 | $obj = '' unless defined $obj; |
||
79 | my ( $wmeth, $pmeth) = @{ $al}{ qw( width pos)}; |
||
80 | my $w = ref $wmeth ? $wmeth->( $obj) : $obj->$wmeth; |
||
81 | my $p = ref $pmeth ? $pmeth->( $obj) : $obj->$pmeth; |
||
82 | $_ ||= 0 for $w, $p; |
||
83 | ( $p, $w - $p); |
||
84 | } |
||
85 | |||
86 | # return left and rigth maxima, or nothing if the aligner is empty |
||
87 | sub _status { |
||
88 | my @lr = ( $_[ 0]->{ left}->max, $_[ 0]->{ right}->max); |
||
89 | # $l and $r should be both defined or undefined, unless the |
||
90 | # MaxKeeper memory is corrupted by forgetting unremembered things. |
||
91 | return unless defined( $lr[ 0]) and defined( $lr[ 1]); |
||
92 | @lr; |
||
93 | } |
||
94 | |||
95 | # remember alignment requirements |
||
96 | sub alloc { |
||
97 | my $al = shift; |
||
98 | for ( @_ ) { |
||
99 | # $_ ||= ''; print "allocing '$_'\n"; |
||
100 | my ( $l, $r) = $al->_measure( $_); |
||
101 | $al->{ left}->remember( $l); # space needed left of pos |
||
102 | $al->{ right}->remember( $r); # ...and right of pos |
||
103 | } |
||
104 | $al; |
||
105 | } |
||
106 | |||
107 | # release alignment requirement. it disturbs an aligner deeply to forget |
||
108 | # things it hasn't remembered. the effects may be delayed. |
||
109 | sub _forget { |
||
110 | my $al = shift; |
||
111 | for ( map defined() ? $_ : '', @_ ) { |
||
112 | # print "forgetting '$_'\n"; |
||
113 | my ( $l, $r) = $al->_measure( $_); |
||
114 | $al->{ left}->forget( $l); |
||
115 | $al->{ right}->forget( $r); |
||
116 | } |
||
117 | $al; |
||
118 | } |
||
119 | |||
120 | # justify a string. a string is aligned within the aligner's field, and |
||
121 | # filled with blanks or cut to size, as appropriate. a string that has |
||
122 | # been allocated will never be trimmed (that is the point of allocation). |
||
123 | # if the aligner is empty it returns the string unaltered. |
||
124 | sub justify { |
||
125 | my $al = shift; |
||
126 | my $str = shift; |
||
127 | # print "justifying '$str'\n"; |
||
128 | $str .= ''; # stringify (objects, numbers, undef) |
||
129 | my ( $l_pad, $r_pad) = $al->_padding( $str); |
||
130 | substr( $str, 0, -$l_pad) = '' if $l_pad < 0; # trim if negative |
||
131 | substr( $str, $r_pad) = '' if $r_pad < 0; # ... both ends |
||
132 | join $str, ' ' x $l_pad, ' ' x $r_pad; # pad if positive |
||
133 | } |
||
134 | |||
135 | # return two numbers that indicate how many blanks are needed on each side |
||
136 | # of a string to justify it. Negative values mean trim that many characters. |
||
137 | # an empty aligner returns ( 0, 0), so doesn't change anything. |
||
138 | sub _padding { |
||
139 | my $al = shift; |
||
140 | my $str = shift; |
||
141 | my ( $this_l, $this_r) = $al->_measure( $str); |
||
142 | my ( $l_pad, $r_pad) = ( 0, 0); |
||
143 | if ( $al->_status ) { |
||
144 | ( $l_pad, $r_pad) = $al->_status; |
||
145 | $l_pad -= $this_l; |
||
146 | $r_pad -= $this_r; |
||
147 | } |
||
148 | ( $l_pad, $r_pad); |
||
149 | } |
||
150 | |||
151 | # _compile_alispec() returns positioners according to specification. In |
||
152 | # effect, it is is the interpreter for alignment specifications. |
||
153 | |||
154 | sub _compile_alispec { # it's a dirty job... |
||
155 | my $width = sub { length shift }; # this is always so for string aligners |
||
156 | my $pos; # the positioner we actually compile |
||
157 | local $_ = shift || ''; # alignment specification |
||
158 | if ( ref() eq 'Regexp' ) { |
||
159 | my $regex = $_; # lexical copy! |
||
160 | $pos = sub { |
||
161 | local $_ = shift; |
||
162 | m/$regex/ ? $-[ 0] : length; # assume match after string |
||
163 | }; |
||
164 | } else { |
||
165 | s/^left/0/; |
||
166 | s/^center/0.5/; |
||
167 | s/^right/1/; |
||
168 | if ( _is_number( $_) ) { |
||
169 | my $proportion = $_; # use lexical copy |
||
170 | $pos = sub { int( $proportion*length shift) }; |
||
171 | } elsif ( $_ =~ /^(?:num|point)(?:\((.*))?/ ) { |
||
172 | my $point = defined $1 ? $1 : ''; |
||
173 | $point =~ s/\)$//; # ignore trailing paren, if present |
||
174 | length $point or $point = '.'; |
||
175 | $pos = sub { index( shift() . $point, $point) } |
||
176 | } else { |
||
177 | $pos = sub { 0 }; |
||
178 | } |
||
179 | } |
||
180 | ( $width, $pos); |
||
181 | } |
||
182 | |||
183 | # decide if a string is a number. (see perlfaq4). This needs to become |
||
184 | # more flexible for auto-alignment |
||
185 | sub _is_number { defined( $_[ 0]) and $_[ 0] =~ /^-?\d+\.?\d*$/ } |
||
186 | |||
187 | package Text::Aligner::Auto; |
||
188 | # Combined numeric and left alignment. Numbers are aligned numerically, |
||
189 | # other strings are left-aligned. The resulting columns are interleaved |
||
190 | # flush left and filled on the right if necessary. |
||
191 | |||
192 | sub _new { # only called by Text::Aligner->new() |
||
193 | my $class = shift; |
||
194 | my $numspec = shift; # currently ignored |
||
195 | bless { |
||
196 | num => Text::Aligner->new( 'num'), # align numbers among themselves |
||
197 | other => Text::Aligner->new, # left-align anything else |
||
198 | }, $class; |
||
199 | } |
||
200 | |||
201 | sub alloc { |
||
202 | my $aa = shift; |
||
203 | my @num = grep _is_number( $_), @_; |
||
204 | my @other = grep !_is_number( $_), @_; |
||
205 | $aa->{ num}->alloc( @num); |
||
206 | $aa->{ other}->alloc( @other); |
||
207 | $aa; |
||
208 | } |
||
209 | |||
210 | sub _forget { |
||
211 | my $aa = shift; |
||
212 | $aa->{ num}->_forget( grep _is_number( $_), @_); |
||
213 | $aa->{ other}->_forget( grep !_is_number( $_), @_); |
||
214 | $aa; |
||
215 | } |
||
216 | |||
217 | # justify as required |
||
218 | sub justify { |
||
219 | my ( $aa, $str) = @_; |
||
220 | # align according to type |
||
221 | $str = $aa->{ _is_number( $str) ? 'num' : 'other'}->justify( $str); |
||
222 | my $combi = Text::Aligner->new; # left-justify pre-aligned string |
||
223 | # initialise to size of partial aligners. (don't initialise from |
||
224 | # empty aligner) |
||
225 | $combi->alloc( $aa->{ num}->justify( '')) if $aa->{ num}->_status; |
||
226 | $combi->alloc( $aa->{ other}->justify( '')) if $aa->{ other}->_status; |
||
227 | $combi->justify( $str); |
||
228 | } |
||
229 | |||
230 | # for convenience |
||
231 | BEGIN { # import _is_number() |
||
232 | *_is_number = \ &Text::Aligner::_is_number; |
||
233 | } |
||
234 | |||
235 | package Text::Aligner::MaxKeeper; |
||
236 | # Keep the maximum of a dynamic set of numbers. Optimized for the case of |
||
237 | # a relatively small range of numbers that may occur repeatedly. |
||
238 | |||
239 | sub new { |
||
240 | bless { |
||
241 | max => undef, |
||
242 | seen => {}, |
||
243 | }, shift; |
||
244 | } |
||
245 | |||
246 | sub max { $_[ 0]->{ max} } |
||
247 | |||
248 | sub remember { |
||
249 | my ( $mk, $val) = @_; |
||
250 | _to_max( $mk->{ max}, $val); |
||
251 | $mk->{ seen}->{ $val}++; |
||
252 | $mk; |
||
253 | } |
||
254 | |||
255 | sub forget { |
||
256 | my ( $mk, $val) = @_; |
||
257 | if ( exists $mk->{ seen}->{ $val} ) { |
||
258 | my $seen = $mk->{ seen}; |
||
259 | unless ( --$seen->{ $val} ) { |
||
260 | delete $seen->{ $val}; |
||
261 | if ( $mk->{ max} == $val ) { |
||
262 | # lost the maximum, recalculate |
||
263 | undef $mk->{ max}; |
||
264 | _to_max( $mk->{ max}, keys %$seen); |
||
265 | } |
||
266 | } |
||
267 | } |
||
268 | $mk; |
||
269 | } |
||
270 | |||
271 | sub _to_max { |
||
272 | my $var = \ shift; |
||
273 | defined $_ and ( not defined $$var or $$var < $_) and $$var = $_ for @_; |
||
274 | $$var; |
||
275 | } |
||
276 | |||
277 | ########################################### main pod documentation begin ## |
||
278 | |||
279 | =head1 NAME |
||
280 | |||
281 | Text::Aligner |
||
282 | |||
283 | =head1 SYNOPSIS |
||
284 | |||
285 | use Text::Aligner qw( align); |
||
286 | |||
287 | # Print the words "just a test!" right-justified each on a line: |
||
288 | |||
289 | my @lines = align( 'right', qw( just a test!); |
||
290 | print "$_\n" for @lines; |
||
291 | |||
292 | =head1 DESCRIPTION |
||
293 | |||
294 | Text::Aligner exports a single function, align(), which is |
||
295 | used to justify strings to various alignment styles. The |
||
296 | alignment specification is the first argument, followed by |
||
297 | any number of scalars which are subject to alignment. |
||
298 | |||
299 | The operation depends on context. In list context, a list of |
||
300 | the justified scalars is returned. In scalar context, the |
||
301 | justified arguments are joined into a single string with newlines |
||
302 | appended. The original arguments remain unchanged. In void |
||
303 | context, in-place justification is attempted. In this case, all |
||
304 | arguments must be lvalues. |
||
305 | |||
306 | Align() also does one level of scalar dereferencing. That is, |
||
307 | whenever one of the arguments is a scalar reference, the scalar |
||
308 | pointed to is aligned instead. Other references are simply stringified. |
||
309 | An undefined argument is interpreted as an empty string without |
||
310 | complaint. |
||
311 | |||
312 | =head1 ALIGNMENT |
||
313 | |||
314 | The first argument of the align() function is an alignment style, a |
||
315 | single scalar. |
||
316 | |||
317 | It can be one of the strings "left", "right", "center", "num", "point", |
||
318 | or "auto", or a regular expression (qr/.../), or a coderef. |
||
319 | |||
320 | A default style of "left" is assumed for every other value, including |
||
321 | "" and undef. |
||
322 | |||
323 | "left", "right" and "center" have the obvious meanings. These can |
||
324 | also be given as numbers 0, 1, and 0.5 respectively. (Other numbers |
||
325 | are also possible, but probably not very useful). |
||
326 | |||
327 | "num", and its synonym "point", specify that the decimal points be |
||
328 | aligned (assumed on the right, unless present). Arbitrary (non-numeric) |
||
329 | strings are also aligned in this manner, so they end up one column left |
||
330 | of the (possibly assumed) decimal point, flush right with any integers. |
||
331 | For the occasional string like "inf", or "-" for missing values, this |
||
332 | may be the right place. A string-only column ends up right-aligned |
||
333 | (unless there are points present). |
||
334 | |||
335 | The "auto" style seperates numeric strings (that are composed of |
||
336 | "-", ".", and digits in the usual manner) and aligns them numerically. |
||
337 | Other strings are left aligned with the number that sticks out |
||
338 | farthest to the left. This gives left alignment for string-only |
||
339 | columns and numeric alignment for columns of numbers. In mixed |
||
340 | columns, strings are reasonably placed to serve as column headings |
||
341 | or intermediate titles. |
||
342 | |||
343 | With "num" (and "point") it is possible to specify another character |
||
344 | for the decimal point in the form "num(,)". In fact, you can specify |
||
345 | any string after a leading "(", and the closing ")" is optional. |
||
346 | "point(=>)" could be used to align certain pieces of Perl code. This |
||
347 | option is currently not available with "auto" alignment (because |
||
348 | recognition of numbers is Anglo-centric). |
||
349 | |||
350 | If a regular expression is specified, the points are aligned where |
||
351 | the first match of the regex starts. A match is assumed immediately |
||
352 | after the string if it doesn't match. |
||
353 | |||
354 | A regular expression is a powerful way of alignment specification. It |
||
355 | can replace most others easily, except center alignment and, of course, |
||
356 | the double action of "auto". |
||
357 | |||
358 | =head1 POSITIONERS |
||
359 | |||
360 | For entirely self-defined forms of alignment, a coderef, also known |
||
361 | as a positioner, can be given instead of an alignment style. This |
||
362 | code will be called once or more times with the string to be aligned |
||
363 | as its argument. It must return two numbers, a width and a position, |
||
364 | that describe how to align a string with other strings. |
||
365 | |||
366 | The width should normally be the length of the string. The position |
||
367 | defines a point relative to the beginning of the string, which is |
||
368 | aligned with the positions given for other strings. |
||
369 | |||
370 | A zero position for all strings results in left alignment, positioning |
||
371 | to the end of the string results in right alignment, and returning |
||
372 | half the length gives center alignment. "num" alignment is realized |
||
373 | by marking the position of the decimal point. |
||
374 | |||
375 | Note that the position you return is a relative measure. Adding a |
||
376 | constant value to all positions results in no change in alignment. |
||
377 | It doesn't have to point inside the string (as in right alignment, |
||
378 | where it points one character past the end of the string). |
||
379 | |||
380 | The first return value of a positioner should almost always be the |
||
381 | length of the given string. It may be useful to ly about the string |
||
382 | length if the string contains escape sequences that occupy no place |
||
383 | on screen. |
||
384 | |||
385 | =head1 USAGE |
||
386 | |||
387 | use Text::Aligner qw( align); |
||
388 | |||
389 | align( $style, $str, ...); |
||
390 | |||
391 | $style must be given and must be an alignment specification. |
||
392 | Any number of scalars can follow. An argument that contains a |
||
393 | scalar reference is dereferenced before it is used. In scalar |
||
394 | and list context, the aligned strings are returned. In void |
||
395 | context, the values are aligned in place and must be lvalues. |
||
396 | |||
397 | =head1 BUGS |
||
398 | |||
399 | None known as of realease, but... |
||
400 | |||
401 | =head1 AUTHOR |
||
402 | |||
403 | Anno Siegel |
||
404 | CPAN ID: ANNO |
||
405 | siegel@zrz.tu-berlin.de |
||
406 | http://www.tu-berlin.de/~siegel |
||
407 | |||
408 | =head1 COPYRIGHT |
||
409 | |||
410 | Copyright (c) 2002 Anno Siegel. All rights reserved. |
||
411 | This program is free software; you can redistribute |
||
412 | it and/or modify it under the same terms as Perl itself. |
||
413 | |||
414 | The full text of the license can be found in the |
||
415 | LICENSE file included with this module. |
||
416 | |||
417 | =head1 SEE ALSO |
||
418 | |||
419 | perl(1) |
||
420 | |||
421 | Text::Table |
||
422 | |||
423 | =cut |
||
424 | |||
425 | 1; #this line is important and will help the module return a true value |
||
426 | __END__ |