File Coverage

blib/lib/Mock/Data/Charset.pm
Criterion Covered Total %
statement 294 355 82.8
branch 179 242 73.9
condition 77 134 57.4
subroutine 39 45 86.6
pod 16 16 100.0
total 605 792 76.3


line stmt bran cond sub pod time code
1             package Mock::Data::Charset;
2 9     9   227230 use strict;
  9         24  
  9         279  
3 9     9   47 use warnings;
  9         18  
  9         288  
4 9     9   1018 use Mock::Data::Util qw( _parse_context _escape_str );
  9         43  
  9         55731  
5             require Carp;
6             our @CARP_NOT= ('Mock::Data::Util');
7             require Mock::Data::Generator;
8             our @ISA= ( 'Mock::Data::Generator' );
9              
10             # ABSTRACT: Generator of strings from a set of characters
11             our $VERSION = '0.01'; # VERSION
12              
13              
14             our @generator_attrs= qw( str_len min_codepoint max_codepoint );
15              
16             sub new {
17 44     44 1 128307 my $class= shift;
18 44         101 my (%self, %parse);
19             # make the common case fast
20 44 100 100     197 if (@_ == 1 && !ref $_[0]) {
21 7         704 qr/[$_[0]]/;
22 7         217 %self= ( notation => $_[0] );
23 7 50       23 if (ref $class) {
24 0   0     0 $self{generator_opts} ||= { %{ $class->{generator_opts} } };
  0         0  
25 0   0     0 $self{max_codepoint} //= $class->{max_codepoint};
26 0         0 $class= ref $class;
27             }
28 7         43 return bless \%self, $class;
29             }
30              
31 37 100       138 %self= @_ != 1? @_ : %{$_[0]};
  14         60  
32              
33             # Look for fields from the parser
34 37 100       150 $parse{classes}= delete $self{classes} if defined $self{classes};
35 37 100       105 $parse{codepoints}= delete $self{codepoints} if defined $self{codepoints};
36 37 50       86 $parse{codepoint_ranges}= delete $self{codepoint_ranges} if defined $self{codepoint_ranges};
37 37 50       92 $parse{negate}= delete $self{negate} if defined $self{negate};
38 37 100       102 if (defined $self{chars}) {
39 3         5 push @{$parse{codepoints}}, map ord, @{$self{chars}};
  3         9  
  3         14  
40 3         7 delete $self{chars};
41             }
42 37 50       92 if (defined $self{ranges}) {
43 0         0 push @{$parse{codepoint_ranges}},
44             map +( ref $_? ( ord $_->[0], ord $_->[1] ) : ord ),
45 0 0       0 @{$self{ranges}};
  0         0  
46 0         0 delete $self{ranges};
47             }
48              
49             # If called on an object, carry over some settings
50 37 50       90 if (ref $class) {
51 0 0 0     0 if (!keys %parse && !defined $self{notation} && !$self{members} && !$self{member_invlist}) {
      0        
      0        
52             @self{'_parse','notation','members','member_invlist'}=
53 0         0 @{$class}{'_parse','notation','members','member_invlist'};
  0         0  
54             }
55 0   0     0 $self{$_} //= $class->{$_} for @generator_attrs;
56 0         0 $class= ref $class;
57             }
58              
59 37 100 66     201 if (defined $self{notation} && !keys %parse) {
    100          
60             # want to trigger the syntax error exception now, not lazily later on
61 20         433 qr/[$self{notation}]/;
62             }
63             elsif (keys %parse) {
64 11         32 $self{_parse}= \%parse;
65             Carp::croak("Charset-building options (classes, chars, codepoints, ranges, codepoint_ranges, negate)"
66             ." cannot be combined with members, member_invlist or notation attributes")
67 11 50 33     66 if $self{members} or $self{member_invlist}; # allow notation to preserve original text
68             }
69             else {
70             # At least one of members, member_invlist, notation, or _parse must be specified
71             Carp::croak("Require at least one of members, member_invlist, notation, or charset-building options")
72 6 50 33     27 unless $self{members} or $self{member_invlist};
73             }
74            
75 37         1999 return bless \%self, $class;
76             }
77              
78             sub _parse {
79             # If the '_parse' wasn't initialized, it can be derived from members or member_invlist or notation
80 26 100   26   98 $_[0]{_parse} || do {
81 15         25 my $self= shift;
82 15 50       35 if (defined $self->{notation}) {
    0          
    0          
83 15         40 $self->{_parse}= $self->parse($self->{notation});
84             }
85             elsif ($self->{members}) {
86 0         0 $self->{_parse}{codepoints}= [ map ord, @{$self->{members}} ];
  0         0  
87             }
88             elsif (my $inv= $self->{member_invlist}) {
89 0         0 my $i;
90 0         0 for ($i= 0; $i < $#$inv; $i+= 2) {
91 0 0       0 if ($inv->[$i] + 1 == $inv->[$i+1]) { push @{$self->{_parse}{codepoints}}, $inv->[$i] }
  0         0  
  0         0  
92 0         0 else { push @{$self->{_parse}{codepoint_ranges}}, $inv->[$i], $inv->[$i+1] - 1; }
  0         0  
93             }
94 0 0       0 if ($i == $#$inv) {
95 0   0     0 push @{$self->{_parse}{codepoint_ranges}}, $inv->[$i], ($self->max_codepoint || 0x10FFFF);
  0         0  
96             }
97             }
98 0         0 else { die "Unhandled lazy-build scenario" }
99 15         47 $self->{_parse};
100             };
101             }
102              
103              
104             sub notation {
105 13   66 13 1 1331 $_[0]{notation} //= _deparse_charset($_[0]->_parse);
106             }
107              
108              
109             sub min_codepoint {
110 113 50   113 1 242 $_[0]{min_codepoint}= $_[1] if @_ > 1;
111             $_[0]{min_codepoint}
112 113         227 }
113             sub max_codepoint {
114             $_[0]{max_codepoint}
115 146     146 1 324 }
116              
117              
118             sub str_len {
119 113 50   113 1 247 $_[0]{str_len}= $_[1] if @_ > 1;
120 113         271 $_[0]{str_len};
121             }
122              
123              
124             sub count {
125 862 100   862 1 295295 $_[0]{members}? scalar @{$_[0]{members}}
  84         244  
126             : $_[0]->_invlist_index->[-1];
127             }
128              
129              
130             sub members {
131 104   66 104 1 300 $_[0]{members} ||= $_[0]->_build_members;
132             }
133              
134             sub _build_members {
135 20     20   33 my $self= shift;
136 20         42 my $invlist= $self->member_invlist;
137 20         35 my @members;
138 20 50       62 if (@$invlist > 1) {
139             push @members, map chr, $invlist->[$_*2] .. ($invlist->[$_*2+1]-1)
140 20         444 for 0 .. (($#$invlist-1)>>1);
141             }
142             # an odd number of elements means the list ends with an "include-all"
143 20 50       70 push @members, map chr, $invlist->[-1] .. 0x10FFFF
144             if 1 & @$invlist;
145 20         70 return \@members;
146             }
147              
148             sub Mock::Data::Charset::Util::expand_invlist {
149 6     6   8411 my $invlist= shift;
150 6         10 my @members;
151 6 100       20 if (@$invlist > 1) {
152             push @members, $invlist->[$_*2] .. ($invlist->[$_*2+1]-1)
153 5         63 for 0 .. (($#$invlist-1)>>1);
154             }
155             # an odd number of elements means the list ends with an "include-all"
156 6 100       22 push @members, $invlist->[-1] .. 0x10FFFF
157             if 1 & @$invlist;
158 6         17 return \@members;
159             }
160              
161             # The index is private because there's not a good way to explain it to the user
162             sub _invlist_index {
163 2329     2329   3275 my $self= shift;
164 2329   66     6795 $self->{_invlist_index} ||= Mock::Data::Charset::Util::create_invlist_index($self->member_invlist);
165             }
166              
167             sub Mock::Data::Charset::Util::create_invlist_index {
168 23     23   5011 my $invlist= shift;
169 23         52 my $n_spans= (@$invlist + 1) >> 1;
170 23         35 my @index;
171 23         105 $#index= $n_spans-1;
172 23         45 my $total= 0;
173             $index[$_]= $total += $invlist->[$_*2+1] - $invlist->[$_*2]
174 23         314 for 0 .. (@$invlist >> 1)-1;
175 23 100       77 if (@$invlist & 1) { # In the case that the final range is infinite
176 6         46 $index[$n_spans-1]= $total + 0x110000 - $invlist->[-1];
177             }
178 23         135 \@index;
179             }
180              
181              
182             sub member_invlist {
183 1604 50   1604 1 3275 if (@_ > 1) {
184 0         0 $_[0]{member_invlist}= $_[1];
185 0         0 delete $_[0]{_invlist_index};
186 0         0 delete $_[0]{members};
187 0         0 delete $_[0]{notation};
188             }
189 1604   66     4504 $_[0]{member_invlist} //= _build_member_invlist(@_);
190             }
191              
192             sub _build_member_invlist {
193 33     33   60 my $self= shift;
194 33         77 my $max_codepoint= $self->max_codepoint;
195             # If the search space is small, and there is already a regex notation, it is probably faster
196             # to iterate and let perl do the work than to parse the charset.
197 33         53 my $invlist;
198 33 100 66     166 if (!defined $max_codepoint || $max_codepoint > 1000 || !defined $self->{notation}) {
      100        
199 25   100     108 $max_codepoint ||= 0x10FFFF;
200 25         47 $invlist= eval {
201 25         70 _parsed_charset_to_invlist($self->_parse, $max_codepoint);
202             }# or main::diag $@
203             }
204 33   66     144 $invlist ||= _charset_invlist_brute_force($self->notation, $max_codepoint);
205             # If a user writes to the invlist, it will become out of sync with the Index,
206             # leading to confusing bugs.
207 33 50       230 if (Internals->can('SvREADONLY')) {
208 33         398 Internals::SvREADONLY($_,1) for @$invlist;
209 33         74 Internals::SvREADONLY(@$invlist,1);
210             }
211 33         182 return $invlist;
212             }
213              
214             sub _charset_invlist_brute_force {
215 8     8   20 my ($notation, $max_codepoint)= @_;
216 8         72 my $re= qr/[$notation]/;
217 8         141 my @invlist;
218             my $match;
219 8         23 for (0..$max_codepoint) {
220 1409 100 100     5854 next unless $match xor (chr =~ $re);
221 36         67 push @invlist, $_;
222 36         58 $match= !$match;
223             }
224             # If an "infinite" range would be returned, but the user set a maximum codepoint,
225             # list the max codepoint as the end of the invlist.
226 8 100 66     40 if ($max_codepoint < 0x10FFFF and 1 & @invlist) {
227 4         9 push @invlist, $max_codepoint+1;
228             }
229 8         39 return \@invlist;
230             }
231              
232             sub _parsed_charset_to_invlist {
233 25     25   61 my ($parse, $max_codepoint)= @_;
234 25         38 my @invlists;
235             # convert the character list into an inversion list
236 25 100       74 if (defined (my $cp= $parse->{codepoints})) {
237 5         25 my @chars= sort { $a <=> $b } @$cp;
  6         21  
238 5         16 my @invlist= (shift @chars);
239 5         16 push @invlist, $invlist[0] + 1;
240 5         23 for (my $i= 0; $i <= $#chars; $i++) {
241             # If the next char is adjacent, extend the span
242 5 100       14 if ($invlist[-1] == $chars[$i]) {
243 2         5 ++$invlist[-1];
244             } else {
245 3         9 push @invlist, $chars[$i], $chars[$i]+1;
246             }
247             }
248 5         13 push @invlists, \@invlist;
249             }
250             # Each range is an inversion list already
251 25 100       72 if (my $r= $parse->{codepoint_ranges}) {
252 9         28 for (my $i= 0; $i < (@$r >> 1); $i++) {
253 10         31 my ($start, $limit)= ($r->[$i*2], $r->[$i*2+1]+1);
254             # Try to combine the range with the most recent inversion list, if possible,
255 10 100 66     51 if (@invlists && $invlists[-1][-1] < $start) {
    50 33        
256 1         2 push @{ $invlists[-1] }, $start, $limit;
  1         5  
257             } elsif (@invlists && $invlists[-1][0] > $limit) {
258 0         0 unshift @{ $invlists[-1] }, $start, $limit;
  0         0  
259             } else {
260             # else just start a new inversion list
261 9         32 push @invlists, [ $start, $limit ]
262             }
263             }
264             }
265             # Convert each character class to an inversion list.
266 25 100       67 if ($parse->{classes}) {
267             push @invlists, _class_invlist($_)
268 11         21 for @{ $parse->{classes} };
  11         51  
269             }
270 25         2573 my $invlist= Mock::Data::Charset::Util::merge_invlists(\@invlists, $max_codepoint);
271             # Perform negation of inversion list by either starting at char 0 or removing char 0
272 25 100       130 if ($parse->{negate}) {
273 1 50       3 if ($invlist->[0]) { unshift @$invlist, 0 }
  1         4  
274 0         0 else { shift @$invlist; }
275             }
276 25         665 return $invlist;
277             }
278              
279              
280             our $_compile;
281             sub compile {
282 3     3 1 40 local $_compile= 1;
283 3         12 shift->generate(@_);
284             }
285             sub generate {
286 113     113 1 213 my ($self, $mock)= (shift, shift);
287 113         242 my ($len, $cp_min, $cp_max, $member_count)
288             = ($self->str_len, $self->min_codepoint, $self->max_codepoint, $self->count);
289 113 100       294 if (@_) {
290 82 100       207 my %opts= ref $_[0] eq 'HASH'? %{ shift() } : ();
  79         310  
291 82 100 33     214 $len= @_? shift : $opts{str_len} // $opts{len} // $opts{size}; # allow some aliases for length
      33        
292 82   66     265 $cp_min= $opts{min_codepoint} // $cp_min;
293 82   66     261 $cp_max= $opts{max_codepoint} // $cp_max;
294             }
295 113 100 100     519 my ($memb_min, $memb_span)= !defined $cp_min && !defined $cp_max? (0,$member_count)
296             : $self->_codepoint_minmax_to_member_range($cp_min, $cp_max);
297              
298             # If compiling, $len will be a function, else it will be an integer
299 0     0   0 $len= !defined $len? ($_compile? sub { 1 } : 1 )
300 0     0   0 : !ref $len? ($_compile? sub { $len } : $len)
301             : ref $len eq 'ARRAY'? (
302 0     0   0 $_compile? sub { $len->[0] + int rand ($len->[1] - $len->[0]) }
303 113 50       432 : $len->[0] + int rand ($len->[1] - $len->[0])
    50          
    50          
    100          
    50          
    100          
    100          
    100          
304             )
305             : ref $len eq 'CODE'? ($_compile? $len : $len->($mock))
306             : Carp::croak("Unknown str_len specification '$len'");
307              
308             # If member list is small-ish, use faster direct array access
309 113 100 100     337 if ($self->{members} || $member_count < 500) {
310 97         192 my $members= $self->members;
311             return sub {
312 3     3   7 my $buf= '';
313             $buf .= $members->[$memb_min + int rand $memb_span]
314 3         10 for 1..$len->($_[0]);
315 3         43 return $buf;
316 97 100       222 } if $_compile;
317 94         154 my $buf= '';
318             $buf .= $members->[$memb_min + int rand $memb_span]
319 94         445 for 1..$len;
320 94         432 return $buf;
321             }
322             else {
323 16         42 my $invlist= $self->member_invlist;
324 16         34 my $index= $self->_invlist_index;
325             return sub {
326 0     0   0 my $ret= '';
327             $ret .= chr _get_invlist_element($memb_min + int rand($memb_span), $invlist, $index)
328 0         0 for 1..$len->($_[0]);
329 16 50       42 } if $_compile;
330 16         30 my $buf= '';
331             $buf .= chr _get_invlist_element($memb_min + int rand($memb_span), $invlist, $index)
332 16         69 for 1..$len;
333 16         79 return $buf;
334             }
335             }
336              
337             sub _codepoint_minmax_to_member_range {
338 27     27   48 my $self= shift;
339 27         52 my ($cp_min, $cp_max)= @_;
340             my $memb_min= !defined $cp_min? 0
341 27 100       63 : do {
342 6         15 my ($at, $ins)= _find_invlist_element($cp_min, $self->member_invlist, $self->_invlist_index);
343 6   33     14 $at // $ins
344             };
345             my $memb_lim= !defined $cp_max? $self->count
346 27 50       57 : do {
347 27         83 my ($at, $ins)= _find_invlist_element($cp_max, $self->member_invlist, $self->_invlist_index);
348 27 100       72 defined $at? $at + 1 : $ins;
349             };
350 27         65 return ($memb_min, $memb_lim-$memb_min);
351             }
352              
353              
354             sub parse {
355 31     31 1 20135 my ($self, $notation)= @_;
356 31 50       79 return { codepoints => [] } unless length $notation;
357 31 50       70 return { classes => ['All'] } if $notation eq '^';
358 31         68 $notation .= ']';
359             # parse function needs $_ to be the input string
360 31         83 pos($notation)= 0;
361 31         116 return _parse_charset() for $notation;
362             }
363              
364             our $have_prop_invlist;
365             our @_backslash_h_invlist= (
366             0x09,0x0A, 0x20,0x21, 0xA0,0xA1, 0x1680,0x1681, 0x2000,0x200B, 0x202F,0x2030,
367             0x205F,0x2060, 0x3000,0x3001
368             );
369             our @_backslash_v_invlist= ( 0x0A,0x0E, 0x85,0x86, 0x2028,0x202A );
370             our %_parse_charset_backslash= (
371             a => ord "\a",
372             b => ord "\b",
373             c => sub { ... },
374             d => sub { push @{$_[0]{classes}}, 'digit'; undef; },
375             D => sub { push @{$_[0]{classes}}, '^digit'; undef; },
376             e => ord "\e",
377             f => ord "\f",
378             h => sub { push @{$_[0]{classes}}, '\\h'; undef; },
379             H => sub { push @{$_[0]{classes}}, '^\\h'; undef; },
380             n => ord "\n",
381             N => \&_parse_charset_namedchar,
382             o => \&_parse_charset_oct,
383             p => \&_parse_charset_classname,
384             P => sub { _parse_charset_classname(shift, 1) },
385             r => ord "\r",
386             s => sub { push @{$_[0]{classes}}, 'space'; undef; },
387             S => sub { push @{$_[0]{classes}}, '^space'; undef; },
388             t => ord "\t",
389             v => sub { push @{$_[0]{classes}}, '\\v'; undef; },
390             V => sub { push @{$_[0]{classes}}, '^\\v'; undef; },
391             w => sub { push @{$_[0]{classes}}, 'word'; undef; },
392             W => sub { push @{$_[0]{classes}}, '^word'; undef; },
393             x => \&_parse_charset_hex,
394             0 => \&_parse_charset_oct,
395             1 => \&_parse_charset_oct,
396             2 => \&_parse_charset_oct,
397             3 => \&_parse_charset_oct,
398             4 => \&_parse_charset_oct,
399             5 => \&_parse_charset_oct,
400             6 => \&_parse_charset_oct,
401             7 => \&_parse_charset_oct,
402             8 => \&_parse_charset_oct,
403             9 => \&_parse_charset_oct,
404             );
405             our %_class_invlist_cache= (
406             '\\h' => \@_backslash_h_invlist,
407             '\\v' => \@_backslash_v_invlist,
408             'Any' => [ 0 ],
409             '\\N' => [ 0, ord("\n"), 1+ord("\n") ],
410             );
411             sub _class_invlist {
412 22     22   20255 my $class= shift;
413 22 100       80 if (ord $class == ord '^') {
414 5         46 return Mock::Data::Charset::Util::negate_invlist(
415             _class_invlist(substr($class,1))
416             );
417             }
418 17   100     95 return $_class_invlist_cache{$class} ||= do {
419 15 100       47 $have_prop_invlist= do { require Unicode::UCD; !!Unicode::UCD->can('prop_invlist') }
  2         2579  
  2         51924  
420             unless defined $have_prop_invlist;
421 15 50       80 return $have_prop_invlist? [ Unicode::UCD::prop_invlist($class) ]
422             : _charset_invlist_brute_force("\\p{$class}", 0x10FFFF);
423             };
424             }
425             sub _parse_charset_hex {
426 3 50   3   17 /\G( [0-9A-Fa-f]{2} | \{ ([0-9A-Fa-f]+) \} )/gcx
427             or die "Invalid hex escape at "._parse_context;
428 3 100       17 return hex(defined $2? $2 : $1);
429             }
430             sub _parse_charset_oct {
431 5     5   12 --pos; # The caller ate one of the characters we need to parse
432 5 50       30 /\G( [0-7]{3} | 0 | o\{ ([0-7]+) \} ) /gcx
433             or die "Invalid octal escape at "._parse_context;
434 5 100       29 return oct(defined $2? $2 : $1);
435             }
436             sub _parse_charset_namedchar {
437 2     2   670 require charnames;
438 2 50       11016 /\G \{ ([^}]+) \} /gcx
439             # or die "Invalid named char following \\N at '".substr($_,pos,10)."'";
440             and return charnames::vianame($1);
441             # Plain "\N" means every character except \n
442 0         0 push @{ $_[0]{classes} }, '\\N';
  0         0  
443 0         0 return;
444             }
445             sub _parse_charset_classname {
446 7     7   21 my ($result, $negate)= @_;
447 7 50       37 /\G \{ ([^}]+) \} /gcx
448             or die "Invalid class name following \\p at "._parse_context;
449 7 100       13 push @{$result->{classes}}, ($negate? "^$1" : $1);
  7         39  
450             undef
451 7         19 }
452             sub _parse_charset {
453 36     36   63 my $flags= shift;
454             # argument is in $_, starting from pos($_)
455 36         79 my %parse;
456             my @range;
457 36         81 $parse{codepoints}= \my @chars;
458 36 100       126 $parse{negate}= 1 if /\G \^ /gcx;
459 36 50       93 if (/\G]/gc) { push @chars, ord ']' }
  0         0  
460 36         52 while (1) {
461 121         157 my $cp; # literal codepoint to be added
462             # Check for special cases
463 121 100 50     463 if (/\G ( \\ | - | \[: | \] ) /gcx) {
    50 66        
      33        
464 78 100       313 if ($1 eq '\\') {
    100          
    100          
465 19 50       62 /\G(.)/gc or die "Unexpected end of input";
466 19   33     72 $cp= $_parse_charset_backslash{$1} || ord $1;
467 19 100       79 $cp= $cp->(\%parse)
468             if ref $cp;
469             }
470             elsif ($1 eq '-') {
471 18 100       46 if (@range == 1) {
472 17         28 push @range, ord '-';
473 17         31 next;
474             }
475             else {
476 1         2 $cp= ord '-';
477             }
478             }
479             elsif ($1 eq '[:') {
480 5 50       38 /\G ( [^:]+ ) :] /gcx
481             or die "Invalid character class at "._parse_context;
482 5         13 push @{$parse{classes}}, $1;
  5         24  
483             }
484             else {
485 36         68 last; # $1 eq ']';
486             }
487             }
488             elsif ($flags && ($flags->{x}||0) >= 2 && /\G[ \t]/gc) {
489 0         0 next; # ignore space and tab under /xx
490             }
491             else {
492 43 50       113 /\G(.)/gc or die "Unexpected end of input";
493 43         79 $cp= ord $1;
494             }
495             # If no single character was found, any range-in-progress needs converted to
496             # charcters
497 68 100       12123 if (!defined $cp) {
    100          
    100          
498 16         29 push @chars, @range;
499 16         29 @range= ();
500             }
501             # At this point, $cp will contain the next ordinal of the character to include,
502             # but it might also be starting or finishing a range.
503             elsif (@range == 1) {
504 8         18 push @chars, $range[0];
505 8         15 $range[0]= $cp;
506             }
507             elsif (@range == 2) {
508 16         26 push @{$parse{codepoint_ranges}}, $range[0], $cp;
  16         39  
509 16         32 @range= ();
510             }
511             else {
512 28         56 push @range, $cp;
513             }
514             #printf "# pos %d cp %d range %s %s include %s\n", pos $_, $cp, $range[0] // '(null)', $range[1] // '(null)', join(',', @include);
515             }
516 36         78 push @chars, @range;
517 36 100       78 if (@chars) {
518 13         54 @chars= sort { $a <=> $b } @chars;
  11         29  
519             } else {
520 23         48 delete $parse{codepoints};
521             }
522 36         195 return \%parse;
523             }
524              
525             sub _ord_to_safe_regex_char {
526 3 0   3   24 return chr($_[0]) =~ /[\w]/? chr $_[0]
    50          
527             : $_[0] <= 0xFF? sprintf('\x%02X',$_[0])
528             : sprintf('\x{%X}',$_[0])
529             }
530              
531             sub _deparse_charset {
532 1     1   3 my $parse= shift;
533 1         3 my $str= '';
534 1 50       14 if (my $cp= $parse->{codepoints}) {
535             $str .= _ord_to_safe_regex_char($_)
536 1         23 for @$cp;
537             }
538 1 50       13 if (my $r= $parse->{codepoint_ranges}) {
539 0         0 for (my $i= 0; $i < (@$r << 1); $i++) {
540 0         0 $str .= _ord_to_safe_regex_char($r->[$i*2]) . '-' . _ord_to_safe_regex_char($r->[$i*2+1]);
541             }
542             }
543 1 50       9 if (my $cl= $parse->{classes}) {
544             # TODO: reverse conversions to \h \v etc.
545 0         0 for (@$cl) {
546 0 0       0 $str .= $_ eq '\N'? '\0-\x09\x0B-\x{10FFFF}'
    0          
547             : ord == ord '^'? '\P{'.substr($_,1).'}'
548             : '\p{'.$_.'}';
549             }
550             }
551 1         6 return $str;
552             }
553              
554              
555             sub get_member {
556 749 100   749 1 3660 $_[0]{members}? $_[0]{members}[$_[1]]
557             : chr _get_invlist_element($_[1], $_[0]->member_invlist, $_[0]->_invlist_index);
558             }
559              
560             sub get_member_codepoint {
561 6 50   6 1 2351 $_[0]{members}? ord $_[0]{members}[$_[1]]
562             : _get_invlist_element($_[1], $_[0]->member_invlist, $_[0]->_invlist_index);
563             }
564              
565             sub _get_invlist_element {
566 1080     1080   2031 my ($ofs, $invlist, $invlist_index)= @_;
567 1080 50       1960 $ofs += @$invlist_index if $ofs < 0;
568 1080 50 33     3798 return undef if $ofs >= $invlist_index->[-1] || $ofs < 0;
569             # Binary Search to find the range that contains this numbered element
570 1080         2019 my ($min, $max, $mid)= (0, $#$invlist_index);
571 1080         1493 while (1) {
572 5608         7187 $mid= ($min+$max) >> 1;
573 5608 100 100     13069 if ($ofs >= $invlist_index->[$mid]) {
    100          
574 2631         3285 $min= $mid+1
575             }
576             elsif ($mid > 0 && $ofs < $invlist_index->[$mid-1]) {
577 1897         2491 $max= $mid-1
578             }
579             else {
580 1080 100       2138 $ofs -= $invlist_index->[$mid-1] if $mid > 0;
581 1080         4230 return $invlist->[$mid*2] + $ofs;
582             }
583             }
584             }
585              
586              
587             sub find_member {
588 754     754 1 3761 my ($self, $char)= @_;
589 754         1485 return _find_invlist_element(ord $char, $self->member_invlist, $self->_invlist_index);
590             }
591              
592             sub _find_invlist_element {
593 787     787   1315 my ($codepoint, $invlist, $index)= @_;
594             # Binary Search to find the range that contains this numbered element
595 787         1521 my ($min, $max, $mid)= (0, $#$invlist);
596 787         1173 while (1) {
597 5749         6993 $mid= ($min+$max) >> 1;
598 5749 100 100     19098 if ($mid > 0 && $codepoint < $invlist->[$mid]) {
    100 100        
599 2341         2907 $max= $mid-1
600             }
601             elsif ($mid < $#$invlist && $codepoint >= $invlist->[$mid+1]) {
602 2621         3429 $min= $mid+1;
603             }
604             else {
605 787 100       1602 return (undef, 0) unless $codepoint >= $invlist->[$mid];
606 786 100       1865 return $codepoint - $invlist->[$mid] unless $mid > 0;
607 778 100       4597 return $codepoint - $invlist->[$mid] + $index->[($mid >> 1) - 1] unless $mid & 1;
608             # if $mid is an odd number, the range is excluded, and there is no match
609 13 100       69 return undef unless wantarray;
610 11         45 return (undef, $index->[($mid-1)>>1]) # return insertion point as second val
611             }
612             }
613             }
614              
615              
616             sub negate {
617 0     0 1 0 my $self= shift;
618 0         0 my $neg= Mock::Data::Charset::Util::negate_invlist($self->member_invlist, $self->max_codepoint);
619 0         0 return $self->new(member_invlist => $neg);
620             }
621             sub Mock::Data::Charset::Util::negate_invlist {
622 5     5   5053 my ($invlist, $max_codepoint)= @_;
623             # Toggle first char of 0
624 5 50       244 $invlist= $invlist->[0]? [ 0, @$invlist ] : [ @{$invlist}[1..$#$invlist] ];
  0         0  
625             # If max_codepoint is defined, and was the final char, remove the range starting at max_codepoint+1
626 5 50 33     48 if (@$invlist & 1 and defined $max_codepoint and $invlist->[-1] == $max_codepoint+1) {
      33        
627 0         0 pop @$invlist;
628             }
629 5         87 return $invlist;
630             }
631              
632              
633             sub union {
634 0     0 1 0 my $self= $_[0];
635 0         0 my @invlists= @_;
636             ref eq 'ARRAY' || ($_= $_->member_invlist)
637 0   0     0 for @invlists;
638 0         0 my $combined= Mock::Data::Charset::Util::merge_invlists(\@invlists, $self->max_codepoint);
639 0         0 return $self->new(member_invlist => $combined);
640             }
641              
642             #=head2 merge_invlists
643             #
644             # my $combined_invlist= $charset->merge_invlist( \@list2, \@list3, ... );
645             # my $combined_invlist= merge_invlist( \@list1, \@list2, ... );
646             #
647             #Merge one or more inversion lists into a superset of all of them.
648             #If called as a method, the L is used as the first list.
649             #
650             #The return value is an inversion list, which can be wrapped in a Charset object by passing it
651             #as the C attribute.
652             #
653             #The current L applies to the result. If called as a plain function, the
654             #C is assumed to be the Unicode maximum of C<0x10FFFF>.
655             #
656             #=cut
657              
658             sub Mock::Data::Charset::Util::merge_invlists {
659 33     33   8939 my @invlists= @{shift()};
  33         83  
660 33   100     107 my $max_codepoint= shift // 0x10FFFF;
661              
662 33 50       88 return [] unless @invlists;
663 33 100       93 return [@{$invlists[0]}] unless @invlists > 1;
  19         105  
664 14         24 my @combined= ();
665             # Repeatedly select the minimum range among the input lists and add it to the result
666 14         41 my @pos= (0)x@invlists;
667 14         37 while (@invlists) {
668 3293         5636 my ($min_ch, $min_i)= ($invlists[0][$pos[0]], 0);
669             # Find which inversion list contains the lowest range
670 3293         5903 for (my $i= 1; $i < @invlists; $i++) {
671 3284 100       7463 if ($invlists[$i][$pos[$i]] < $min_ch) {
672 814         1180 $min_ch= $invlists[$i][$pos[$i]];
673 814         1504 $min_i= $i;
674             }
675             }
676 3293 100       5484 last if $min_ch > $max_codepoint;
677             # Check for overlap of this new inclusion range with the previous
678 3291 100 100     8162 if (@combined && $combined[-1] >= $min_ch) {
679             # they overlap, so just replace the end-codepoint of the range
680             # if the new endpoint is larger
681 3111         4554 my $new_end= $invlists[$min_i][$pos[$min_i]+1];
682 3111 100 100     8753 $combined[-1]= $new_end if !defined $new_end || $combined[-1] < $new_end;
683             }
684             else {
685             # else, simply append the range
686 180         297 push @combined, @{$invlists[$min_i]}[$pos[$min_i] .. $pos[$min_i]+1];
  180         401  
687             }
688             # If the list is empty now, remove it from consideration
689 3291 100       4548 if (($pos[$min_i] += 2) >= @{$invlists[$min_i]}) {
  3291 50       8561  
690 21         37 splice @invlists, $min_i, 1;
691 21         33 splice @pos, $min_i, 1;
692             # If the invlist ends with an infinite range now, we are done
693 21 100       57 if (!defined $combined[-1]) {
694 6         13 pop @combined;
695 6         14 last;
696             }
697             }
698             # If this is the only list remaining, append the rest and done
699             elsif (@invlists == 1) {
700 0         0 push @combined, @{$invlists[0]}[$pos[0] .. $#{$invlists[0]}];
  0         0  
  0         0  
701 0         0 last;
702             }
703             }
704 14         38 while ($combined[-1] > $max_codepoint) {
705 1         5 pop @combined;
706             }
707             # If the list ends with inclusion, and the max_codepoint is less than unicode max,
708             # end the list with it.
709 14 100 100     56 if (1 & @combined and $max_codepoint < 0x10FFFF) {
710 1         3 push @combined, $max_codepoint+1;
711             }
712 14         48 return \@combined;
713             }
714              
715             1;
716              
717             __END__