File Coverage

blib/lib/Parse/BooleanLogic.pm
Criterion Covered Total %
statement 212 256 82.8
branch 105 144 72.9
condition 59 85 69.4
subroutine 20 24 83.3
pod 13 14 92.8
total 409 523 78.2


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             Parse::BooleanLogic - parser of boolean expressions
4              
5             =head1 SYNOPSIS
6              
7             use Parse::BooleanLogic;
8             use Data::Dumper;
9              
10             my $parser = Parse::BooleanLogic->new( operators => ['', 'OR'] );
11             my $tree = $parser->as_array( 'label:parser subject:"boolean logic"' );
12             print Dumper($tree);
13              
14             $parser = new Parse::BooleanLogic;
15             $tree = $parser->as_array( 'x = 10' );
16             print Dumper($tree);
17              
18             $tree = $parser->as_array( 'x = 10 OR (x > 20 AND x < 30)' );
19             print Dumper($tree);
20              
21             # custom parsing using callbacks
22             $parser->parse(
23             string => 'x = 10 OR (x > 20 AND x < 30)',
24             callback => {
25             open_paren => sub { ... },
26             operator => sub { ... },
27             operand => sub { ... },
28             close_paren => sub { ... },
29             error => sub { ... },
30             },
31             );
32              
33             =head1 DESCRIPTION
34              
35             This module is quite fast parser for boolean expressions. Originally it's been writen for
36             Request Tracker to parse SQL like expressions and it's still capable, but
37             it can be used to parse other boolean logic sentences with OPERANDs joined using
38             binary OPERATORs and grouped and nested using parentheses (OPEN_PAREN and CLOSE_PAREN).
39              
40             Operand is not qualified strictly what makes parser flexible enough to parse different
41             things, for example:
42              
43             # SQL like expressions
44             (task.status = "new" OR task.status = "open") AND task.owner_id = 123
45              
46             # Google like search syntax used in Gmail and other service
47             subject:"some text" (from:me OR to:me) label:todo !label:done
48              
49             # Binary boolean logic expressions
50             (a | b) & (c | d)
51              
52             You can change literals used for boolean operators and parens. Read more
53             about this in description of constructor's arguments.
54              
55             As you can see quoted strings are supported. Read about that below in
56             L.
57              
58             =cut
59              
60 10     10   9862 use 5.008;
  10         50  
  10         417  
61 10     10   66 use strict;
  10         20  
  10         384  
62 10     10   66 use warnings;
  10         20  
  10         782  
63              
64             package Parse::BooleanLogic;
65              
66             our $VERSION = '0.09';
67              
68 10     10   49 use constant OPERAND => 1;
  10         20  
  10         682  
69 10     10   63 use constant OPERATOR => 2;
  10         17  
  10         431  
70 10     10   47 use constant OPEN_PAREN => 4;
  10         29  
  10         456  
71 10     10   47 use constant CLOSE_PAREN => 8;
  10         17  
  10         420  
72 10     10   46 use constant STOP => 16;
  10         18  
  10         610  
73             my @tokens = qw[OPERAND OPERATOR OPEN_PAREN CLOSE_PAREN STOP];
74              
75 10     10   9547 use Regexp::Common qw(delimited);
  10         26182  
  10         53  
76             my $re_delim = qr{$RE{delimited}{-delim=>qq{\'\"}}{-esc=>'\\'}};
77              
78             =head1 METHODS
79              
80             =head2 Building parser
81              
82             =head3 new
83              
84             A constuctor, takes the following named arguments:
85              
86             =over 4
87              
88             =item operators, default is ['AND' 'OR']
89              
90             Pair of literal strings representing boolean operators AND and OR,
91             pass it as array reference. For example:
92              
93             # from t/custom_ops.t
94             my $parser = Parse::BooleanLogic->new( operators => [qw(& |)] );
95              
96             # from t/custom_googlish.t
97             my $parser = Parse::BooleanLogic->new( operators => ['', 'OR'] );
98              
99             It's ok to have any operators and even empty.
100              
101             =item parens, default is ['(', ')']
102              
103             Pair of literal strings representing parentheses, for example it's
104             possible to use curly braces:
105              
106             # from t/custom_parens.t
107             my $parser = Parse::BooleanLogic->new( parens => [qw({ })] );
108              
109             No matter which pair is used parens must be balanced in expression.
110              
111             =back
112              
113             This constructor compiles several heavy weight regular expressions
114             so it's better avoid building object each time right before parsing,
115             but instead use global or cached one.
116              
117             =cut
118              
119             sub new {
120 9     9 1 6796 my $proto = shift;
121 9   33     149 my $self = bless {}, ref($proto) || $proto;
122 9         35 return $self->init( @_ );
123             }
124              
125             =head3 init
126              
127             An initializer, called from the constructor. Compiles regular expressions
128             and do other things with constructor's arguments. Returns this object back.
129              
130             =cut
131              
132             sub init {
133 9     9 1 18 my $self = shift;
134 9         22 my %args = @_;
135 9 100       40 if ( $args{'operators'} ) {
136 2         4 my @ops = map lc $_, @{ $args{'operators'} };
  2         11  
137 2         14 $self->{'operators'} = [ @ops ];
138 2 100       9 @ops = reverse @ops if length $ops[1] > length $ops[0];
139 2         4 foreach ( @ops ) {
140 4 100       9 unless ( length ) {
141 1         2 $_ = "(?<=\\s)";
142             }
143             else {
144 3 100       12 if ( /^\w/ ) {
145 1         3 $_ = '\b'. "\Q$_\E";
146             }
147             else {
148 2         3 $_ = "\Q$_\E";
149             }
150 3 100       17 if ( /\w$/ ) {
151 1         2 $_ .= '\b';
152             }
153             }
154 4         73 $self->{'re_operator'} = qr{(?:$ops[0]|$ops[1])}i;
155             }
156             } else {
157 7         42 $self->{'operators'} = [qw(and or)];
158 7         35 $self->{'re_operator'} = qr{\b(?:AND|OR)\b}i;
159             }
160              
161 9 100       33 if ( $args{'parens'} ) {
162 1         2 $self->{'parens'} = $args{'parens'};
163 1         19 $self->{'re_open_paren'} = qr{\Q$args{'parens'}[0]\E};
164 1         11 $self->{'re_close_paren'} = qr{\Q$args{'parens'}[1]\E};
165             } else {
166 8         27 $self->{'re_open_paren'} = qr{\(};
167 8         33 $self->{'re_close_paren'} = qr{\)};
168             }
169 9         365 $self->{'re_tokens'} = qr{(?:$self->{'re_operator'}|$self->{'re_open_paren'}|$self->{'re_close_paren'})};
170             # the following need some explanation
171             # operand is something consisting of delimited strings and other strings that are not our major tokens
172             # so it's a (delim string or anything until a token, ['"](start of a delim) or \Z) - this is required part
173             # then you can have zero or more ocurences of above group, but with one exception - "anything" can not start with a token or ["']
174 9         1284 $self->{'re_operand'} = qr{(?:$re_delim|.+?(?=$self->{re_tokens}|["']|\Z))(?:$re_delim|(?!$self->{re_tokens}|["']).+?(?=$self->{re_tokens}|["']|\Z))*};
175              
176 9         62 foreach my $re (qw(re_operator re_operand re_open_paren re_close_paren)) {
177 36         1972 $self->{"m$re"} = qr{\G($self->{$re})};
178             }
179              
180 9         47 return $self;
181             }
182              
183              
184             =head2 Parsing expressions
185              
186             =head3 as_array $string [ %options ]
187              
188             Takes a string and parses it into perl structure, where parentheses represented using
189             array references, operands are hash references with one key/value pair: operand,
190             when binary operators are simple scalars. So string C 20 AND x < 30)>
191             is parsed into the following structure:
192              
193             [
194             { operand => 'x = 10' },
195             'OR',
196             [
197             { operand => 'x > 20' },
198             'AND',
199             { operand => 'x < 30' },
200             ]
201             ]
202              
203             Aditional options:
204              
205             =over 4
206              
207             =item operand_cb - custom operands handler, for example:
208              
209             my $tree = $parser->as_array(
210             "some string",
211             operand_cb => sub {
212             my $op = shift;
213             if ( $op =~ m/^(!?)(label|subject|from|to):(.*)/ ) {
214             ...
215             } else {
216             die "You have an error in your query, in '$op'";
217             }
218             },
219             );
220              
221              
222             =item error_cb - custom errors handler
223              
224             my $tree = $parser->as_array(
225             "some string",
226             error_cb => sub {
227             my $msg = shift;
228             MyParseException->throw($msg);
229             },
230             );
231              
232             =back
233              
234             =cut
235              
236             { # static variables
237              
238             my ($tree, $node, @pnodes);
239             my %callback;
240             $callback{'open_paren'} = sub {
241             push @pnodes, $node;
242             push @{ $pnodes[-1] }, $node = []
243             };
244             $callback{'close_paren'} = sub { $node = pop @pnodes };
245             $callback{'operator'} = sub { push @$node, $_[0] };
246             $callback{'operand'} = sub { push @$node, { operand => $_[0] } };
247              
248             sub as_array {
249 0     0 1 0 my $self = shift;
250 0         0 my $string = shift;
251 0         0 my %arg = (@_);
252              
253 0         0 $node = $tree = [];
254 0         0 @pnodes = ();
255              
256 0 0 0     0 unless ( $arg{'operand_cb'} || $arg{'error_cb'} ) {
257 0         0 $self->parse(string => $string, callback => \%callback);
258 0         0 return $tree;
259             }
260              
261 0         0 my %cb = %callback;
262 0 0       0 if ( $arg{'operand_cb'} ) {
263 0     0   0 $cb{'operand'} = sub { push @$node, $arg{'operand_cb'}->( $_[0] ) };
  0         0  
264             }
265 0 0       0 $cb{'error'} = $arg{'error_cb'} if $arg{'error_cb'};
266 0         0 $self->parse(string => $string, callback => \%cb);
267 0         0 return $tree;
268             } }
269              
270             =head3 parse
271              
272             Takes named arguments: string and callback. Where the first one is scalar with
273             expression, the latter is a reference to hash with callbacks: open_paren, operator
274             operand, close_paren and error. Callback for errors is optional and parser dies if
275             it's omitted. Each callback is called when parser finds corresponding element in the
276             string. In all cases the current match is passed as argument into the callback.
277              
278             Here is simple example based on L method:
279              
280             # result tree and the current group
281             my ($tree, $node);
282             $tree = $node = [];
283              
284             # stack with nested groups, outer most in the bottom, inner on the top
285             my @pnodes = ();
286              
287             my %callback;
288             # on open_paren put the current group on top of the stack,
289             # create new empty group and at the same time put it into
290             # the end of previous one
291             $callback{'open_paren'} = sub {
292             push @pnodes, $node;
293             push @{ $pnodes[-1] }, $node = []
294             };
295              
296             # on close_paren just switch to previous group by taking it
297             # from the top of the stack
298             $callback{'close_paren'} = sub { $node = pop @pnodes };
299              
300             # push binary operators as is and operands as hash references
301             $callback{'operator'} = sub { push @$node, $_[0] };
302             $callback{'operand'} = sub { push @$node, { operand => $_[0] } };
303              
304             # run parser
305             $parser->parse( string => $string, callback => \%callback );
306              
307             return $tree;
308              
309             Using this method you can build other representations of an expression.
310              
311             =cut
312              
313             sub parse {
314 33     33 1 184666 my $self = shift;
315 33         180 my %args = (
316             string => '',
317             callback => {},
318             @_
319             );
320 33         100 my ($string, $cb) = @args{qw(string callback)};
321 33 50       106 $string = '' unless defined $string;
322              
323             # States
324 33         83 my $want = OPERAND | OPEN_PAREN | STOP;
325 33         49 my $last = 0;
326 33         47 my $depth = 0;
327              
328 33         39 while (1) {
329             # State Machine
330 143 100 100     2381 if ( $string =~ /\G\s+/gc ) {
    100 100        
    100 66        
    100 100        
    100 33        
    50          
331             }
332             elsif ( ($want & OPERATOR ) && $string =~ /$self->{'mre_operator'}/gc ) {
333 17         60 $cb->{'operator'}->( $1 );
334 17         109 $last = OPERATOR;
335 17         26 $want = OPERAND | OPEN_PAREN;
336             }
337             elsif ( ($want & OPEN_PAREN ) && $string =~ /$self->{'mre_open_paren'}/gc ) {
338 14         57 $cb->{'open_paren'}->( $1 );
339 14         95 $depth++;
340 14         18 $last = OPEN_PAREN;
341 14         23 $want = OPERAND | OPEN_PAREN;
342             }
343             elsif ( ($want & CLOSE_PAREN) && $string =~ /$self->{'mre_close_paren'}/gc ) {
344 14         46 $cb->{'close_paren'}->( $1 );
345 14         58 $depth--;
346 14         21 $last = CLOSE_PAREN;
347 14         17 $want = OPERATOR;
348 14 50       34 $want |= $depth? CLOSE_PAREN : STOP;
349             }
350             elsif ( ($want & OPERAND ) && $string =~ /$self->{'mre_operand'}/gc ) {
351 48         116 my $m = $1;
352 48         147 $m=~ s/\s+$//;
353 48         156 $cb->{'operand'}->( $m );
354 48         327 $last = OPERAND;
355 48         92 $want = OPERATOR;
356 48 100       114 $want |= $depth? CLOSE_PAREN : STOP;
357             }
358             elsif ( ($want & STOP) && $string =~ /\G\s*$/igc ) {
359 33         45 $last = STOP;
360 33         53 last;
361             }
362             else {
363 0         0 last;
364             }
365             }
366              
367 33 50 33     135 if (!$last || !($want & $last)) {
368 0         0 my $tmp = substr( $string, 0, pos($string) );
369 0         0 $tmp .= '>>>here<<<'. substr($string, pos($string));
370 0         0 my $msg = "Incomplete or incorrect expression, expecting a ". $self->bitmask_to_string($want) ." in '$tmp'";
371 0 0       0 $cb->{'error'}? $cb->{'error'}->($msg): die $msg;
372 0         0 return;
373             }
374              
375 33 50       138 if ( $depth ) {
376 0         0 my $msg = "Incomplete query, $depth paren(s) isn't closed in '$string'";
377 0 0       0 $cb->{'error'}? $cb->{'error'}->($msg): die $msg;
378 0         0 return;
379             }
380             }
381              
382             sub bitmask_to_string {
383 0     0 0 0 my $self = shift;
384 0         0 my $mask = shift;
385              
386 0         0 my @res;
387 0         0 for( my $i = 0; $i < @tokens; $i++ ) {
388 0 0       0 next unless $mask & (1<<$i);
389 0         0 push @res, $tokens[$i];
390             }
391              
392 0         0 my $tmp = join ', ', splice @res, 0, -1;
393 0 0       0 unshift @res, $tmp if $tmp;
394 0         0 return join ' or ', @res;
395             }
396              
397             =head2 Quoting and dequoting
398              
399             This module supports quoting with single quote ' and double ",
400             literal quotes escaped with \.
401              
402             from L with ' and " as delimiters.
403              
404             =head3 q, qq, fq and dq
405              
406             Four methods to work with quotes:
407              
408             =over 4
409              
410             =item q - quote a string with single quote character.
411              
412             =item qq - quote a string with double quote character.
413              
414             =item fq - quote with single if string has no single quote character, otherwisee use double quotes.
415              
416             =item dq - delete either single or double quotes from a string if it's quoted.
417              
418             =back
419              
420             All four works either in place or return result, for example:
421              
422             $parser->q($str); # inplace
423              
424             my $q = $parser->q($s); # $s is untouched
425              
426             =cut
427              
428             sub q {
429 8 100   8 1 3162 if ( defined wantarray ) {
430 4         8 my $s = $_[1];
431 4         16 $s =~ s/(?=['\\])/\\/g;
432 4         13 return "'$s'";
433             } else {
434 4         15 $_[1] =~ s/(?=['\\])/\\/g;
435 4         8 substr($_[1], 0, 0) = "'";
436 4         6 $_[1] .= "'";
437 4         8 return;
438             }
439             }
440              
441             sub qq {
442 8 100   8 1 2963 if ( defined wantarray ) {
443 4         5 my $s = $_[1];
444 4         11 $s =~ s/(?=["\\])/\\/g;
445 4         12 return "\"$s\"";
446             } else {
447 4         12 $_[1] =~ s/(?=["\\])/\\/g;
448 4         6 substr($_[1], 0, 0) = '"';
449 4         3 $_[1] .= '"';
450 4         8 return;
451             }
452             }
453              
454             sub fq {
455 6 100   6 1 2313 if ( index( $_[1], "'" ) >= 0 ) {
456 2 100       6 if ( defined wantarray ) {
457 1         2 my $s = $_[1];
458 1         2 $s =~ s/(?=["\\])/\\/g;
459 1         4 return "\"$s\"";
460             } else {
461 1         4 $_[1] =~ s/(?=["\\])/\\/g;
462 1         2 substr($_[1], 0, 0) = '"';
463 1         2 $_[1] .= '"';
464 1         2 return;
465             }
466             } else {
467 4 100       8 if ( defined wantarray ) {
468 2         4 my $s = $_[1];
469 2         4 $s =~ s/(?=\\)/\\/g;
470 2         6 return "'$s'";
471             } else {
472 2         4 $_[1] =~ s/(?=\\)/\\/g;
473 2         4 substr($_[1], 0, 0) = "'";
474 2         3 $_[1] .= "'";
475 2         3 return;
476             }
477             }
478             }
479              
480             sub dq {
481 22 0   22 1 9215 return defined wantarray? $_[1] : ()
    50          
482             unless $_[1] =~ /^$re_delim$/o;
483              
484 22 100       72 if ( defined wantarray ) {
485 11         15 my $s = $_[1];
486 11         21 my $q = substr( $s, 0, 1, '' );
487 11         12 substr( $s, -1 ) = '';
488 11         140 $s =~ s/\\([$q\\])/$1/g;
489 11         33 return $s;
490             } else {
491 11         27 my $q = substr( $_[1], 0, 1, '' );
492 11         12 substr( $_[1], -1 ) = '';
493 11         99 $_[1] =~ s/\\([$q\\])/$1/g;
494 11         24 return;
495             }
496             }
497              
498             =head2 Tree evaluation and modification
499              
500             Several functions taking a tree of boolean expressions as returned by
501             L method and evaluating or changing it using a callback.
502              
503             =head3 walk $tree $callbacks @rest
504              
505             A simple method for walking a $tree using four callbacks: open_paren,
506             close_paren, operand and operator. All callbacks are optional.
507              
508             Example:
509              
510             $parser->walk(
511             $tree,
512             {
513             open_paren => sub { ... },
514             close_paren => sub { ... },
515             ...
516             },
517             $some_context_argument, $another, ...
518             );
519              
520             Any additional arguments (@rest) are passed all the time into callbacks.
521              
522             =cut
523              
524             sub walk {
525 0     0 1 0 my ($self, $tree, $cb, @rest) = @_;
526              
527 0         0 foreach my $entry ( @$tree ) {
528 0 0       0 if ( ref $entry eq 'ARRAY' ) {
    0          
529 0 0       0 $cb->{'open_paren'}->( @rest ) if $cb->{'open_paren'};
530 0         0 $self->walk( $entry, $cb, @rest );
531 0 0       0 $cb->{'close_paren'}->( @rest ) if $cb->{'close_paren'};
532             } elsif ( ref $entry ) {
533 0 0       0 $cb->{'operand'}->( $entry, @rest ) if $cb->{'operand'};
534             } else {
535 0 0       0 $cb->{'operator'}->( $entry, @rest ) if $cb->{'operator'};
536             }
537             }
538             }
539              
540             =head3 filter $tree $callback @rest
541              
542             Filters a $tree using provided $callback. The callback is called for each operand
543             in the tree and operand is left when it returns true value.
544              
545             Any additional arguments (@rest) are passed all the time into the callback.
546             See example below.
547              
548             Boolean operators (AND/OR) are skipped according to parens and left first rule,
549             for example:
550              
551             X OR Y AND Z -> X AND Z
552             X OR (Y AND Z) -> X OR Z
553             X OR Y AND Z -> Y AND Z
554             X OR (Y AND Z) -> Y AND Z
555             X OR Y AND Z -> X OR Y
556             X OR (Y AND Z) -> X OR Y
557              
558             Returns new sub-tree. Original tree is not changed, but operands in new tree
559             still refer to the same hashes in the original.
560              
561             Example:
562              
563             my $filter = sub {
564             my ($condition, $some) = @_;
565             return 1 if $condition->{'operand'} eq $some;
566             return 0;
567             };
568             my $new_tree = $parser->filter( $tree, $filter, $some );
569              
570             See also L
571              
572             =cut
573              
574             sub filter {
575 34     34 1 81 my ($self, $tree, $cb, @rest) = @_;
576              
577 34         37 my $skip_next = 0;
578              
579 34         38 my @res;
580 34         60 foreach my $entry ( @$tree ) {
581 114 100 50     241 $skip_next-- and next if $skip_next > 0;
582              
583 96 100       260 if ( ref $entry eq 'ARRAY' ) {
    100          
584 8         20 my $tmp = $self->filter( $entry, $cb, @rest );
585 8 100       23 $tmp = $tmp->[0] if @$tmp == 1;
586 8 100 100     47 if ( !$tmp || (ref $tmp eq 'ARRAY' && !@$tmp) ) {
      33        
587 2         3 pop @res;
588 2 100       7 $skip_next++ unless @res;
589             } else {
590 6         20 push @res, $tmp;
591             }
592             } elsif ( ref $entry ) {
593 66 100       129 if ( $cb->( $entry, @rest ) ) {
594 33         169 push @res, $entry;
595             } else {
596 33         126 pop @res;
597 33 100       106 $skip_next++ unless @res;
598             }
599             } else {
600 22         33 push @res, $entry;
601             }
602             }
603 34 100 100     140 return $res[0] if @res == 1 && ref $res[0] eq 'ARRAY';
604 33         194 return \@res;
605             }
606              
607             =head3 solve $tree $callback @rest
608              
609             Solves a boolean expression represented by a $tree using provided $callback.
610             The callback is called for operands and should return a boolean value
611             (0 or 1 will work).
612              
613             Any additional arguments (@rest) are passed all the time into the callback.
614             See example below.
615              
616             Functions matrixes:
617              
618             A B AND OR
619             0 0 0 0
620             0 1 0 1
621             1 0 0 1
622             1 1 1 1
623              
624             Whole branches of the tree can be skipped when result is obvious, for example:
625              
626             1 OR (...)
627             0 AND (...)
628              
629             Returns result of the expression.
630              
631             Example:
632              
633             my $solver = sub {
634             my ($condition, $some) = @_;
635             return 1 if $condition->{'operand'} eq $some;
636             return 0;
637             };
638             my $result = $parser->solve( $tree, $filter, $some );
639              
640             See also L.
641              
642             =cut
643              
644             sub solve {
645 50     50 1 139 my ($self, $tree, $cb, @rest) = @_;
646              
647 50         108 my ($res, $ea, $skip_next) = (0, $self->{'operators'}[1], 0);
648 50         184 foreach my $entry ( @$tree ) {
649 178 100 50     381 $skip_next-- and next if $skip_next > 0;
650 150 100       288 unless ( ref $entry ) {
651 64         82 $ea = lc $entry;
652 64 100 100     391 $skip_next++ if
      100        
      66        
653             ( $res && $ea eq $self->{'operators'}[1])
654             || (!$res && $ea eq $self->{'operators'}[0]);
655 64         101 next;
656             }
657              
658 86         94 my $cur;
659 86 100       166 if ( ref $entry eq 'ARRAY' ) {
660 8         23 $cur = $self->solve( $entry, $cb, @rest );
661             } else {
662 78         160 $cur = $cb->( $entry, @rest );
663             }
664 86 100       362 if ( $ea eq $self->{'operators'}[1] ) {
665 68   66     289 $res ||= $cur;
666             } else {
667 18   66     80 $res &&= $cur;
668             }
669             }
670 50         205 return $res;
671             }
672              
673             =head3 fsolve $tree $callback @rest
674              
675             Does in filter+solve in one go. Callback can return undef to filter out an operand,
676             and a defined boolean value to be used in solve.
677              
678             Any additional arguments (@rest) are passed all the time into the callback.
679              
680             Returns boolean result of the equation or undef if all operands have been filtered.
681              
682             See also L and L.
683              
684             =cut
685              
686             sub fsolve {
687 40     40 1 85 my ($self, $tree, $cb, @rest) = @_;
688              
689 40         83 my ($res, $ea, $skip_next) = (undef, $self->{'operators'}[1], 0);
690 40         65 foreach my $entry ( @$tree ) {
691 154 100 50     294 $skip_next-- and next if $skip_next > 0;
692 124 100       203 unless ( ref $entry ) {
693 48         59 $ea = lc $entry;
694 48 100 100     245 $skip_next++ if
      100        
      66        
695             ( $res && $ea eq $self->{'operators'}[1])
696             || (!$res && $ea eq $self->{'operators'}[0]);
697 48         61 next;
698             }
699              
700 76         78 my $cur;
701 76 50       113 if ( ref $entry eq 'ARRAY' ) {
702 0         0 $cur = $self->fsolve( $entry, $cb, @rest );
703             } else {
704 76         146 $cur = $cb->( $entry, @rest );
705             }
706 76 100       277 if ( defined $cur ) {
707 58   100     194 $res ||= 0;
708 58 100       114 if ( $ea eq $self->{'operators'}[1] ) {
709 50   66     155 $res ||= $cur;
710             } else {
711 8   66     30 $res &&= $cur;
712             }
713             } else {
714 18 100       40 $skip_next++ unless defined $res;
715             }
716             }
717 40         154 return $res;
718             }
719              
720             =head3 partial_solve $tree $callback @rest
721              
722             Partially solve a $tree. Callback can return undef or a new expression
723             and a defined boolean value to be used in solve.
724              
725             Returns either result or array reference with expression.
726              
727             Any additional arguments (@rest) are passed all the time into the callback.
728              
729             =cut
730              
731             sub partial_solve {
732 19     19 1 54 my ($self, $tree, $cb, @rest) = @_;
733              
734 19         24 my @res;
735              
736 19         40 my ($last, $ea, $skip_next) = (0, $self->{'operators'}[1], 0);
737 19         29 foreach my $entry ( @$tree ) {
738 51 100 50     107 $skip_next-- and next if $skip_next > 0;
739 47 100       209 unless ( ref $entry ) {
740 16         24 $ea = lc $entry;
741 16 100       28 unless ( ref $last ) {
742 8 100 100     58 $skip_next++ if
      100        
      66        
743             ( $last && $ea eq $self->{'operators'}[1])
744             || (!$last && $ea eq $self->{'operators'}[0]);
745             } else {
746 8         10 push @res, $entry;
747             }
748 16         22 next;
749             }
750              
751 31 50       58 if ( ref $entry eq 'ARRAY' ) {
752 0         0 $last = $self->solve( $entry, $cb, @rest );
753             # drop parens with one condition inside
754 0 0 0     0 $last = $last->[0] if ref $last && @$last == 1;
755             } else {
756 31         100 $last = $cb->( $entry, @rest );
757 31 100       197 $last = $entry unless defined $last;
758             }
759 31 100       50 unless ( ref $last ) {
760 18 100       53 if ( $ea eq $self->{'operators'}[0] ) {
    50          
761             # (...) AND 0
762 4 100       10 unless ( $last ) { @res = () } else { pop @res };
  2         7  
  2         6  
763             }
764             elsif ( $ea eq $self->{'operators'}[1] ) {
765             # (...) OR 1
766 14 100       20 if ( $last ) { @res = () } else { pop @res };
  7         14  
  7         27  
767             }
768             } else {
769 13         25 push @res, $last;
770             }
771             }
772              
773 19 100       110 return $last unless @res; # solution
774 9         51 return \@res; # more than one condition
775             }
776              
777             1;
778              
779             =head1 ALTERNATIVES
780              
781             There are some alternative implementations available on the CPAN.
782              
783             =over 4
784              
785             =item L - similar purpose with several differences.
786              
787             =item Another?
788              
789             =back
790              
791             =head1 AUTHORS
792              
793             Ruslan Zakirov Eruz@cpan.orgE, Robert Spier Erspier@pobox.comE
794              
795             =head1 COPYRIGHT
796              
797             This program is free software; you can redistribute it and/or modify it under
798             the same terms as Perl itself.
799              
800             =cut