File Coverage

blib/lib/List/Prefixed.pm
Criterion Covered Total %
statement 120 121 99.1
branch 60 66 90.9
condition 14 15 93.3
subroutine 14 14 100.0
pod 4 4 100.0
total 212 220 96.3


line stmt bran cond sub pod time code
1             package List::Prefixed;
2 2     2   50299 use 5.004;
  2         7  
  2         82  
3 2     2   12 use strict;
  2         3  
  2         79  
4 2     2   10 use warnings;
  2         9  
  2         77  
5 2     2   15 use Carp;
  2         4  
  2         764  
6              
7             # -- globals --
8              
9             our $VERSION = '0.03';
10              
11             # Public global variables.
12             our (
13             $UC_ESCAPE_STYLE, # "PCRE" | "Java" | undef
14             $REGEX_ESCAPE, # CODE: Str -> Str
15             $REGEX_UNESCAPE # CODE: Str -> Str
16             );
17              
18             # The default unicode escape format is the PCRE style \x{FF}.
19             # - To output regular expressions in \uFFFF (Java, etc.) style, set $UC_ESCAPE_STYLE to 'Java'.
20             # - To avoid replacement of non-ASCII characters at all, set $UC_ESCAPE_STYLE to undef.
21             $UC_ESCAPE_STYLE = 'PCRE';
22              
23             # Unicode escape styles
24             my %UC_escape = (
25             PCRE => [ '\\x{%X}' => qr/\\x\{([0-9A-F]{2,6})}/i ],
26             Java => [ '\\u%04X' => qr/\\u([0-9A-F]{4,6})/i ],
27             );
28              
29             # Get the escape form (0) / unescape regex (1) depending on $UC_ESCAPE_STYLE.
30             my $UC_ESCAPE = sub {
31             return undef unless defined $UC_ESCAPE_STYLE;
32             return $UC_escape{$UC_ESCAPE_STYLE}[$_[0]] if exists $UC_escape{$UC_ESCAPE_STYLE};
33             croak "Unsupported UC_ESCAPE_STYLE: '$UC_ESCAPE_STYLE'"; # RTFM
34             };
35              
36             # The default escape procedure.
37             $REGEX_ESCAPE = sub {
38              
39             # quotemeta EXPR
40             # Returns the value of EXPR with all non-"word" characters backslashed.
41             # (That is, all characters not matching "/[A-Za-z_0-9]/" will be preceded by
42             # a backslash in the returned string, regardless of any locale settings.)
43             local $_ = quotemeta $_[0];
44              
45             # additionally apply $UC_ESCAPE_STYLE
46             if ( defined(my $uc_form = &$UC_ESCAPE(0)) ) {
47 2     2   1217 s/\\?(\P{ASCII})/ sprintf $uc_form => ord $1 /eg;
  2         20  
  2         26  
48             }
49            
50             $_;
51             };
52              
53             # The default unescape procedure.
54             $REGEX_UNESCAPE = sub {
55             local $_ = $_[0];
56              
57             # apply $UC_ESCAPE_STYLE first
58             if ( defined(my $uc_re = &$UC_ESCAPE(1)) ) {
59             s/$uc_re/ chr hex $1 /eg;
60             }
61              
62             # replace backslash escapes as inserted by quotemeta
63             s/\\([^A-Z0-9_])/$1/gi;
64            
65             $_;
66             };
67              
68             # Globals may passed as module arguments, e.g.:
69             #
70             # use List::Prefixed uc_escape_style => 'Java';
71             #
72             sub import {
73 2     2   22 shift; # __PACKAGE__;
74 2         4 my %args = @_;
75              
76 2 50       9 $UC_ESCAPE_STYLE = $args{uc_escape_style} if exists $args{uc_escape_style};
77 2 50       6 $REGEX_ESCAPE = $args{regex_escape} if exists $args{regex_escape};
78 2 50       33 $REGEX_UNESCAPE = $args{regex_unescape} if exists $args{regex_unescape};
79             }
80              
81             # -- public --
82              
83             # Construct a Prefixed object from a string list
84             sub fold {
85              
86             local *reduce = sub {
87             # Large lists may cause a 'Deep recursion on anonymous subroutine' warning,
88             # see http://perldoc.perl.org/perldiag.html
89 2     2   37034 no warnings 'recursion';
  2         5  
  2         1839  
90              
91 203180     203180   151185 my ($prefix, $nlist, $opt) = @{$_[0]};
  203180         277263  
92              
93 203180 100 66     711301 return $_[0] unless ref $nlist && @$nlist > 1;
94              
95             # 1st char of the prefix of 1st node in list
96 191600         136895 my ($c);
97              
98             # check whether 2nd prefix starts with same letter as the 1st
99 191600 50       306313 if ( length $nlist->[0][0] ) {
100 191600         236547 $c = substr $nlist->[0][0], 0, 1; # first char
101 191600 100       372334 undef $c unless $c eq substr $nlist->[1][0], 0, 1;
102             }
103              
104 191600 100       278702 unless ( defined $c )
105             {
106 40342 100       90922 return $_[0] unless @$nlist > 2;
107              
108             # try to reduce next list part
109 24886         25896 my $first = shift @$nlist;
110 24886         57885 my $next = reduce(bless ['', $nlist, 0]);
111 24886 100       62239 return bless [ $prefix, [ $first, $next ], $opt] if length $next->[0];
112              
113             # couldn't be reduced
114 19825         18244 return bless [ $prefix, [ $first, @{$next->[1]} ], $opt ];
  19825         77756  
115             }
116              
117             # reduce any ensuing node whose prefix starts with $c
118 151258         131463 my @new;
119 151258         122945 my $newopt = undef;
120 151258         152469 my $qc = quotemeta $c;
121 151258         219671 while ( @$nlist )
122             {
123 1586200 100       5765743 last unless $nlist->[0][0] =~ s/^$qc//;
124              
125             # reduce node or detect new optional node
126 1559177         1544025 my $n = shift @$nlist;
127 1559177 100       2290486 if ( length $n->[0] )
128             {
129 1544988         1278684 push @new, $n;
130 1544988         2503474 next;
131             }
132 14189         27988 $newopt = 1;
133             }
134              
135 151258 100 100     451028 if ( @$nlist || $opt )
136             {
137 33980         159319 my $new = reduce(bless [ $c, [ @new ], $newopt ]);
138 33980 100       72362 if ( @$nlist )
139             {
140             # reduce remaining nlist
141 27023         52448 my $next = reduce(bless ['', $nlist, 0]);
142 27023 100       74889 return bless [ $prefix, [ $new, $next ], $opt] if length $next->[0];
143              
144             # couldn't be reduced
145 22766         20669 return bless [ $prefix, [ $new, @{$next->[1]} ], $opt ];
  22766         153656  
146             }
147              
148             # current node is optional
149 6957         61972 return bless [ $prefix, [ $new, @$nlist ], $opt ];
150             }
151              
152             # nothing left to reduce
153 117278         451921 reduce(bless [ $prefix.$c, [ @new ], $newopt ]);
154 15     15 1 5372582 };
155              
156 15         31 shift; # __PACKAGE__
157 15         27 my @s = sort keys %{ { map{ $_ => 1 } @_ } }; # unique sorted
  15         208  
  81569         312939  
158 15 100       40480 return bless [$_[0],[],undef] if @s == 1; # singleton
159 13         414 reduce(bless [ '', [( map { [$_] } @s )],undef]);
  81566         102447  
160             };
161              
162             my $RE_PREFIX = qr/(?:\\(?:.|\n)|[^\|\(\)])+/;
163              
164             # Construct a Prefixed object from a regular expression.
165             #
166             # Solution based on basic regular expression evaluation.
167             sub unfold {
168 8     8 1 248254 shift; # __PACKAGE__
169 8         21 my ($regex) = @_;
170 8         13 my ($nn,$cn,@st);
171 8         35 while ( length $regex ) {
172              
173             # prefix string
174 197281 100       10259835 if ( $regex =~ s/^\|?($RE_PREFIX)// ) {
175 110669         241919 my $p = &$REGEX_UNESCAPE($1);
176 110669 100       170385 if ( $cn ) {
177 110666 100       299818 $cn->[1] = [] unless ref $cn->[1];
178 110666         97015 push @{$cn->[1]}, ($nn = bless [$p]);
  110666         271789  
179             }
180             else {
181 3         7 $cn = $nn = bless [$p];
182             }
183 110669         248151 next;
184             }
185              
186             # node start
187 86612 100       3836718 if ( $regex =~ s/^\(\?:// ) {
188 43306 100       79356 if ( $nn ) {
189 43301         58620 $cn = $nn;
190             }
191             else {
192 5         18 $cn = $nn = bless [''];
193             }
194 43306         55023 push @st, $nn;
195 43306         109115 next;
196             }
197              
198             # node end
199 43306 50       3764672 if ( $regex =~ s/^\)(\?)?// ) {
200 43306         64656 $cn = pop @st;
201 43306 100       150769 $cn->[2] = defined $1 ? 1 : undef;
202 43306 100       84121 $cn = $st[$#st] if @st;
203 43306         108346 next;
204             }
205              
206 0         0 die "invalid: '$regex'";
207             }
208 8         22 $cn;
209             };
210              
211              
212             sub regex {
213              
214             local *to_regex_rec = sub {
215 110690     110690   75991 my ($prefix, $nlist, $opt) = @{$_[0]};
  110690         181136  
216 110690         133563 my $rv = &$REGEX_ESCAPE($prefix);
217 110690 100 100     255953 if ( $nlist && @$nlist )
218             {
219 43310         51038 $rv .= '(?:'.(join '|', map { to_regex_rec($_) } @$nlist).')';
  110675         133756  
220 43310 100       76068 $rv .= '?' if $opt;
221             }
222 110690         211343 $rv;
223 15     15 1 13176 };
224              
225 15         32 my $node = shift;
226 15         43 to_regex_rec($node);
227             }
228              
229             sub list {
230 60     60 1 32941638 my $node = shift;
231 60         90 my ($s) = @_;
232 60 100       129 $s = '' unless defined $s;
233 60         89 my $qs = quotemeta $s;
234              
235             local *list_rec = sub {
236 469     469   473 my ($p,$n,$list) = @_;
237 469         582 my ($prefix, $nlist, $opt) = @$n;
238 469         1073 my $p2 = $p.$prefix;
239 469         470 my $qp2 = quotemeta $p2;
240 469         295 my ($push,$continue);
241 469 100       2593 if ( $p2 =~ m/^$qs/ ) {
    100          
242             # current prefix starts with search string
243 305         318 $push = $continue = 1;
244             }
245             elsif ( $s =~ m/^$qp2/ ) {
246             # search string starts with current prefix
247 41         54 $continue = 1;
248             }
249              
250 469 100 100     1394 if ($nlist && @$nlist) {
    100          
251 234 100 100     566 push @$list, $p2 if $push && $opt;
252 234 100       411 if ( $continue ) {
253 167         499 list_rec($p2,$_,$list) foreach @$nlist;
254             }
255             }
256             elsif ( $push ) {
257 179         563 push @$list, $p2;
258             }
259 60         319 };
260              
261 60         70 my @list;
262 60         107 list_rec('',$node,\@list);
263 60 50       792 wantarray ? @list : \@list;
264             }
265              
266              
267             # Standard new constructor is an alias for fold.
268             *new = \&fold;
269              
270             1;
271             __END__