File Coverage

blib/lib/String/Truncate.pm
Criterion Covered Total %
statement 74 74 100.0
branch 28 28 100.0
condition 5 7 71.4
subroutine 19 19 100.0
pod 2 2 100.0
total 128 130 98.4


line stmt bran cond sub pod time code
1 3     3   93782 use strict;
  3         8  
  3         126  
2 3     3   20 use warnings;
  3         8  
  3         212  
3             package String::Truncate;
4             # ABSTRACT: a module for when strings are too long to be displayed in...
5             $String::Truncate::VERSION = '1.100602';
6 3     3   21 use Carp qw(croak);
  3         6  
  3         597  
7 3     3   3488 use Sub::Install 0.03 qw(install_sub);
  3         9811  
  3         21  
8              
9             # =head1 SYNOPSIS
10             #
11             # This module handles the simple but common problem of long strings and finite
12             # terminal width. It can convert:
13             #
14             # "this is your brain" -> "this is your ..."
15             # or "...is your brain"
16             # or "this is... brain"
17             # or "... is your b..."
18             #
19             # It's simple:
20             #
21             # use String::Truncate qw(elide);
22             #
23             # my $brain = "this is your brain";
24             #
25             # elide($brain, 16); # first option
26             # elide($brain, 16, { truncate => 'left' }); # second option
27             # elide($brain, 16, { truncate => 'middle' }); # third option
28             # elide($brain, 16, { truncate => 'ends' }); # fourth option
29             #
30             # String::Trunc::trunc($brain, 16); # => "this is your bra"
31             #
32             # =func elide
33             #
34             # elide($string, $length, \%arg)
35             #
36             # This function returns the string, if it is less than or equal to C<$length>
37             # characters long. If it is longer, it truncates the string and marks the
38             # elision.
39             #
40             # Valid arguments are:
41             #
42             # truncate - elide at left, right, middle, or ends? (default: right)
43             # marker - how to mark the elision (default: ...)
44             # at_space - if true, strings will be broken at whitespace if possible
45             #
46             # =cut
47              
48             my %elider_for = (
49             right => \&_elide_right,
50             left => \&_elide_left,
51             middle => \&_elide_middle,
52             ends => \&_elide_ends,
53             );
54              
55             sub _elide_right {
56 38     38   102 &_assert_1ML; ## no critic Ampersand
57 37         62 my ($string, $length, $marker, $at_space) = @_;
58 37         55 my $keep = $length - length($marker);
59              
60 37 100       67 if ($at_space) {
61            
62 11         225 my ($substr) = $string =~ /\A(.{0,$keep})\s/s;
63 11 100 66     73 $substr = substr($string, 0, $keep)
64             unless defined $substr and length $substr;
65              
66 11         84 return $substr . $marker;
67             } else {
68 26         263 return substr($string, 0, $keep) . $marker;
69             }
70             }
71              
72             sub _elide_left {
73 18     18   35 &_assert_1ML; ## no critic Ampersand
74 18         32 my ($string, $length, $marker, $at_space) = @_;
75 18         32 my $keep = $length - length($marker);
76 18         66 return $marker
77             . reverse(_elide_right(scalar reverse($string), $keep, q{}, $at_space));
78             }
79              
80             sub _elide_middle {
81 5     5   13 &_assert_1ML; ## no critic Ampersand
82 5         10 my ($string, $length, $marker, $at_space) = @_;
83 5         8 my $keep = $length - length($marker);
84 5         16 my ($keep_left, $keep_right) = (int($keep / 2)) x 2;
85 5 100       16 $keep_left +=1 if ($keep_left + $keep_right) < $keep;
86 5         12 return _elide_right($string, $keep_left, q{}, $at_space)
87             . $marker
88             . _elide_left($string, $keep_right, q{}, $at_space)
89             }
90              
91             sub _elide_ends {
92 3     3   7 &_assert_2ML; ## no critic Ampersand
93 2         4 my ($string, $length, $marker, $at_space) = @_;
94 2         6 my $midpoint = int(length($string) / 2);
95 2         4 my $each = int($length / 2);
96              
97 2         12 return _elide_left(substr($string, 0, $midpoint), $each, $marker, $at_space)
98             . _elide_right(substr($string, -$midpoint), $each, $marker, $at_space)
99             }
100              
101             sub _assert_1ML {
102 61     61   162 my ($string, $length, $marker) = @_;
103 61 100       382 croak "elision marker <$marker> is longer than allowed length $length!"
104             if length($marker) > $length;
105             }
106              
107             sub _assert_2ML {
108 3     3   6 my ($string, $length, $marker) = @_;
109             # this should only complain if needed: elide('foobar', 3, {marker=>'...'})
110             # should be ok -- rjbs, 2006-02-24
111 3 100       268 croak "two elision markers <$marker> are longer than allowed length $length!"
112             if (length($marker) * 2) > $length;
113             }
114              
115             sub elide {
116 43     43 1 382 my ($string, $length, $arg) = @_;
117 43 100       109 $arg = {} unless $arg;
118 43   100     150 my $truncate = $arg->{truncate} || 'right';
119              
120 43 100       256 croak "invalid value for truncate argument: $truncate"
121             unless my $elider = $elider_for{ $truncate };
122              
123             # hey, this might be really easy:
124 42 100       146 return $string if length($string) <= $length;
125              
126 32 100       81 my $marker = defined $arg->{marker} ? $arg->{marker} : '...';
127 32 100       71 my $at_space = defined $arg->{at_space} ? $arg->{at_space} : 0;
128            
129 32         79 return $elider->($string, $length, $marker, $at_space);
130             }
131            
132             # =func trunc
133             #
134             # trunc($string, $length, \%arg)
135             #
136             # This acts just like C, but assumes an empty marker, so it actually
137             # truncates the string normally.
138             #
139             # =cut
140              
141             sub trunc {
142 14     14 1 345 my ($string, $length, $arg) = @_;
143 14 100       42 $arg = {} unless $arg;
144              
145 14 100       160 croak "marker may not be passed to trunc()" if exists $arg->{marker};
146 13         47 $arg->{marker} = q{};
147              
148 13         34 return elide($string, $length, $arg);
149             }
150              
151             # =head1 IMPORTING
152             #
153             # String::Truncate exports both C and C, and also supports the
154             # Exporter-style ":all" tag.
155             #
156             # use String::Truncate (); # export nothing
157             # use String::Truncate qw(elide); # export just elide()
158             # use String::Truncate qw(:all); # export both elide() and trunc()
159             # use String::Truncate qw(-all); # export both elide() and trunc()
160             #
161             # When exporting, you may also supply default values:
162             #
163             # use String::Truncate -all => defaults => { length => 10, marker => '--' };
164             #
165             # # or
166             #
167             # use String::Truncate -all => { length => 10, marker => '--' };
168             #
169             # These values affect only the imported version of the functions. You may pass
170             # arguments as usual to override them, and you may call the subroutine by its
171             # fully-qualified name to get the standard behavior.
172             #
173             # =cut
174              
175 3     3   21309 use Sub::Exporter::Util ();
  3         80701  
  3         342  
176             use Sub::Exporter 0.953 -setup => {
177             exports => {
178             Sub::Exporter::Util::merge_col(defaults => {
179 8         3283 trunc => sub { trunc_with_defaults($_[2]) },
180 8         4888 elide => sub { elide_with_defaults($_[2]) },
181             })
182 3         41 },
183             collectors => [ qw(defaults) ]
184 3     3   38 };
  3         96  
185              
186             # =head1 BUILDING CODEREFS
187             #
188             # The imported builds and installs lexical closures (code references) that merge
189             # in given values to the defaults. You can build your own closures without
190             # importing them into your namespace. To do this, use the C
191             # and C routines.
192             #
193             # =head2 elide_with_defaults
194             #
195             # my $elider = String::Truncate::elide_with_defaults(\%arg);
196             #
197             # This routine, never exported, builds a coderef which behaves like C, but
198             # uses default values when needed. All the valid arguments to C are valid
199             # here, as well as C.
200             #
201             # =cut
202              
203             sub _code_with_defaults {
204 6     6   13 my ($code, $skip_defaults) = @_;
205            
206             sub {
207 16   50 16   55 my $defaults = shift || {};
208 16         73 my %defaults = %$defaults;
209 16         54 delete $defaults{$_} for @$skip_defaults;
210              
211 16         31 my $length = delete $defaults{length};
212              
213             sub {
214 40     40   4675 my $string = $_[0];
215 40 100       111 my $length = defined $_[1] ? $_[1] : $length;
216 40 100       116 my $arg = { %defaults, (defined $_[2] ? %{ $_[2] } : ()) };
  22         86  
217              
218 40         110 return $code->($string, $length, $arg);
219             }
220 16         126 }
221 6         49 }
222              
223             BEGIN {
224 3     3   2216 install_sub({
225             code => _code_with_defaults(\&elide),
226             as => 'elide_with_defaults',
227             });
228             }
229              
230             # =head2 trunc_with_defaults
231             #
232             # This routine behaves exactly like elide_with_defaults, with one obvious
233             # exception: it returns code that works like C rather than C. If a
234             # C argument is passed, it is ignored.
235             #
236             # =cut
237              
238             BEGIN {
239 3     3   299 install_sub({
240             code => _code_with_defaults(\&trunc, ['marker']),
241             as => 'trunc_with_defaults',
242             });
243             }
244              
245             # =head1 SEE ALSO
246             #
247             # L does a very similar thing. So does L.
248             #
249             # =head1 BUGS
250             #
251             # Please report any bugs or feature requests through the web interface at
252             # L. I will be
253             # notified, and then you'll automatically be notified of progress on your bug as
254             # I make changes.
255             #
256             # =head1 ACKNOWLEDGEMENTS
257             #
258             # Ian Langworth gave me some good advice about naming things. (Also some bad
259             # jokes. Nobody wants String::ETOOLONG, Ian.) Hans Dieter Pearcey suggested
260             # allowing defaults just in time for a long bus ride, and I was rescued from
261             # boredom by that suggestion
262             #
263             # =cut
264              
265             1; # End of String::Truncate
266              
267             __END__