File Coverage

blib/lib/Parse/BooleanLogic.pm
Criterion Covered Total %
statement 211 255 82.7
branch 105 144 72.9
condition 63 85 74.1
subroutine 20 24 83.3
pod 13 14 92.8
total 412 522 78.9


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   8220 use 5.008;
  10         37  
61 10     10   53 use strict;
  10         19  
  10         201  
62 10     10   46 use warnings;
  10         18  
  10         560  
63              
64             package Parse::BooleanLogic;
65              
66             our $VERSION = '0.10';
67              
68 10     10   63 use constant OPERAND => 1;
  10         27  
  10         891  
69 10     10   67 use constant OPERATOR => 2;
  10         19  
  10         548  
70 10     10   58 use constant OPEN_PAREN => 4;
  10         18  
  10         532  
71 10     10   66 use constant CLOSE_PAREN => 8;
  10         20  
  10         532  
72 10     10   61 use constant STOP => 16;
  10         19  
  10         752  
73             my @tokens = qw[OPERAND OPERATOR OPEN_PAREN CLOSE_PAREN STOP];
74              
75 10     10   5588 use Regexp::Common qw(delimited);
  10         27415  
  10         57  
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 5745 my $proto = shift;
121 9   33     79 my $self = bless {}, ref($proto) || $proto;
122 9         40 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 22 my $self = shift;
134 9         26 my %args = @_;
135 9 100       46 if ( $args{'operators'} ) {
136 2         4 my @ops = map lc $_, @{ $args{'operators'} };
  2         13  
137 2         11 $self->{'operators'} = [ @ops ];
138 2 100       9 @ops = reverse @ops if length $ops[1] > length $ops[0];
139 2         5 foreach ( @ops ) {
140 4 100       15 unless ( length ) {
141 1         3 $_ = "(?<=\\s)";
142             }
143             else {
144 3 100       14 if ( /^\w/ ) {
145 1         3 $_ = '\b'. "\Q$_\E";
146             }
147             else {
148 2         4 $_ = "\Q$_\E";
149             }
150 3 100       12 if ( /\w$/ ) {
151 1         3 $_ .= '\b';
152             }
153             }
154 4         126 $self->{'re_operator'} = qr{(?:$ops[0]|$ops[1])}i;
155             }
156             } else {
157 7         34 $self->{'operators'} = [qw(and or)];
158 7         33 $self->{'re_operator'} = qr{\b(?:AND|OR)\b}i;
159             }
160              
161 9 100       34 if ( $args{'parens'} ) {
162 1         2 $self->{'parens'} = $args{'parens'};
163 1         33 $self->{'re_open_paren'} = qr{\Q$args{'parens'}[0]\E};
164 1         24 $self->{'re_close_paren'} = qr{\Q$args{'parens'}[1]\E};
165             } else {
166 8         34 $self->{'re_open_paren'} = qr{\(};
167 8         25 $self->{'re_close_paren'} = qr{\)};
168             }
169 9         488 $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         1126 $self->{'re_operand'} = qr{(?:$re_delim|.+?(?=$self->{re_tokens}|["']|\Z))(?:$re_delim|(?!$self->{re_tokens}|["']).+?(?=$self->{re_tokens}|["']|\Z))*};
175              
176 9         41 foreach my $re (qw(re_operator re_operand re_open_paren re_close_paren)) {
177 36         1448 $self->{"m$re"} = qr{\G($self->{$re})};
178             }
179              
180 9         49 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 139658 my $self = shift;
315 33         146 my %args = (
316             string => '',
317             callback => {},
318             @_
319             );
320 33         95 my ($string, $cb) = @args{qw(string callback)};
321 33 50       96 $string = '' unless defined $string;
322              
323             # States
324 33         55 my $want = OPERAND | OPEN_PAREN | STOP;
325 33         45 my $last = 0;
326 33         55 my $depth = 0;
327              
328 33         43 while (1) {
329             # State Machine
330 143 100 100     2238 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         67 $cb->{'operator'}->( $1 );
334 17         105 $last = OPERATOR;
335 17         27 $want = OPERAND | OPEN_PAREN;
336             }
337             elsif ( ($want & OPEN_PAREN ) && $string =~ /$self->{'mre_open_paren'}/gc ) {
338 14         61 $cb->{'open_paren'}->( $1 );
339 14         110 $depth++;
340 14         22 $last = OPEN_PAREN;
341 14         25 $want = OPERAND | OPEN_PAREN;
342             }
343             elsif ( ($want & CLOSE_PAREN) && $string =~ /$self->{'mre_close_paren'}/gc ) {
344 14         53 $cb->{'close_paren'}->( $1 );
345 14         63 $depth--;
346 14         23 $last = CLOSE_PAREN;
347 14         23 $want = OPERATOR;
348 14 50       30 $want |= $depth? CLOSE_PAREN : STOP;
349             }
350             elsif ( ($want & OPERAND ) && $string =~ /$self->{'mre_operand'}/gc ) {
351 48         147 my $m = $1;
352 48         199 $m=~ s/\s+$//;
353 48         157 $cb->{'operand'}->( $m );
354 48         320 $last = OPERAND;
355 48         70 $want = OPERATOR;
356 48 100       108 $want |= $depth? CLOSE_PAREN : STOP;
357             }
358             elsif ( ($want & STOP) && $string =~ /\G\s*$/igc ) {
359 33         64 $last = STOP;
360 33         60 last;
361             }
362             else {
363 0         0 last;
364             }
365             }
366              
367 33 50 33     131 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       135 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 4632 if ( defined wantarray ) {
430 4         9 my $s = $_[1];
431 4         20 $s =~ s/(?=['\\])/\\/g;
432 4         15 return "'$s'";
433             } else {
434 4         21 $_[1] =~ s/(?=['\\])/\\/g;
435 4         10 substr($_[1], 0, 0) = "'";
436 4         8 $_[1] .= "'";
437 4         9 return;
438             }
439             }
440              
441             sub qq {
442 8 100   8 1 5118 if ( defined wantarray ) {
443 4         8 my $s = $_[1];
444 4         20 $s =~ s/(?=["\\])/\\/g;
445 4         16 return "\"$s\"";
446             } else {
447 4         20 $_[1] =~ s/(?=["\\])/\\/g;
448 4         9 substr($_[1], 0, 0) = '"';
449 4         10 $_[1] .= '"';
450 4         8 return;
451             }
452             }
453              
454             sub fq {
455 6 100   6 1 3770 if ( index( $_[1], "'" ) >= 0 ) {
456 2 100       8 if ( defined wantarray ) {
457 1         2 my $s = $_[1];
458 1         4 $s =~ s/(?=["\\])/\\/g;
459 1         5 return "\"$s\"";
460             } else {
461 1         5 $_[1] =~ s/(?=["\\])/\\/g;
462 1         4 substr($_[1], 0, 0) = '"';
463 1         3 $_[1] .= '"';
464 1         2 return;
465             }
466             } else {
467 4 100       14 if ( defined wantarray ) {
468 2         4 my $s = $_[1];
469 2         9 $s =~ s/(?=\\)/\\/g;
470 2         8 return "'$s'";
471             } else {
472 2         9 $_[1] =~ s/(?=\\)/\\/g;
473 2         4 substr($_[1], 0, 0) = "'";
474 2         5 $_[1] .= "'";
475 2         5 return;
476             }
477             }
478             }
479              
480             sub dq {
481 22 0   22 1 14203 return defined wantarray? $_[1] : ()
    50          
482             unless $_[1] =~ /^$re_delim$/o;
483              
484 22 100       66 if ( defined wantarray ) {
485 11         18 my $s = $_[1];
486 11         32 my $q = substr( $s, 0, 1, '' );
487 11         15 substr( $s, -1 ) = '';
488 11         150 $s =~ s/\\([$q\\])/$1/g;
489 11         44 return $s;
490             } else {
491 11         33 my $q = substr( $_[1], 0, 1, '' );
492 11         18 substr( $_[1], -1 ) = '';
493 11         153 $_[1] =~ s/\\([$q\\])/$1/g;
494 11         37 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 97 my ($self, $tree, $cb, @rest) = @_;
576              
577 34         48 my $skip_next = 0;
578              
579 34         48 my @res;
580 34         68 foreach my $entry ( @$tree ) {
581 114 100 50     224 $skip_next-- and next if $skip_next > 0;
582              
583 96 100       218 if ( ref $entry eq 'ARRAY' ) {
    100          
584 8         21 my $tmp = $self->filter( $entry, $cb, @rest );
585 8 100       24 $tmp = $tmp->[0] if @$tmp == 1;
586 8 100 100     43 if ( !$tmp || (ref $tmp eq 'ARRAY' && !@$tmp) ) {
      66        
587 2         3 pop @res;
588 2 100       7 $skip_next++ unless @res;
589             } else {
590 6         13 push @res, $tmp;
591             }
592             } elsif ( ref $entry ) {
593 66 100       131 if ( $cb->( $entry, @rest ) ) {
594 33         130 push @res, $entry;
595             } else {
596 33         101 pop @res;
597 33 100       84 $skip_next++ unless @res;
598             }
599             } else {
600 22         43 push @res, $entry;
601             }
602             }
603 34 100 100     116 return $res[0] if @res == 1 && ref $res[0] eq 'ARRAY';
604 33         170 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 116 my ($self, $tree, $cb, @rest) = @_;
646              
647 50         112 my ($res, $ea, $skip_next) = (0, $self->{'operators'}[1], 0);
648 50         90 foreach my $entry ( @$tree ) {
649 178 100 50     334 $skip_next-- and next if $skip_next > 0;
650 150 100       262 unless ( ref $entry ) {
651 64         99 $ea = lc $entry;
652             $skip_next++ if
653             ( $res && $ea eq $self->{'operators'}[1])
654 64 100 100     286 || (!$res && $ea eq $self->{'operators'}[0]);
      100        
      100        
655 64         112 next;
656             }
657              
658 86         112 my $cur;
659 86 100       146 if ( ref $entry eq 'ARRAY' ) {
660 8         28 $cur = $self->solve( $entry, $cb, @rest );
661             } else {
662 78         151 $cur = $cb->( $entry, @rest );
663             }
664 86 100       272 if ( $ea eq $self->{'operators'}[1] ) {
665 68   66     207 $res ||= $cur;
666             } else {
667 18   66     55 $res &&= $cur;
668             }
669             }
670 50         224 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 122 my ($self, $tree, $cb, @rest) = @_;
688              
689 40         125 my ($res, $ea, $skip_next) = (undef, $self->{'operators'}[1], 0);
690 40         81 foreach my $entry ( @$tree ) {
691 154 100 50     317 $skip_next-- and next if $skip_next > 0;
692 124 100       233 unless ( ref $entry ) {
693 48         79 $ea = lc $entry;
694             $skip_next++ if
695             ( $res && $ea eq $self->{'operators'}[1])
696 48 100 100     217 || (!$res && $ea eq $self->{'operators'}[0]);
      100        
      100        
697 48         88 next;
698             }
699              
700 76         123 my $cur;
701 76 50       138 if ( ref $entry eq 'ARRAY' ) {
702 0         0 $cur = $self->fsolve( $entry, $cb, @rest );
703             } else {
704 76         159 $cur = $cb->( $entry, @rest );
705             }
706 76 100       242 if ( defined $cur ) {
707 58   100     208 $res ||= 0;
708 58 100       104 if ( $ea eq $self->{'operators'}[1] ) {
709 50   66     166 $res ||= $cur;
710             } else {
711 8   66     27 $res &&= $cur;
712             }
713             } else {
714 18 100       42 $skip_next++ unless defined $res;
715             }
716             }
717 40         178 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 60 my ($self, $tree, $cb, @rest) = @_;
733              
734 19         32 my @res;
735              
736 19         47 my ($last, $ea, $skip_next) = (0, $self->{'operators'}[1], 0);
737 19         34 foreach my $entry ( @$tree ) {
738 51 100 50     98 $skip_next-- and next if $skip_next > 0;
739 47 100       93 unless ( ref $entry ) {
740 16         31 $ea = lc $entry;
741 16 100       27 unless ( ref $last ) {
742             $skip_next++ if
743             ( $last && $ea eq $self->{'operators'}[1])
744 8 100 100     52 || (!$last && $ea eq $self->{'operators'}[0]);
      100        
      100        
745             } else {
746 8         14 push @res, $entry;
747             }
748 16         33 next;
749             }
750              
751 31 50       62 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         64 $last = $cb->( $entry, @rest );
757 31 100       129 $last = $entry unless defined $last;
758             }
759 31 100       52 unless ( ref $last ) {
760 18 100       49 if ( $ea eq $self->{'operators'}[0] ) {
    50          
761             # (...) AND 0
762 4 100       10 unless ( $last ) { @res = () } else { pop @res };
  2         6  
  2         5  
763             }
764             elsif ( $ea eq $self->{'operators'}[1] ) {
765             # (...) OR 1
766 14 100       26 if ( $last ) { @res = () } else { pop @res };
  7         13  
  7         12  
767             }
768             } else {
769 13         30 push @res, $last;
770             }
771             }
772              
773 19 100       78 return $last unless @res; # solution
774 9         46 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