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 1     1   39634 use 5.014002;
  1         5  
  1         45  
3 1     1   7 use strict;
  1         2  
  1         43  
4 1     1   6 use warnings;
  1         7  
  1         39  
5 1     1   7 use Carp;
  1         2  
  1         392  
6              
7             # -- globals --
8              
9             our $VERSION = '0.02';
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 1     1   831 s/\\?(\P{ASCII})/ sprintf $uc_form => ord $1 /eg;
  1         10  
  1         18  
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 1     1   15 shift; # __PACKAGE__;
74 1         4 my %args = @_;
75              
76 1 50       7 $UC_ESCAPE_STYLE = $args{uc_escape_style} if exists $args{uc_escape_style};
77 1 50       4 $REGEX_ESCAPE = $args{regex_escape} if exists $args{regex_escape};
78 1 50       18 $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 1     1   25877 no warnings 'recursion';
  1         2  
  1         1311  
90              
91 54     54   52 my ($prefix, $nlist, $opt) = @{$_[0]};
  54         99  
92              
93 54 100 66     332 return $_[0] unless ref $nlist && @$nlist > 1;
94              
95             # 1st char of the prefix of 1st node in list
96 43         43 my ($c);
97              
98             # check whether 2nd prefix starts with same letter as the 1st
99 43 50       101 if ( length $nlist->[0][0] ) {
100 43         82 $c = substr $nlist->[0][0], 0, 1; # first char
101 43 100       132 undef $c unless $c eq substr $nlist->[1][0], 0, 1;
102             }
103              
104 43 100       94 unless ( defined $c )
105             {
106 16 100       76 return $_[0] unless @$nlist > 2;
107              
108             # try to reduce next list part
109 8         7 my $first = shift @$nlist;
110 8         67 my $next = reduce(bless ['', $nlist, 0]);
111 8 100       39 return bless [ $prefix, [ $first, $next ], $opt] if length $next->[0];
112              
113             # couldn't be reduced
114 5         9 return bless [ $prefix, [ $first, @{$next->[1]} ], $opt ];
  5         94  
115             }
116              
117             # reduce any ensuing node whose prefix starts with $c
118 27         24 my @new;
119 27         26 my $newopt = undef;
120 27         47 my $qc = quotemeta $c;
121 27         52 while ( @$nlist )
122             {
123 87 100       541 last unless $nlist->[0][0] =~ s/^$qc//;
124              
125             # reduce node or detect new optional node
126 80         102 my $n = shift @$nlist;
127 80 100       136 if ( length $n->[0] )
128             {
129 65         66 push @new, $n;
130 65         126 next;
131             }
132 15         33 $newopt = 1;
133             }
134              
135 27 100 100     102 if ( @$nlist || $opt )
136             {
137 9         99 my $new = reduce(bless [ $c, [ @new ], $newopt ]);
138 9 100       29 if ( @$nlist )
139             {
140             # reduce remaining nlist
141 7         20 my $next = reduce(bless ['', $nlist, 0]);
142 7 100       179 return bless [ $prefix, [ $new, $next ], $opt] if length $next->[0];
143              
144             # couldn't be reduced
145 4         7 return bless [ $prefix, [ $new, @{$next->[1]} ], $opt ];
  4         119  
146             }
147              
148             # current node is optional
149 2         10 return bless [ $prefix, [ $new, @$nlist ], $opt ];
150             }
151              
152             # nothing left to reduce
153 18         97 reduce(bless [ $prefix.$c, [ @new ], $newopt ]);
154 14     14 1 17549 };
155              
156 14         33 shift; # __PACKAGE__
157 14         27 my @s = sort keys %{ { map{ $_ => 1 } @_ } }; # unique sorted
  14         44  
  52         217  
158 14 100       133 return bless [$_[0],[],undef] if @s == 1; # singleton
159 12         36 reduce(bless [ '', [( map { [$_] } @s )],undef]);
  49         107  
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 7     7 1 182945 shift; # __PACKAGE__
169 7         22 my ($regex) = @_;
170 7         16 my ($nn,$cn,@st);
171 7         38 while ( length $regex ) {
172              
173             # prefix string
174 87 100       576 if ( $regex =~ s/^\|?($RE_PREFIX)// ) {
175 43         89 my $p = &$REGEX_UNESCAPE($1);
176 43 100       77 if ( $cn ) {
177 40 100       114 $cn->[1] = [] unless ref $cn->[1];
178 40         39 push @{$cn->[1]}, ($nn = bless [$p]);
  40         93  
179             }
180             else {
181 3         10 $cn = $nn = bless [$p];
182             }
183 43         108 next;
184             }
185              
186             # node start
187 44 100       147 if ( $regex =~ s/^\(\?:// ) {
188 22 100       41 if ( $nn ) {
189 18         23 $cn = $nn;
190             }
191             else {
192 4         16 $cn = $nn = bless [''];
193             }
194 22         32 push @st, $nn;
195 22         52 next;
196             }
197              
198             # node end
199 22 50       86 if ( $regex =~ s/^\)(\?)?// ) {
200 22         27 $cn = pop @st;
201 22 100       53 $cn->[2] = defined $1 ? 1 : undef;
202 22 100       49 $cn = $st[$#st] if @st;
203 22         48 next;
204             }
205              
206 0         0 die "invalid: '$regex'";
207             }
208 7         21 $cn;
209             };
210              
211              
212             sub regex {
213              
214             local *to_regex_rec = sub {
215 63     63   58 my ($prefix, $nlist, $opt) = @{$_[0]};
  63         127  
216 63         122 my $rv = &$REGEX_ESCAPE($prefix);
217 63 100 100     230 if ( $nlist && @$nlist )
218             {
219 26         52 $rv .= '(?:'.(join '|', map { to_regex_rec($_) } @$nlist).')';
  49         119  
220 26 100       61 $rv .= '?' if $opt;
221             }
222 63         292 $rv;
223 14     14 1 692 };
224              
225 14         33 my $node = shift;
226 14         39 to_regex_rec($node);
227             }
228              
229             sub list {
230 59     59 1 24879 my $node = shift;
231 59         96 my ($s) = @_;
232 59 100       169 $s = '' unless defined $s;
233 59         100 my $qs = quotemeta $s;
234              
235             local *list_rec = sub {
236 241     241   292 my ($p,$n,$list) = @_;
237 241         307 my ($prefix, $nlist, $opt) = @$n;
238 241         354 my $p2 = $p.$prefix;
239 241         284 my $qp2 = quotemeta $p2;
240 241         202 my ($push,$continue);
241 241 100       2352 if ( $p2 =~ m/^$qs/ ) {
    100          
242             # current prefix starts with search string
243 137         195 $push = $continue = 1;
244             }
245             elsif ( $s =~ m/^$qp2/ ) {
246             # search string starts with current prefix
247 37         66 $continue = 1;
248             }
249              
250 241 100 100     1021 if ($nlist && @$nlist) {
    100          
251 107 100 100     335 push @$list, $p2 if $push && $opt;
252 107 100       228 if ( $continue ) {
253 87         406 list_rec($p2,$_,$list) foreach @$nlist;
254             }
255             }
256             elsif ( $push ) {
257 87         356 push @$list, $p2;
258             }
259 59         416 };
260              
261 59         82 my @list;
262 59         128 list_rec('',$node,\@list);
263 59 50       918 wantarray ? @list : \@list;
264             }
265              
266              
267             # Standard new constructor is an alias for fold.
268             *new = \&fold;
269              
270             1;
271             __END__