File Coverage

blib/lib/String/Elide/Parts.pm
Criterion Covered Total %
statement 87 87 100.0
branch 39 44 88.6
condition 20 29 68.9
subroutine 6 6 100.0
pod 1 1 100.0
total 153 167 91.6


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