File Coverage

lib/Badger/Logic.pm
Criterion Covered Total %
statement 88 93 94.6
branch 25 40 62.5
condition 15 30 50.0
subroutine 26 26 100.0
pod 9 10 90.0
total 163 199 81.9


line stmt bran cond sub pod time code
1             #========================================================================
2             #
3             # Badger::Logic
4             #
5             # DESCRIPTION
6             # Simple parser and evaluator for boolean logic expressions, e.g.
7             # 'purple or orange', 'animal and (eats_nuts or eats_berries)'
8             #
9             # AUTHOR
10             # Andy Wardley
11             #
12             #========================================================================
13              
14             package Badger::Logic;
15              
16             use Badger::Class
17 2         22 version => 0.01,
18             debug => 0,
19             base => 'Badger::Base',
20             as_text => 'text',
21             constants => 'HASH',
22             constant => {
23             LOGIC => 'Badger::Logic',
24             },
25             exports => {
26             any => 'LOGIC Logic',
27             },
28             messages => {
29             no_text => 'No text expression specified.',
30             no_rhs => 'Missing expression following "%s"',
31             bad_text => 'Unexpected text in expression: %s',
32             parse => 'Could not parse logic expression: %s',
33             no_rparen => 'Missing ")" at end of nested expression',
34 2     2   395 };
  2         4  
35              
36             our $NODE = {
37             'item' => 'Badger::Logic::Item',
38             'not' => 'Badger::Logic::Not',
39             'and' => 'Badger::Logic::And',
40             'or' => 'Badger::Logic::Or',
41             };
42              
43             *test = \&evaluate;
44              
45              
46             sub Logic {
47             return @_
48 3 100   3 1 28 ? LOGIC->new(@_)
49             : LOGIC;
50             }
51              
52             sub new {
53 26     26 1 181 my $class = shift;
54 26         25 my $text = shift;
55 26 50       56 return $class->error_msg('no_text')
56             unless defined $text;
57 26 50       84 bless {
58             text => ref $text ? $text : \$text,
59             }, $class;
60             }
61              
62             sub evaluate {
63 21     21 1 24 my $self = shift;
64 21 50 33     61 my $args = @_ && ref $_[0] eq HASH ? shift : { @_ };
65 21         28 $self->tree->evaluate($args);
66             }
67              
68             sub tree {
69 22     22 1 20 my $self = shift;
70             return $self->{ tree }
71 22   33     48 ||= $self->parse($self->{ text });
72             }
73              
74             sub text {
75 8     8 1 10 ${ shift->{ text } };
  8         15  
76             }
77              
78             sub tree_text {
79 1     1 0 3 shift->tree->text;
80             }
81              
82             sub parse {
83 22     22 1 22 my $self = shift;
84 22         21 my $text = shift;
85 22 50       28 my $tref = ref $text ? $text : \$text;
86 22         19 $self->debug("parse($$tref)\n") if DEBUG;
87 22   50     27 my $expr = $self->parse_expr($tref)
88             || return $self->error_msg( parse => $$tref );
89 22         19 $self->debug("expr: ", $expr->text) if DEBUG;
90 22 50       33 if ($$tref =~ / \G \s* (.+) $/cigsx) {
91 0         0 return $self->error_msg( bad_text => $1 );
92             }
93 22         53 return $expr;
94             }
95              
96             sub parse_expr {
97 73     73 1 62 my $self = shift;
98 73         63 my $text = shift;
99 73   50     89 my $left = $self->parse_unary($text) || return;
100              
101 73         69 $self->debug("got unary: ", $left->text) if DEBUG;
102              
103 73 100       146 if ($$text =~ / \G \s+ (and|or) \s+ /cigx) {
    50          
104 35         46 my $op = $1;
105 35 50       49 $self->debug("binary op: $op\n") if $DEBUG;
106 35   50     48 my $right = $self->parse_expr($text)
107             || return $self->error_msg( no_rhs => $op );
108 35         80 return $NODE->{ lc $op }->new( $left, $right );
109             }
110             elsif ($$text =~ / \G \s* \( /cgx) {
111 0   0     0 my $expr = $self->parse_expr($text)
112             || return $self->error_msg( no_rhs => '(' );
113 0 0       0 $$text =~ / \G \s* \) /cgx
114             || return $self->error_msg('no_rparen');
115              
116 0         0 return $self->error_msg( bad_text => $1 );
117             }
118              
119 38         61 return $left;
120             }
121              
122             sub parse_unary {
123 73     73 1 60 my $self = shift;
124 73         60 my $text = shift;
125              
126 73 100       176 if ($$text =~ / \G \s* (not) \s+ /cigx) {
127 13         21 my $op = $1;
128 13 50       20 $self->debug("unary op: $op\n") if $DEBUG;
129 13   50     20 my $right = $self->parse_term($text)
130             || return $self->error_msg( no_rhs => $op );
131 13         30 return $NODE->{ lc $op }->new($right);
132             }
133 60   33     73 return $self->parse_term($text)
134             || $self->decline('Not a unary expression');
135             }
136              
137             sub parse_term {
138 73     73 1 64 my $self = shift;
139 73         61 my $text = shift;
140              
141 73 100       179 if ($$text =~ / \G \s* (\w+) /cigx) {
    100          
    50          
142 54 50       71 $self->debug("item: $1\n") if $DEBUG;
143 54         82 return $NODE->{ item }->new($1);
144             }
145             elsif ($$text =~ / \G \s* (['"]) ((?:\\?.)*?) \1 /cigx) {
146 3 50       4 $self->debug("string: $2\n") if $DEBUG;
147 3         6 return $NODE->{ item }->new($2);
148             }
149             elsif ($$text =~ / \G \s* \( /cgx) {
150 16   50     23 my $expr = $self->parse_expr($text)
151             || return $self->error_msg( no_rhs => '(' );
152 16 50       39 $$text =~ / \G \s* \) /cgx
153             || return $self->error_msg('no_rparen');
154 16         37 return $expr;
155             }
156              
157 0         0 return $self->decline('Not a term');
158             }
159              
160              
161             #=======================================================================
162             # node types
163             #=======================================================================
164              
165             package Badger::Logic::Expr;
166 2     2   21 use base 'Badger::Base';
  2         10  
  2         367  
167              
168             sub new {
169 105     105   94 my $class = shift;
170 105         303 bless [ @_ ], $class;
171             }
172              
173             package Badger::Logic::Item;
174 2     2   13 use base 'Badger::Logic::Expr';
  2         11  
  2         925  
175              
176             sub evaluate {
177 41     41   37 my $self = shift;
178 41 50 33     100 my $args = @_ && ref $_[0] eq 'HASH' ? shift : { @_ };
179 41         140 return $args->{ $self->[0] };
180             }
181              
182             sub text {
183 4     4   25 $_[0]->[0];
184             }
185              
186             package Badger::Logic::Not;
187 2     2   14 use base 'Badger::Logic::Expr';
  2         5  
  2         627  
188              
189             sub evaluate {
190 12     12   13 my $self = shift;
191 12 100       16 return $self->[0]->evaluate(@_) ? 0 : 1;
192             }
193              
194             sub text {
195 1     1   2 my $self = shift;
196 1         2 '(not ' . $self->[0]->text . ')';
197             }
198              
199             package Badger::Logic::And;
200 2     2   14 use base 'Badger::Logic::Expr';
  2         3  
  2         620  
201              
202             sub evaluate {
203 15     15   18 my $self = shift;
204 15   100     23 return $self->[0]->evaluate(@_)
205             && $self->[1]->evaluate(@_);
206             }
207              
208             sub text {
209 2     2   3 my $self = shift;
210 2         4 '(' . $self->[0]->text . ' and ' . $self->[1]->text . ')';
211             }
212              
213             package Badger::Logic::Or;
214 2     2   13 use base 'Badger::Logic::Expr';
  2         4  
  2         463  
215              
216 2     2   14 use Badger::Debug ':all';
  2         4  
  2         21  
217             sub evaluate {
218 16     16   14 my $self = shift;
219 16   100     23 return $self->[0]->evaluate(@_)
220             || $self->[1]->evaluate(@_);
221             }
222              
223             sub text {
224 1     1   2 my $self = shift;
225 1         2 '(' . $self->[0]->text . ' or ' . $self->[1]->text . ')';
226             }
227              
228             1;
229             __END__