File Coverage

blib/lib/No/Worries/String.pm
Criterion Covered Total %
statement 33 105 31.4
branch 10 48 20.8
condition 0 10 0.0
subroutine 8 13 61.5
pod 6 6 100.0
total 57 182 31.3


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: No/Worries/String.pm #
4             # #
5             # Description: string handling without worries #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package No::Worries::String;
14 20     20   82818 use strict;
  20         54  
  20         723  
15 20     20   126 use warnings;
  20         55  
  20         1797  
16             our $VERSION = "1.5";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.13 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 20     20   1153 use No::Worries::Export qw(export_control);
  20         55  
  20         134  
24 20     20   143 use Params::Validate qw(validate validate_pos :types);
  20         44  
  20         41651  
25              
26             #
27             # global variables
28             #
29              
30             our(
31             @_ByteSuffix, # byte suffixes used by bytefmt
32             @_Map, # mapping of characters to escaped strings
33             %_Plural, # pluralization cache
34             );
35              
36             #
37             # format a number of bytes
38             #
39              
40             sub string_bytefmt ($;$) {
41 0     0 1 0 my($number, $precision) = @_;
42 0         0 my($index);
43              
44 0 0       0 $precision = 2 unless defined($precision);
45 0         0 $index = 0;
46 0   0     0 while ($_ByteSuffix[$index] and $number > 1024) {
47 0         0 $index++;
48 0         0 $number /= 1024.0;
49             }
50 0 0       0 return("$number $_ByteSuffix[$index]") if $number =~ /^\d+$/;
51 0         0 return(sprintf("%.${precision}f %s", $number, $_ByteSuffix[$index]));
52             }
53              
54             #
55             # escape a string (quite compact, human friendly but not Perl eval()'able)
56             #
57              
58             sub string_escape ($) {
59 6     6 1 88 my($string) = @_;
60 6         7 my(@list);
61              
62 6         76 validate_pos(@_, { type => SCALAR });
63 6         33 foreach my $ord (map(ord($_), split(//, $string))) {
64 23 100       60 push(@list, $ord < 256 ? $_Map[$ord] : sprintf("\\x{%04x}", $ord));
65             }
66 6         41 return(join("", @list));
67             }
68              
69             #
70             # return the plural form of the given noun
71             #
72              
73             sub string_plural ($) {
74 3     3 1 7 my($noun) = @_;
75              
76 3 100       12 unless ($_Plural{$noun}) {
77 2 50       23 if ($noun =~ /(ch|s|sh|x|z)$/) {
    100          
    50          
    50          
    50          
78 0         0 $_Plural{$noun} = $noun . "es";
79             } elsif ($noun =~ /[bcdfghjklmnpqrstvwxz]y$/) {
80 1         5 $_Plural{$noun} = substr($noun, 0, -1) . "ies";
81             } elsif ($noun =~ /f$/) {
82 0         0 $_Plural{$noun} = substr($noun, 0, -1) . "ves";
83             } elsif ($noun =~ /fe$/) {
84 0         0 $_Plural{$noun} = substr($noun, 0, -2) . "ves";
85             } elsif ($noun =~ /[bcdfghjklmnpqrstvwxz]o$/) {
86 0         0 $_Plural{$noun} = $noun . "es";
87             } else {
88 1         3 $_Plural{$noun} = $noun . "s";
89             }
90             }
91 3         11 return($_Plural{$noun});
92             }
93              
94             #
95             # quantify the given (count, noun) pair
96             #
97              
98             sub string_quantify ($$) {
99 0     0 1 0 my($count, $noun) = @_;
100              
101 0 0       0 return($count . " " . ($count == 1 ? $noun : string_plural($noun)));
102             }
103              
104             #
105             # return the real length of a string (removing ANSI Escape sequences)
106             #
107              
108             sub _strlen ($) {
109 0     0   0 my($string) = @_;
110              
111 0 0       0 return(0) unless defined($string);
112 0         0 $string =~ s/\x1b\[[0-9;]*[mGKH]//g;
113 0         0 return(length($string));
114             }
115              
116             #
117             # return an aligned and padded string
118             #
119              
120             sub _strpad ($$$) {
121 0     0   0 my($string, $length, $align) = @_;
122 0         0 my($strlen, $before, $after);
123              
124 0 0       0 $string = "" unless defined($string);
125 0         0 $strlen = _strlen($string);
126 0   0     0 $align ||= "left";
127 0 0       0 if ($align eq "left") {
    0          
    0          
128 0         0 $before = 0;
129 0         0 $after = $length - $strlen;
130             } elsif ($align eq "right") {
131 0         0 $before = $length - $strlen;
132 0         0 $after = 0;
133             } elsif ($align eq "center") {
134 0         0 $before = ($length - $strlen) >> 1;
135 0         0 $after = $length - $strlen - $before;
136             } else {
137 0         0 die("unexpected alignment: $align\n");
138             }
139 0         0 return((" " x $before) . $string . (" " x $after));
140             }
141              
142             #
143             # transform a table into a string
144             #
145              
146             my %string_table_options = (
147             align => { optional => 1, type => ARRAYREF },
148             colsep => { optional => 1, type => SCALAR },
149             header => { optional => 1, type => ARRAYREF },
150             headsep => { optional => 1, type => SCALAR },
151             indent => { optional => 1, type => SCALAR },
152             );
153              
154             sub string_table ($@) {
155 0     0 1 0 my($lines, %option, @collen, $index, $length, $result);
156              
157             # handle options
158 0         0 $lines = shift(@_);
159 0 0       0 %option = validate(@_, \%string_table_options) if @_;
160 0   0     0 $option{align} ||= [];
161 0 0       0 $option{colsep} = " | " unless defined($option{colsep});
162 0 0       0 $option{headsep} = "=" unless defined($option{headsep});
163 0 0       0 $option{indent} = "" unless defined($option{indent});
164             # compute column lengths
165 0 0       0 foreach my $line ($option{header} ? ($option{header}) : (), @{ $lines }) {
  0         0  
166 0         0 $index = 0;
167 0         0 foreach my $entry (@{ $line }) {
  0         0  
168 0         0 $length = _strlen($entry);
169 0 0 0     0 $collen[$index] = $length
170             unless defined($collen[$index]) and $collen[$index] >= $length;
171 0         0 $index++;
172             }
173             }
174             # compute total length
175 0         0 $length = length($option{colsep}) * (@collen - 1);
176 0         0 foreach my $collen (@collen) {
177 0         0 $length += $collen;
178             }
179 0         0 $result = "";
180             # format header
181 0 0       0 if ($option{header}) {
182 0         0 $result .= $option{indent};
183 0         0 $index = 0;
184 0         0 while ($index < @collen) {
185 0 0       0 $result .= $option{colsep} if $index;
186             $result .= _strpad($option{header}[$index],
187 0         0 $collen[$index], $option{align}[$index]);
188 0         0 $index++;
189             }
190 0         0 $result .= "\n";
191 0         0 $result .= $option{indent};
192 0         0 $result .= substr($option{headsep} x $length, 0, $length) . "\n";
193             }
194             # format lines
195 0         0 foreach my $line (@{ $lines }) {
  0         0  
196 0         0 $result .= $option{indent};
197 0         0 $index = 0;
198 0         0 while ($index < @collen) {
199 0 0       0 $result .= $option{colsep} if $index;
200             $result .= _strpad($line->[$index],
201 0         0 $collen[$index], $option{align}[$index]);
202 0         0 $index++;
203             }
204 0         0 $result .= "\n";
205             }
206 0         0 return($result);
207             }
208              
209             #
210             # remove leading and trailing spaces
211             #
212              
213             sub string_trim ($) {
214 37     37 1 87 my($string) = @_;
215              
216 37         480 validate_pos(@_, { type => SCALAR });
217 37         219 $string =~ s/^\s+//;
218 37         166 $string =~ s/\s+$//;
219 37         262 return($string);
220             }
221              
222             #
223             # module initialization
224             #
225              
226             @_ByteSuffix = qw(B kB MB GB TB PB EB ZB YB);
227             foreach my $ord (0 .. 255) {
228             $_Map[$ord] = 32 <= $ord && $ord < 127 ?
229             chr($ord) : sprintf("\\x%02x", $ord);
230             }
231             $_Map[ord("\t")] = "\\t";
232             $_Map[ord("\n")] = "\\n";
233             $_Map[ord("\r")] = "\\r";
234             $_Map[ord("\e")] = "\\e";
235             $_Map[ord("\\")] = "\\\\";
236             %_Plural = (
237             "child" => "children",
238             "data" => "data",
239             "foot" => "feet",
240             "index" => "indices",
241             "man" => "men",
242             "tooth" => "teeth",
243             "woman" => "women",
244             );
245              
246             #
247             # export control
248             #
249              
250             sub import : method {
251 21     21   86 my($pkg, %exported);
252              
253 21         70 $pkg = shift(@_);
254 21         303 grep($exported{$_}++, map("string_$_",
255             qw(bytefmt escape plural quantify table trim)));
256 21         175 export_control(scalar(caller()), $pkg, \%exported, @_);
257             }
258              
259             1;
260              
261             __DATA__