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-28'; # DATE
4             our $VERSION = '0.06'; # VERSION
5              
6 1     1   12477 use 5.010001;
  1         3  
7 1     1   3 use strict;
  1         1  
  1         16  
8 1     1   4 use warnings;
  1         1  
  1         22  
9              
10 1     1   2 use Exporter;
  1         1  
  1         662  
11             our @ISA = qw(Exporter);
12             our @EXPORT_OK = qw(elide);
13              
14             sub _elide_part {
15 48     48   61 my ($str, $len, $marker, $truncate) = @_;
16              
17 48         35 my $len_marker = length($marker);
18 48 100       58 if ($len <= $len_marker) {
19 13         25 return substr($marker, 0, $len);
20             }
21              
22 35 100       66 if ($truncate eq 'left') {
    100          
    100          
23 3         8 return $marker . substr($str, length($str) - $len+$len_marker);
24             } elsif ($truncate eq 'middle') {
25 12         17 my $left = substr($str, 0,
26             ($len-$len_marker)/2);
27 12         14 my $right = substr($str,
28             length($str) - ($len-$len_marker-length($left)));
29 12         24 return $left . $marker . $right;
30             } elsif ($truncate eq 'ends') {
31 7 100       12 if ($len <= 2*$len_marker) {
32 2         5 return substr($marker . $marker, 0, $len);
33             }
34 5         16 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 5652 my ($str, $len, $opts) = @_;
43              
44 60   100     122 $opts //= {};
45 60   100     124 my $truncate = $opts->{truncate} // 'right';
46 60   100     141 my $marker = $opts->{marker} // '..';
47 60   100     138 my $default_prio = $opts->{default_prio} // 1;
48              
49             # split into parts by priority
50 60         38 my @parts;
51             my @parts_attrs;
52 60         212 while ($str =~ m#]*)>(.*?)|(.*?)(?=
53 169 100       386 if (defined $1) {
    100          
    50          
54 32 50       48 next unless length $2;
55 32         38 push @parts, $2;
56 32         78 push @parts_attrs, $1;
57             } elsif (defined $3) {
58 17 50       26 next unless length $3;
59 17         20 push @parts, $3;
60 17         42 push @parts_attrs, undef;
61             } elsif (defined $4) {
62 120 100       270 next unless length $4;
63 60         64 push @parts, $4;
64 60         144 push @parts_attrs, undef;
65             }
66             }
67 60 100 66     197 return "" unless @parts && $len > 0;
68 56         108 for my $i (0..@parts-1) {
69 105 100       111 if (defined $parts_attrs[$i]) {
70 32         29 my $attrs = {};
71 32 100 33     87 $attrs->{truncate} = $1 // $2
72             if $parts_attrs[$i] =~ /\btruncate=(?:"([^"]*)"|(\S+))/;
73 32 100 33     77 $attrs->{marker} = $1 // $2
74             if $parts_attrs[$i] =~ /\bmarker=(?:"([^"]*)"|(\S+))/;
75 32 50 33     147 $attrs->{prio} = $1 // $2
76             if $parts_attrs[$i] =~ /\bprio(?:rity)?=(?:"([^"]*)"|(\S+))/;
77 32         37 $parts_attrs[$i] = $attrs;
78             } else {
79 73         116 $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         41 while (1) {
88             # (re)calculate total len of all parts
89 112         69 my $all_parts_len = 0;
90 112         161 $all_parts_len += length($_) for @parts;
91              
92             # total len of all parts is short enough, we're done
93 112 100       133 if ($all_parts_len <= $len) {
94 56         254 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         32 my $highest_prio;
100 56         46 for (@parts_attrs) {
101             $highest_prio = $_->{prio} if !defined($highest_prio) ||
102 113 100 100     272 $highest_prio < $_->{prio};
103             }
104 56         32 my @high_indexes;
105 56         43 my $high_parts_len = 0;
106 56         63 for my $i (0..$#parts_attrs) {
107 113 100       152 if ($parts_attrs[$i]{prio} == $highest_prio) {
108 61         47 $high_parts_len += length $parts[$i];
109 61         54 push @high_indexes, $i;
110             }
111             }
112              
113 56 100       79 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         8 for (reverse @high_indexes) {
117 10         10 splice @parts, $_, 1;
118 10         10 splice @parts_attrs, $_, 1;
119 10         19 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         32 my @must_elide_total_len_after_this;
128 46         29 my $tot_to_elide = $all_parts_len - $len;
129 46         45 for my $i (0..$#high_indexes) {
130 51         86 $must_elide_total_len_after_this[$i] =
131             int( ($i+1)/@high_indexes * $tot_to_elide );
132             }
133             # calculate how many characters to truncate for each part
134 46         30 my $tot_already_elided = 0;
135 46         30 my $tot_still_to_elide = 0;
136 46         56 for my $i (reverse 0..$#high_indexes) {
137 51         39 my $idx = $high_indexes[$i];
138 51         33 my $part_len = length $parts[$idx];
139 51         46 my $to_elide = $must_elide_total_len_after_this[$#high_indexes - $i] -
140             $tot_already_elided + $tot_still_to_elide;
141 51 50       74 if ($to_elide <= 0) {
    100          
142             # leave this part alone
143             } elsif ($part_len <= $to_elide) {
144             # we need to eliminate this part
145 3         3 splice @parts, $idx, 1;
146 3         3 splice @parts_attrs, $idx, 1;
147 3         3 $tot_already_elided += $part_len;
148 3         4 $tot_still_to_elide += ($to_elide - $part_len);
149             } else {
150             $parts[$idx] = _elide_part(
151             $parts[$idx],
152             $part_len - $to_elide,
153             $parts_attrs[$idx]{marker} // $marker,
154 48   66     191 $parts_attrs[$idx]{truncate} // $truncate,
      66        
155             );
156 48         46 $tot_already_elided += $to_elide;
157 48         76 $tot_still_to_elide = 0;
158             }
159             }
160              
161             } # while 1
162             }
163              
164             1;
165             # ABSTRACT: Elide a string with multiple parts of different priorities
166              
167             __END__