#======================================================================== # # Badger::Logic # # DESCRIPTION # Simple parser and evaluator for boolean logic expressions, e.g. # 'purple or orange', 'animal and (eats_nuts or eats_berries)' # # AUTHOR # Andy Wardley # #======================================================================== package Badger::Logic; use Badger::Class version => 0.01, debug => 0, base => 'Badger::Base', as_text => 'text', constants => 'HASH', constant => { LOGIC => 'Badger::Logic', }, exports => { any => 'LOGIC Logic', }, messages => { no_text => 'No text expression specified.', no_rhs => 'Missing expression following "%s"', bad_text => 'Unexpected text in expression: %s', parse => 'Could not parse logic expression: %s', no_rparen => 'Missing ")" at end of nested expression', }; our $NODE = { 'item' => 'Badger::Logic::Item', 'not' => 'Badger::Logic::Not', 'and' => 'Badger::Logic::And', 'or' => 'Badger::Logic::Or', }; *test = \&evaluate; sub Logic { return @_ ? LOGIC->new(@_) : LOGIC; } sub new { my $class = shift; my $text = shift; return $class->error_msg('no_text') unless defined $text; bless { text => ref $text ? $text : \$text, }, $class; } sub evaluate { my $self = shift; my $args = @_ && ref $_[0] eq HASH ? shift : { @_ }; $self->tree->evaluate($args); } sub tree { my $self = shift; return $self->{ tree } ||= $self->parse($self->{ text }); } sub text { ${ shift->{ text } }; } sub tree_text { shift->tree->text; } sub parse { my $self = shift; my $text = shift; my $tref = ref $text ? $text : \$text; $self->debug("parse($$tref)\n") if DEBUG; my $expr = $self->parse_expr($tref) || return $self->error_msg( parse => $$tref ); $self->debug("expr: ", $expr->text) if DEBUG; if ($$tref =~ / \G \s* (.+) $/cigsx) { return $self->error_msg( bad_text => $1 ); } return $expr; } sub parse_expr { my $self = shift; my $text = shift; my $left = $self->parse_unary($text) || return; $self->debug("got unary: ", $left->text) if DEBUG; if ($$text =~ / \G \s+ (and|or) \s+ /cigx) { my $op = $1; $self->debug("binary op: $op\n") if $DEBUG; my $right = $self->parse_expr($text) || return $self->error_msg( no_rhs => $op ); return $NODE->{ lc $op }->new( $left, $right ); } elsif ($$text =~ / \G \s* \( /cgx) { my $expr = $self->parse_expr($text) || return $self->error_msg( no_rhs => '(' ); $$text =~ / \G \s* \) /cgx || return $self->error_msg('no_rparen'); return $self->error_msg( bad_text => $1 ); } return $left; } sub parse_unary { my $self = shift; my $text = shift; if ($$text =~ / \G \s* (not) \s+ /cigx) { my $op = $1; $self->debug("unary op: $op\n") if $DEBUG; my $right = $self->parse_term($text) || return $self->error_msg( no_rhs => $op ); return $NODE->{ lc $op }->new($right); } return $self->parse_term($text) || $self->decline('Not a unary expression'); } sub parse_term { my $self = shift; my $text = shift; if ($$text =~ / \G \s* (\w+) /cigx) { $self->debug("item: $1\n") if $DEBUG; return $NODE->{ item }->new($1); } elsif ($$text =~ / \G \s* (['"]) ((?:\\?.)*?) \1 /cigx) { $self->debug("string: $2\n") if $DEBUG; return $NODE->{ item }->new($2); } elsif ($$text =~ / \G \s* \( /cgx) { my $expr = $self->parse_expr($text) || return $self->error_msg( no_rhs => '(' ); $$text =~ / \G \s* \) /cgx || return $self->error_msg('no_rparen'); return $expr; } return $self->decline('Not a term'); } #======================================================================= # node types #======================================================================= package Badger::Logic::Expr; use base 'Badger::Base'; sub new { my $class = shift; bless [ @_ ], $class; } package Badger::Logic::Item; use base 'Badger::Logic::Expr'; sub evaluate { my $self = shift; my $args = @_ && ref $_[0] eq 'HASH' ? shift : { @_ }; return $args->{ $self->[0] }; } sub text { $_[0]->[0]; } package Badger::Logic::Not; use base 'Badger::Logic::Expr'; sub evaluate { my $self = shift; return $self->[0]->evaluate(@_) ? 0 : 1; } sub text { my $self = shift; '(not ' . $self->[0]->text . ')'; } package Badger::Logic::And; use base 'Badger::Logic::Expr'; sub evaluate { my $self = shift; return $self->[0]->evaluate(@_) && $self->[1]->evaluate(@_); } sub text { my $self = shift; '(' . $self->[0]->text . ' and ' . $self->[1]->text . ')'; } package Badger::Logic::Or; use base 'Badger::Logic::Expr'; use Badger::Debug ':all'; sub evaluate { my $self = shift; return $self->[0]->evaluate(@_) || $self->[1]->evaluate(@_); } sub text { my $self = shift; '(' . $self->[0]->text . ' or ' . $self->[1]->text . ')'; } 1; __END__ =head1 NAME Badger::Logic - parse and evaluate simple logical expressions =head1 SYNOPSIS use Badger::Logic 'Logic'; my $logic = Logic('animal and (eats_nuts or eats_berries)'); my $values = { animal => 1, eats_nuts => 1, } if ($logic->test($values)) { print "This is an animal that eats nuts or berries\n"; } =head1 DESCRIPTION This module implements a simple parser and evaluator for boolean logic expressions. It evolved from a piece of code that I originally wrote to handle role-based authentication in web applications. =head1 EXPORTABLE SUBROUTINES =head2 LOGIC This is a shortcut alias to C. use Badger::Logic 'LOGIC'; my $logic = LOGIC->new($expr); # same as Badger::Logic->new($expr); =head2 Logic() This subroutine returns the name of the C class when called without arguments. Thus it can be used as an alias for C as per L. use Badger::Logic 'Logic'; my $logic = Logic->new($expr); # same as Badger::Logic->new($expr); When called with arguments, it creates a new C object. my $logic = Logic($expr); # same as Badger::Logic->new($expr); =head1 METHODS =head2 new($expr) Constructor method to create a new C object from an expression. my $logic = Badger::Logic->new('animal and (cat or dog)'); =head2 evaluate($values) / test($values) Method to evaluate the expression. A reference to a hash array should be passed containing the values that the expression can test. my $values = { animal => 1, cat => 1, }; if ($logic->evaluate($values)) { print "This animal is a cat or a dog\n"; } =head2 tree() Returns a reference to the root of a tree of C objects that represent the parsed expression. =head2 text() Returns a text representation of the logic expression. =head1 INTERNAL METHODS =head2 parse($text) Main method to parse a logical expression. This calls L and then checks that all of the text has been successfully parsed. It returns a reference to a C object. =head2 parse_expr($text) Method to parse a binary expression. =head2 parse_unary($text) Method to parse a unary expression. =head2 parse_term($text) Method to parse a single term in a logical expression. =head1 AUTHOR Andy Wardley L =head1 COPYRIGHT Copyright (C) 2007-2009 Andy Wardley. All Rights Reserved. This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut # Local Variables: # mode: perl # perl-indent-level: 4 # indent-tabs-mode: nil # End: # # vim: expandtab shiftwidth=4: