File Coverage

blib/lib/Text/Aligner.pm
Criterion Covered Total %
statement 150 159 94.3
branch 56 66 84.8
condition 17 23 73.9
subroutine 34 35 97.1
pod 2 2 100.0
total 259 285 90.8


line stmt bran cond sub pod time code
1             # Text::Aligner - Align text in columns
2             package Text::Aligner;
3             $Text::Aligner::VERSION = '0.16';
4 2     2   70339 use strict;
  2         22  
  2         59  
5 2     2   9 use warnings;
  2         4  
  2         46  
6              
7 2     2   46 use 5.008;
  2         7  
8              
9             BEGIN {
10 2     2   14 use Exporter ();
  2         3  
  2         57  
11 2     2   12 use vars qw (@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  2         4  
  2         244  
12 2     2   37 @ISA = qw (Exporter);
13 2         6 @EXPORT = qw ();
14 2         4 @EXPORT_OK = qw ( align);
15 2         1086 %EXPORT_TAGS = ();
16             }
17              
18             # this is a non-method, and currently the only user interface
19             sub align ($@) {
20 7     7 1 7046 my $ali = Text::Aligner->new( shift);
21 7 100       59 $ali->_alloc( map ref eq 'SCALAR' ? $$_ : $_, @_);
22 7 100       16 if ( defined wantarray ) {
23 4 50       17 my @just = map $ali->_justify( ref eq 'SCALAR' ? $$_ : $_), @_;
24 4 100       35 return @just if wantarray;
25 1         11 return join "\n", @just, '';
26             } else {
27 3         8 for ( @_ ) {
28 8 100       27 $_ = $ali->_justify( $_) for ref eq 'SCALAR' ? $$_ : $_; # one-shot
29             }
30             }
31             }
32              
33             ### class Text::Aligner
34              
35             sub _new { # internal constructor
36 68     68   101 my $class = shift;
37 68         121 my ( $width, $pos) = @_; # both method-or-coderef (this is very general)
38 68         145 bless {
39             width => $width,
40             pos => $pos,
41             left => Text::Aligner::MaxKeeper->new,
42             right => Text::Aligner::MaxKeeper->new,
43             }, $class;
44             }
45              
46             # Construct an aligner
47             sub new {
48 71     71 1 761 my ( $class, $spec) = @_;
49 71   100     268 $spec ||= 0; # left alignment is default
50 71         90 my $al;
51 71 100 66     261 if ( !ref( $spec) and $spec =~ s/^auto/num/ ) {
52 3         18 $al = Text::Aligner::Auto->_new( $spec);
53             } else {
54 68         125 $al = $class->_new( _compile_alispec( $spec));
55             }
56 71         145 $al;
57             }
58              
59             # return left and right field widths for an object
60             sub _measure0 {
61 0     0   0 my $al = shift;
62 0         0 my $obj = shift;
63 0 0       0 $obj = '' unless defined $obj;
64 0         0 my ( $w, $p);
65 0 0       0 if ( ref $obj ) {
66 0         0 ( $w, $p) = ( $obj->$al->{ width}->(), $obj->$al->{ pos}->() );
67             } else {
68 0         0 ( $w, $p) = ( $al->{ width}->( $obj), $al->{ pos}->( $obj) );
69             }
70 0   0     0 $_ ||= 0 for $w, $p;
71 0         0 ( $p, $w - $p);
72             }
73              
74 2     2   1325 use Term::ANSIColor 2.02;
  2         18620  
  2         2891  
75              
76             # return left and right field widths for an object
77             sub _measure {
78 482     482   649 my $al = shift;
79 482         620 my $obj = shift;
80 482 100       935 $obj = '' unless defined $obj;
81 482         653 my ( $wmeth, $pmeth) = @{ $al}{ qw( width pos)};
  482         954  
82              
83             # support colorized strings
84 482 50       1281 $obj = Term::ANSIColor::colorstrip($obj) unless ref $obj;
85              
86 482 50       4987 my $w = ref $wmeth ? $wmeth->( $obj) : $obj->$wmeth;
87 482 50       973 my $p = ref $pmeth ? $pmeth->( $obj) : $obj->$pmeth;
88 482   100     1819 $_ ||= 0 for $w, $p;
89 482         1123 ( $p, $w - $p);
90             }
91              
92             # Return left and right maxima, or nothing if the aligner is empty
93             sub _status {
94 734     734   1276 my @lr = ( $_[ 0]->{ left}->max, $_[ 0]->{ right}->max);
95             # $l and $r should be both defined or undefined, unless the
96             # MaxKeeper memory is corrupted by forgetting unremembered things.
97 734 100 66     2171 return unless defined( $lr[ 0]) and defined( $lr[ 1]);
98 632         1371 @lr;
99             }
100              
101             # remember alignment requirements
102             sub _alloc {
103 101     101   2249 my $al = shift;
104 101         178 for ( @_ ) {
105             # $_ ||= ''; print "allocing '$_'\n";
106 110         190 my ( $l, $r) = $al->_measure( $_);
107 110         287 $al->{ left}->remember( $l); # space needed left of pos
108 110         182 $al->{ right}->remember( $r); # ...and right of pos
109             }
110 101         141 $al;
111             }
112              
113             # release alignment requirement. it disturbs an aligner deeply to forget
114             # things it hasn't remembered. the effects may be delayed.
115             sub _forget {
116 36     36   24638 my $al = shift;
117 36 50       155 for ( map defined() ? $_ : '', @_ ) {
118             # print "forgetting '$_'\n";
119 30         67 my ( $l, $r) = $al->_measure( $_);
120 30         96 $al->{ left}->forget( $l);
121 30         55 $al->{ right}->forget( $r);
122             }
123 36         80 $al;
124             }
125              
126             sub _spaces {
127 684     684   1037 my ($repeat_count) = @_;
128 684 100       2215 return (($repeat_count > 0) ? (' ' x $repeat_count) : '');
129             }
130              
131             # justify a string. a string is aligned within the aligner's field, and
132             # filled with blanks or cut to size, as appropriate. a string that has
133             # been allocated will never be trimmed (that is the point of allocation).
134             # if the aligner is empty it returns the string unaltered.
135             sub _justify {
136 342     342   133032 my $al = shift;
137 342         498 my $str = shift;
138             # print "justifying '$str'\n";
139 342         526 $str .= ''; # stringify (objects, numbers, undef)
140 342         633 my ( $l_pad, $r_pad) = $al->_padding( $str);
141 342 100       722 substr( $str, 0, -$l_pad) = '' if $l_pad < 0; # trim if negative
142 342 100       586 substr( $str, $r_pad) = '' if $r_pad < 0; # ... both ends
143 342         566 return _spaces($l_pad) . $str . _spaces($r_pad); # pad if positive
144             }
145              
146             # return two numbers that indicate how many blanks are needed on each side
147             # of a string to justify it. Negative values mean trim that many characters.
148             # an empty aligner returns ( 0, 0), so doesn't change anything.
149             sub _padding {
150 342     342   442 my $al = shift;
151 342         471 my $str = shift;
152 342         641 my ( $this_l, $this_r) = $al->_measure( $str);
153 342         607 my ( $l_pad, $r_pad) = ( 0, 0);
154 342 100       600 if ( $al->_status ) {
155 288         454 ( $l_pad, $r_pad) = $al->_status;
156 288         459 $l_pad -= $this_l;
157 288         373 $r_pad -= $this_r;
158             }
159 342         706 ( $l_pad, $r_pad);
160             }
161              
162             # _compile_alispec() returns positioners according to specification. In
163             # effect, it is the interpreter for alignment specifications.
164              
165             sub _compile_alispec { # it's a dirty job...
166 482     482   766 my $width = sub { length shift }; # this is always so for string aligners
  82     82   6977  
167 82         140 my $pos; # the positioner we actually compile
168 82   100     266 local $_ = shift || ''; # alignment specification
169 82 100       178 if ( ref() eq 'Regexp' ) {
170 2         4 my $regex = $_; # lexical copy!
171             $pos = sub {
172 24     24   11068 local $_ = shift;
173 24 100       152 return m/$regex/ ? $-[ 0] : length; # assume match after string
174 2         9 };
175             } else {
176 80         154 s/^left/0/;
177 80         146 s/^center/0.5/;
178 80         123 s/^right/1/;
179 80 100       136 if ( _is_number( $_) ) {
    100          
180 11         24 my $proportion = $_; # use lexical copy
181 11     204   43 $pos = sub { int( $proportion*length shift) };
  204         16951  
182             } elsif ( $_ =~ /^(?:num|point)(?:\((.*))?/ ) {
183 8 100       35 my $point = defined $1 ? $1 : '';
184 8         23 $point =~ s/\)$//; # ignore trailing paren, if present
185 8 100       24 length $point or $point = '.';
186 141     141   11376 $pos = sub { index( shift() . $point, $point) }
187 8         30 } else {
188 61     197   175 $pos = sub { 0 };
  197         299  
189             }
190             }
191 82         249 ( $width, $pos);
192             }
193              
194             # decide if a string is a number. (see perlfaq4).
195             sub _is_number {
196 179     179   3675 my ($x) = @_;
197 179 100       370 return 0 unless defined $x;
198 170 100       638 return 0 if $x !~ /\d/;
199 65 100       341 return 1 if $x =~ /^-?\d+\.?\d*$/;
200 14         32 $x = Term::ANSIColor::colorstrip($x);
201 14         196 $x =~ /^-?\d+\.?\d*$/
202             }
203              
204             package Text::Aligner::Auto;
205             # Combined numeric and left alignment. Numbers are aligned numerically,
206             # other strings are left-aligned. The resulting columns are interleaved
207             # flush left and filled on the right if necessary.
208             $Text::Aligner::Auto::VERSION = '0.16';
209             sub _new { # only called by Text::Aligner->new()
210 3     3   9 my $class = shift;
211 3         6 my $numspec = shift; # currently ignored
212 3         11 bless {
213             num => Text::Aligner->new( 'num'), # align numbers among themselves
214             other => Text::Aligner->new, # left-align anything else
215             }, $class;
216             }
217              
218             sub _alloc {
219 8     8   535 my $aa = shift;
220 8         22 my @num = grep _is_number( $_), @_;
221 8         18 my @other = grep !_is_number( $_), @_;
222 8         25 $aa->{ num}->_alloc( @num);
223 8         24 $aa->{ other}->_alloc( @other);
224 8         15 $aa;
225             }
226              
227             sub _forget {
228 6     6   6296 my $aa = shift;
229 6         19 $aa->{ num}->_forget( grep _is_number( $_), @_);
230 6         18 $aa->{ other}->_forget( grep !_is_number( $_), @_);
231 6         13 $aa;
232             }
233              
234             # Justify as required
235             sub _justify {
236 52     52   33890 my ( $aa, $str) = @_;
237             # align according to type
238 52 100       127 $str = $aa->{ _is_number( $str) ? 'num' : 'other'}->_justify( $str);
239 52         116 my $combi = Text::Aligner->new; # left-justify pre-aligned string
240             # initialise to size of partial aligners. (don't initialise from
241             # empty aligner)
242 52 100       107 $combi->_alloc( $aa->{ num}->_justify( '')) if $aa->{ num}->_status;
243 52 100       108 $combi->_alloc( $aa->{ other}->_justify( '')) if $aa->{ other}->_status;
244 52         101 $combi->_justify( $str);
245             }
246              
247             # for convenience
248             BEGIN { # import _is_number()
249 2     2   673 *_is_number = \ &Text::Aligner::_is_number;
250             }
251              
252             package Text::Aligner::MaxKeeper;
253             # Keep the maximum of a dynamic set of numbers. Optimized for the case of
254             # a relatively small range of numbers that may occur repeatedly.
255             $Text::Aligner::MaxKeeper::VERSION = '0.16';
256             sub new {
257 137     137   617 bless {
258             max => undef,
259             seen => {},
260             }, shift;
261             }
262              
263 1475     1475   2583 sub max { $_[ 0]->{ max} }
264              
265             sub remember {
266 228     228   357 my ( $mk, $val) = @_;
267 228         440 _to_max( $mk->{ max}, $val);
268 228         481 $mk->{ seen}->{ $val}++;
269 228         330 $mk;
270             }
271              
272             sub forget {
273 65     65   112 my ( $mk, $val) = @_;
274 65 50       152 if ( exists $mk->{ seen}->{ $val} ) {
275 65         88 my $seen = $mk->{ seen};
276 65 100       128 unless ( --$seen->{ $val} ) {
277 63         99 delete $seen->{ $val};
278 63 100       114 if ( $mk->{ max} == $val ) {
279             # lost the maximum, recalculate
280 62         93 undef $mk->{ max};
281 62         119 _to_max( $mk->{ max}, keys %$seen);
282             }
283             }
284             }
285 65         99 $mk;
286             }
287              
288             sub _to_max {
289 290     290   441 my $var = \ shift;
290 290   100     1072 defined $_ and ( not defined $$var or $$var < $_) and $$var = $_ for @_;
      66        
      66        
291 290         424 $$var;
292             }
293              
294             1; #this line is important and will help the module return a true value
295              
296             __END__