File Coverage

blib/lib/String/Elide/Parts.pm
Criterion Covered Total %
statement 87 87 100.0
branch 39 44 88.6
condition 18 27 66.6
subroutine 6 6 100.0
pod 1 1 100.0
total 151 165 91.5


line stmt bran cond sub pod time code
1             package String::Elide::Parts;
2              
3             our $DATE = '2017-01-28'; # DATE
4             our $VERSION = '0.05'; # VERSION
5              
6 1     1   13562 use 5.010001;
  1         2  
7 1     1   4 use strict;
  1         1  
  1         18  
8 1     1   4 use warnings;
  1         1  
  1         24  
9              
10 1     1   3 use Exporter;
  1         1  
  1         965  
11             our @ISA = qw(Exporter);
12             our @EXPORT_OK = qw(elide);
13              
14             sub _elide_part {
15 44     44   63 my ($str, $len, $marker, $truncate) = @_;
16              
17 44         36 my $len_marker = length($marker);
18 44 100       55 if ($len <= $len_marker) {
19 10         27 return substr($marker, 0, $len);
20             }
21              
22 34 100       71 if ($truncate eq 'left') {
    100          
    100          
23 3         23 return $marker . substr($str, length($str) - $len+$len_marker);
24             } elsif ($truncate eq 'middle') {
25 12         21 my $left = substr($str, 0,
26             ($len-$len_marker)/2);
27 12         19 my $right = substr($str,
28             length($str) - ($len-$len_marker-length($left)));
29 12         29 return $left . $marker . $right;
30             } elsif ($truncate eq 'ends') {
31 7 100       12 if ($len <= 2*$len_marker) {
32 2         9 return substr($marker . $marker, 0, $len);
33             }
34 5         18 return $marker . substr($str, (length($str)-$len)/2 + $len_marker,
35             $len-2*$len_marker) . $marker;
36             } else { # right
37 12         33 return substr($str, 0, $len-$len_marker) . $marker;
38             }
39             }
40              
41             sub elide {
42 58     58 1 6532 my ($str, $len, $opts) = @_;
43              
44 58   100     144 $opts //= {};
45 58   100     119 my $truncate = $opts->{truncate} // 'right';
46 58   100     159 my $marker = $opts->{marker} // '..';
47              
48             # split into parts by priority
49 58         45 my @parts;
50             my @parts_attrs;
51 58         262 while ($str =~ m#]*)>(.*?)|(.*?)(?=
52 161 100       412 if (defined $1) {
    100          
    50          
53 30 50       46 next unless length $2;
54 30         39 push @parts, $2;
55 30         75 push @parts_attrs, $1;
56             } elsif (defined $3) {
57 15 50       23 next unless length $3;
58 15         17 push @parts, $3;
59 15         42 push @parts_attrs, undef;
60             } elsif (defined $4) {
61 116 100       270 next unless length $4;
62 58         72 push @parts, $4;
63 58         151 push @parts_attrs, undef;
64             }
65             }
66 58 100 66     205 return "" unless @parts && $len > 0;
67 54         112 for my $i (0..@parts-1) {
68 99 100       105 if (defined $parts_attrs[$i]) {
69 30         23 my $attrs = {};
70 30 100 33     87 $attrs->{truncate} = $1 // $2
71             if $parts_attrs[$i] =~ /\btruncate=(?:"([^"]*)"|(\S+))/;
72 30 100 33     84 $attrs->{marker} = $1 // $2
73             if $parts_attrs[$i] =~ /\bmarker=(?:"([^"]*)"|(\S+))/;
74 30 50 33     156 $attrs->{prio} = $1 // $2
75             if $parts_attrs[$i] =~ /\bprio(?:rity)?=(?:"([^"]*)"|(\S+))/;
76 30         37 $parts_attrs[$i] = $attrs;
77             } else {
78 69         123 $parts_attrs[$i] = {prio=>1};
79             }
80             }
81              
82             #use DD; dd \@parts; dd \@parts_attrs;
83              
84             # used to flip and flop between eliding left and right end, used when
85             # truncate is 'ends'
86 54         48 my $flip = 0;
87              
88             # elide and truncate prio by prio until str is short enough
89             PRIO:
90 54         34 while (1) {
91             # (re)calculate total len of all parts
92 108         69 my $all_parts_len = 0;
93 108         178 $all_parts_len += length($_) for @parts;
94              
95             # total len of all parts is short enough, we're done
96 108 100       151 if ($all_parts_len <= $len) {
97 54         302 return join("", @parts);
98             }
99              
100             # we still need to elide some parts. first collect part indexes that
101             # have the current largest priority.
102 54         34 my $highest_prio;
103 54         44 for (@parts_attrs) {
104             $highest_prio = $_->{prio} if !defined($highest_prio) ||
105 107 100 100     265 $highest_prio < $_->{prio};
106             }
107 54         28 my @high_indexes;
108 54         41 my $high_parts_len = 0;
109 54         73 for my $i (0..$#parts_attrs) {
110 107 100       139 if ($parts_attrs[$i]{prio} == $highest_prio) {
111 56         52 $high_parts_len += length $parts[$i];
112 56         56 push @high_indexes, $i;
113             }
114             }
115              
116 54 100       78 if ($all_parts_len - $high_parts_len >= $len) {
117             # we need to fully eliminate all the highest parts part then search
118             # for another set of parts
119 10         11 for (reverse @high_indexes) {
120 10         10 splice @parts, $_, 1;
121 10         9 splice @parts_attrs, $_, 1;
122 10         20 next PRIO;
123             }
124             }
125              
126             # elide all to-be-elided parts equally
127              
128             # after this position, must elide a total of this number of characters
129             # after this position
130 44         27 my @must_elide_total_len_after_this;
131 44         34 my $tot_to_elide = $all_parts_len - $len;
132 44         44 for my $i (0..$#high_indexes) {
133 46         96 $must_elide_total_len_after_this[$i] =
134             int( ($i+1)/@high_indexes * $tot_to_elide );
135             }
136             # calculate how many characters to truncate for each part
137 44         26 my $tot_already_elided = 0;
138 44         34 my $tot_still_to_elide = 0;
139 44         70 for my $i (reverse 0..$#high_indexes) {
140 46         33 my $idx = $high_indexes[$i];
141 46         35 my $part_len = length $parts[$idx];
142 46         52 my $to_elide = $must_elide_total_len_after_this[$#high_indexes - $i] -
143             $tot_already_elided + $tot_still_to_elide;
144 46 50       68 if ($to_elide <= 0) {
    100          
145             # leave this part alone
146             } elsif ($part_len <= $to_elide) {
147             # we need to eliminate this part
148 2         2 splice @parts, $idx, 1;
149 2         2 splice @parts_attrs, $idx, 1;
150 2         2 $tot_already_elided += $part_len;
151 2         4 $tot_still_to_elide += ($to_elide - $part_len);
152             } else {
153             $parts[$idx] = _elide_part(
154             $parts[$idx],
155             $part_len - $to_elide,
156             $parts_attrs[$idx]{marker} // $marker,
157 44   66     199 $parts_attrs[$idx]{truncate} // $truncate,
      66        
158             );
159 44         43 $tot_already_elided += $to_elide;
160 44         73 $tot_still_to_elide = 0;
161             }
162             }
163              
164             } # while 1
165             }
166              
167             1;
168             # ABSTRACT: Elide a string with multiple parts of different priorities
169              
170             __END__