File Coverage

blib/lib/Text/Shorten.pm
Criterion Covered Total %
statement 166 179 92.7
branch 81 102 79.4
condition 81 107 75.7
subroutine 8 8 100.0
pod 3 3 100.0
total 339 399 84.9


line stmt bran cond sub pod time code
1             package Text::Shorten;
2              
3 6     6   28185 use warnings;
  6         12  
  6         201  
4 6     6   31 use strict;
  6         11  
  6         176  
5 6     6   5576 use Hash::SafeKeys;
  6         756543  
  6         597  
6 6     6   54 use Exporter;
  6         13  
  6         2724  
7             our @ISA = qw(Exporter);
8             our @EXPORT = qw();
9             our @EXPORT_OK = qw(shorten_scalar shorten_array shorten_hash);
10             our %EXPORT_TAGS = (all => [ @EXPORT_OK ]);
11             our $VERSION = '0.06';
12              
13             our $DOTDOTDOT = '...';
14             our $DOTDOTDOT_LENGTH = length($DOTDOTDOT);
15             our $DEFAULT_ARRAY_ELEM_SEPARATOR_LENGTH = 1;
16             our $DEFAULT_HASH_ELEM_SEPARATOR_LENGTH = 1;
17             our $DEFAULT_HASH_KEYVALUE_SEPARATOR_LENGTH = 2;
18             our $HASHREPR_SORTKEYS;
19              
20             sub shorten_scalar {
21 10080     10080 1 6111042 my ($scalar, $maxlen) = @_;
22              
23 10080 100       35218 return $scalar if length($scalar) < $maxlen;
24              
25 9561 50 33     38459 if ($scalar =~ /^['"]./ # "']/
26             && substr($scalar,0,1) eq substr($scalar,-1)) {
27              
28 0         0 return substr($scalar,0,$maxlen-$DOTDOTDOT_LENGTH-1)
29             . $DOTDOTDOT . substr($scalar,-1);
30              
31             }
32              
33 9561         90984 my ($sign,$d1,$d,$d2,$e,$exp) =
34             $scalar =~ /^\s* ([+-]?)
35             (\d*)
36             (\.?)
37             (\d*)
38             ([Ee]?)
39             ([+-]?\d+)? \s*$/x;
40              
41             {
42 6     6   36 no warnings 'uninitialized';
  6         12  
  6         12343  
  9561         16934  
43 9561 50       32826 if ("$d1$d2" eq "") {
44             # not a number.
45              
46 0         0 return substr($scalar, 0, $maxlen-$DOTDOTDOT_LENGTH) . $DOTDOTDOT;
47             }
48             }
49              
50             # the rest of this function is strategies for making a number shorter.
51             # We may lose precision but we won't need to use $DOTDOTDOT
52              
53 9561   100     38148 $sign ||= '';
54 9561   100     21350 $d1 ||= '0';
55 9561 50       17873 $d2 = '' if not defined $d2;
56 9561   100     20157 $e ||= '';
57 9561 100       22135 $exp = '' if not defined $exp;
58 9561   100     20971 my $E = $e || 'e';
59              
60 9561 100       43048 $exp =~ s/^\+// if length("$sign$d1$d$d2$e$exp") > $maxlen;
61 9561 100       52551 $d1 =~ s/^0+(.)/$1/ if length("$sign$d1$d$d2$e$exp") > $maxlen;
62 9561 100       52824 $d2 =~ s/(.)0+$/$1/ if length("$sign$d1$d2$e$exp") > $maxlen;
63 9561 100       61805 $exp =~ s/^(\-?)0+(.)/$1$2/ if length("$sign$d1$d$d2$exp") > $maxlen;
64 9561 100       35661 $sign =~ s/^\+// if length("$sign$d1$d$d2$e$exp") > $maxlen;
65              
66 9561         11932 my $lastc = 0;
67 9561         26918 while (length("$sign$d1$d$d2$e$exp") > $maxlen) {
68              
69 662473 100 100     7856984 if ($d && $d2 eq '') { # 1.E23 => 1E23
70 7331         9811 $d = '';
71 7331   50     14615 $d1 ||= '0';
72 7331         21228 next;
73             }
74              
75 655142 100 66     15767008 if ($e && ($exp eq '' || $exp==0)) { # 4.567E0 => 4.567
      66        
76 4518         7406 $e = $exp = '';
77 4518         13213 next;
78             }
79              
80 650624 100 66     7853150 if (($d1 eq '' || $d1 == 0) && $d2 ne '') {
      66        
81 606         1101 $d1=0;
82 606   100     2824 while ($d1 == 0 && $d2 ne '') { # 0.123 => 1.23,
83             # 0.0004E-5 => 0.004E-6
84 6342         9534 $d1 = substr($d2,0,1);
85 6342         8703 $d2 = substr($d2,1);
86 6342   66     13200 $e ||= $E;
87 6342         28722 $exp--;
88             }
89 606         6407 next;
90             }
91              
92 650018 100 100     7378446 if ($e eq '' && $d2 ne '') {
93             # start truncating digits
94 138348         196592 my $c = chop $d2;
95 138348 100 100     2295525 if ($c > 5 || ($c == 5 && $lastc)) {
      66        
96 65492         75328 my $new_d2 = $d2;
97 65492         67096 $lastc = 0;
98 65492 50       146906 if (length($new_d2) > length($d2)) {
99 0         0 $d1++;
100 0         0 $d2 = substr($new_d2,1);
101             }
102             } else {
103 72856         87728 $lastc = 1;
104             }
105 138348         393474 next;
106             }
107              
108 511670 100 66     10182251 if ($d eq '' && $d2 eq '') {
109             # start truncating digits and increment exp
110 329209         426020 my $c = chop $d1;
111 329209 100 100     1211510 if ($c > 5 || ($c==5 && $lastc)) {
      66        
112 192178         200468 $lastc = 0;
113 192178         242332 $d1++;
114             } else {
115 137031         160236 $lastc = 1;
116             }
117 329209   66     598968 $e ||= $E;
118 329209         336962 $exp++;
119 329209         976517 next;
120             }
121              
122 182461 50       337507 if ($d2 ne '') {
123             # start truncating digits
124 182461         262541 my $c = chop $d2;
125 182461 100 100     940662 if ($c > 5 || ($c == 5 && $lastc)) {
      66        
126 86038         100851 my $new_d2 = $d2;
127 86038         86347 $lastc = 0;
128 86038 50       168433 if (length($new_d2) > length($d2)) {
129 0         0 $d1++;
130 0         0 $d2 = substr($new_d2,1);
131             }
132             } else {
133 96423         123639 $lastc = 1;
134             }
135 182461         533971 next;
136             }
137             }
138              
139 9561 100       24614 $d = '' if $d2 eq '';
140 9561   100     187687 while ($e && $d && $d1 ne '' && $d1 > 9) { # 98.7E6 => 9.87E7
      66        
      100        
141 5130         10070 $d2 = chop($d1) . $d2;
142 5130         6947 $d = '.';
143 5130         266818 $exp++;
144             }
145 9561 100       21585 $d = '' if $d2 eq '';
146 9561         52425 return "$sign$d1$d$d2$e$exp";
147             }
148              
149             # XXX - do we want to make it configurable whether shorten_array
150             # always returns at least one full element?
151              
152             sub shorten_array {
153 7     7 1 5183 my ($array, $maxlen, $seplen, @key) = @_;
154 7 100       27 if (!defined($seplen)) {
    50          
155 4         9 $seplen = $DEFAULT_ARRAY_ELEM_SEPARATOR_LENGTH;
156             } elsif ($seplen =~ /\D/) {
157 0         0 $seplen = length($seplen);
158             }
159 7         9 my $dotslen = $seplen + $DOTDOTDOT_LENGTH;
160              
161 7         9 my $n = $#{$array} + 1;
  7         13  
162 7 100       21 @key = (0) if @key == 0;
163 7 50       11 @key = sort {$a <=> $b} grep { $_ >= 0 && $_ < $n } @key;
  109         108  
  112         368  
164 7         426 my @inc = (0) x $n;
165              
166 7 100       15 my $len = $n > 0 ? $dotslen - 1 : 0;
167 7 100 100     43 if (@key > 1 || (@key == 1 && $key[0] != 0)) {
      66        
168              
169 3         201 my @prio = (0) x $n;
170             # "prioritize" elements for display, giving preference to
171             # key items
172             # items between key items
173             # the first item
174             # the last item
175 3 50       12 if ($n > 0) {
176 3         34 $prio[$_] = 8 for @key;
177 3         36 $prio[$_] += 4 for $key[0]..$key[-1];
178             }
179 3         4 $prio[0] += 2;
180              
181 3         5 my $insert_fails = 0;
182 3 50       257 for my $i ( sort { $prio[$b] <=> $prio[$a] || $a <=> $b } 0..$n-1) {
  3073         4839  
183 325 100 100     1085 last if $prio[$i] < 8 && $len > $maxlen;
184              
185             # what are the consequences of including $array->[$i] in the output?
186             #
187             # if none of $array->[$i]'s neighbors are excluded:
188             # then we lose $dotslen and add length of $array->[$i]
189             # [ a , ... , c ] ==> [ a , b , c ]
190             #
191             # if one of $array->[$i]'s neighbors are excluded:
192             # then we add $array->[$i]
193             # [ a , ... ] => [ a , b , ... ]
194             # [ ... , c ] => [ ... , b , c ]
195             # [ edge ... ] => [ edge a , ... ]
196             #
197             # if two of $array->[$i]'s neighbors are excluded
198             # then we gain $dotslen + $array->[$i]
199             # [ ... ] => [ ... , a , ... ]
200              
201 324   100     1786 my $excl = ($i>0 && !$inc[$i-1]) + ($i<$n-1 && !$inc[$i+1]) + 0;
      33        
202 324   33     1219 my $dlen = defined($array->[$i])&&length($array->[$i])
203             + $seplen + $dotslen * ($excl - 1);
204 324 100 100     2057 if ($prio[$i] >= 8 || $len + $dlen <= $maxlen) {
205 122         144 $inc[$i] = 1;
206 122         117 $len += $dlen;
207 122         160 $insert_fails = 0;
208             } else {
209              
210             # for very large arrays, don't keep trying to squeeze
211             # that last element in when there is a low probability
212             # that it will work ...
213              
214 202 100       435 last if ++$insert_fails > 100;
215             }
216             }
217              
218             } else {
219              
220             # don't need to sort, don't need to check $prio[$i]
221 4         5 my $insert_fails = 0;
222 4         11 for my $i (0 .. $n-1) {
223 123 50       220 last if $len > $maxlen;
224 123   100     1098 my $excl = ($i>0 && !$inc[$i-1]) + ($i<$n-1 && !$inc[$i+1]) + 0;
      66        
225 123   33     727 my $dlen = defined($array->[$i])&&length($array->[$i])
226             + $seplen + $dotslen * ($excl - 1);
227 123 100       218 if ($len + $dlen <= $maxlen) {
228 20         22 $inc[$i] = 1;
229 20         32 $len += $dlen;
230             } else {
231 103 100       239 last if ++$insert_fails > 100;
232             }
233             }
234             }
235              
236             # construct array, including elements in @inc
237 7         91 my @result = ();
238 7         1058 my @explicit = grep $inc[$_], 0..$#inc;
239 7         115 my $i = 0;
240 7         19 while (@explicit) {
241 142 100       334 if ($i < $explicit[0]) {
242 4         6 push @result, $DOTDOTDOT;
243             }
244 142         144 $i = shift @explicit;
245 142         239 push @result, $array->[$i];
246 142         241 $i++;
247             }
248 7 100       18 if ($i < $n) {
249 5         8 push @result, $DOTDOTDOT;
250             }
251 7         234 return @result;
252             }
253              
254             sub shorten_hash {
255 48     48 1 10040 my ($hash, $maxlen, $sep1, $sep2, @key) = @_;
256              
257 48 50       108 if (!defined($sep1)) {
    0          
258 48         70 $sep1 = $DEFAULT_HASH_ELEM_SEPARATOR_LENGTH;
259             } elsif ($sep1 =~ /\D/) {
260 0         0 $sep1 = length($sep1);
261             }
262 48 50       79 if (!defined($sep2)) {
    0          
263 48         57 $sep2 = $DEFAULT_HASH_KEYVALUE_SEPARATOR_LENGTH;
264             } elsif ($sep2 =~ /\D/) {
265 0         0 $sep2 = length($sep2);
266             }
267              
268 48         59 my $total_len = -$sep1;
269 48         137 my @safekeys = safekeys %$hash;
270 48         1810 for my $k (@safekeys) {
271 359         612 $total_len += length($k) + length($hash->{$k}) + $sep1 + $sep2;
272 359 100       695 last if $total_len > $maxlen;
273             }
274 48 50       98 if ($total_len <= $maxlen) {
275             # ok to include all elements
276 0         0 return map { [ $_ , $hash->{$_} ] } @safekeys;
  0         0  
277             }
278              
279 48         100 my @r = ();
280 48         62 my @hashkeys = ();
281 48         67 my $hashkey = {};
282              
283 48 100       100 if (@key > 0) {
284 9         12 my %key = map { $_ => 1 } @key;
  18         40  
285 9 50       19 if (100 > @safekeys) {
286 0 0 0     0 @hashkeys = sort {
      0        
287 0         0 ($key{$b}||0) <=> ($key{$a}||0)
288             || $a cmp $b
289             } @safekeys;
290             } else {
291 9         11 @hashkeys = @key = grep { defined $key{$_} } @key;
  18         47  
292 9         35 push @hashkeys, grep { !defined $key{$_} } @safekeys;
  900         1194  
293             }
294             } else {
295 39 100       74 if (100 > @safekeys) {
296 25         275 @hashkeys = sort @safekeys;
297             } else {
298 14         17 $hashkey = $hash;
299             }
300             }
301              
302 48         75 my $len = 3;
303 48         130 my @sk = safekeys %$hashkey;
304 48 100       1132 if ($HASHREPR_SORTKEYS) {
305 2         175 @sk = sort @sk;
306             }
307              
308 48         89 foreach my $key (@hashkeys, @sk) {
309 350         581 my $dlen = $sep1 + $sep2 + length($key) + length($hash->{$key});
310 350 100 100     870 last if $len + $dlen > $maxlen
311             && @r > 0; # always include at least one key-value pair
312 302         630 push @r, [ $key, $hash->{$key} ];
313 302         424 $len += $dlen;
314             }
315 48         569 return @r, [ $DOTDOTDOT ];
316             }
317              
318             1;
319              
320             __END__