# # Copyright (C) Motorola 2003 - All rights reserved # Copyright (C) Crawford Currie 2004 # Copyright (C) Crawford Currie and Aurelio A. Heckert 2005 # use strict; use Time::ParseDate; =begin text ---++ class Search Search operators work on the fields of a TWiki::Contrib::Map. The fields are given by name, and the values by strings. Strings should always be surrounded by 'single-quotes'. String which are regular expressions use 'perl' re syntax (see =man perlre= for help) *Warning* single and double quotes are not allowed in values! The following operators are available: | *Operator* | *LHS* | *RHS* | *Meaning* | | = | field name | regular expression | Value exactly matches this regular expression. The expression must match the whole string. | | != | field name | regular expression | Field is not this RE. Inverse of = | | =~ | field name | regular expression | Value contains this regular expression i.e. the RE is found somewhere in the field value. | | < | field name | number or text (string containing a number e.g '4' is a numeric value) | Value is < | | > | field name | number or text | Value is > | | >= | field name | number or text | Value is >= | | <= | field name | number or text | Value is <= | | EARLIER_THAN | field name | date (string containing a date e.g. '1 Apr 2003' | Date is earlier than the given date | | LATER_THAN | field name | date | Date is later than the given date | | WITHIN_DAYS | field name | integer | Date (which must be in the future) is within n _working_ days of todays date | | ! | none | expr | Boolean NOT | | AND | expr | expr | Boolean AND | | OR | expr | expr | Boolean OR | | () | N/A | N/A | Bracketed subexpression | Dates for =EARLIER_THAN=, =LATER_THAN= and =WITHIN_DAYS= must be dates in the format expected by =Time::ParseDate= (like the ActionTrackerPlugin). =WITHIN_DAYS= works out the number of _working_ days (i.e. excluding Saturday and Sunday). Apologies in advance if your weekend is offset ± a day! A search object implements the "matches" method as its general contract with the rest of the world. ---+++ Example Get a list of attachments that have a date earlier than 1st January 2000 $db = new TWiki::Contrib::DBCache( $web ); # always done $db->load(); my $search = new TWiki::Contrib::Search("date EARLIER_THAN '1st January 2000'"); foreach my $topic ($db->getKeys()) { my $attachments = $topic->get("attachments"); foreach my $val ($attachments->getValues()) { if ($search->matches($val)) { print $val->get("name") . "\n"; } } } =cut { package TWiki::Contrib::Search; # Operator precedences my %prec = ( '=' => 4, '=~' => 4, '!=' => 4, '>=' => 4, '<=' => 4, '<' => 4, '>' => 4, 'EARLIER_THAN' => 4, 'LATER_THAN' => 4, 'WITHIN_DAYS' => 4, 'IS_DATE' => 4, '!' => 3, 'AND' => 2, 'OR' => 1 ); my $bopRE = "AND\\b|OR\\b|!=|=~?|<=?|>=?|LATER_THAN\\b|EARLIER_THAN\\b|WITHIN_DAYS\\b|IS_DATE\\b"; my $uopRE = "!"; my $now = time(); # PUBLIC STATIC used for testing only; force 'now' to be a particular # time. sub forceTime { my $t = shift; $now = Time::ParseDate::parsedate( $t ); } =begin text ---+++ =new($string)= * =$string= - string containing an expression to parse Construct a new search node by parsing the passed expression. =cut sub new { my ( $class, $string, $left, $op, $right ) = @_; my $this; if ( defined( $string )) { if ( $string =~ m/^\s*$/o ) { return new TWiki::Contrib::Search( undef, undef, "TRUE", undef ); } else { my $rest; ( $this, $rest ) = _parse( $string ); return $this; } } else { $this = {}; $this->{right} = $right; $this->{left} = $left; $this->{op} = $op; return bless( $this, $class ); } } # PRIVATE STATIC generate a Search by popping the top two operands # and the top operator. Push the result back onto the operand stack. sub _apply { my ( $opers, $opands ) = @_; my $o = pop( @$opers ); my $r = pop( @$opands ); die "Bad search" unless defined( $r ); my $l = undef; if ( $o =~ /^$bopRE$/o ) { $l = pop( @$opands ); die "Bad search" unless defined( $l ); } my $n = new TWiki::Contrib::Search( undef, $l, $o, $r ); push( @$opands, $n); } # PRIVATE STATIC simple stack parser for grabbing boolean expressions sub _parse { my $string = shift; $string .= " "; my @opands; my @opers; while ( $string !~ m/^\s*$/o ) { if ( $string =~ s/^\s*($bopRE)//o ) { # Binary comparison op my $op = $1; while ( scalar( @opers ) > 0 && $prec{$op} < $prec{$opers[$#opers]} ) { _apply( \@opers, \@opands ); } push( @opers, $op ); } elsif ( $string =~ s/^\s*($uopRE)//o ) { # unary op push( @opers, $1 ); } elsif ( $string =~ s/^\s*\'(.*?)\'//o ) { push( @opands, $1 ); } elsif ( $string =~ s/^\s*([\w\.]+)//o ) { push( @opands, $1 ); } elsif ( $string =~ s/\s*\(//o ) { my $oa; ( $oa, $string ) = _parse( $string ); push( @opands, $oa ); } elsif ( $string =~ s/^\s*\)//o ) { last; } else { return ( undef, "Parser stuck at $string" ); } } while ( scalar( @opers ) > 0 ) { _apply( \@opers, \@opands ); } die "Bad search" unless ( scalar( @opands ) == 1 ); return ( pop( @opands ), $string ); } =begin text ---+++ =matches($object)= -> boolean * -=$object= - object to test; must implement =get= See if object matches the search. =$object= can actually be any object that provides the method "get" that returns a value given a string key. =cut sub matches { my ( $this, $map ) = @_; my $op = $this->{op}; return 1 if ( $op eq "TRUE" ); my $r = $this->{right}; return 0 unless ( defined( $r )); if ($op eq "!") { return !( $r->matches( $map )) }; my $l = $this->{left}; return 0 unless ( defined( $l )); if ($op eq "OR" ) { return ( $l->matches( $map ) || $r->matches( $map )) }; if ($op eq "AND" ) { return ( $l->matches( $map ) && $r->matches( $map )) }; return 0 unless ( defined( $map )); my $val = $map->get( $l ); # Compatibility: values have now moved down into the form for topics, # but we need this just in case the search specifier is "old style" unless ($val) { $val = $map->get($map->get("form"))->get( $l ); } return 0 unless ( defined( $val )); # Check if it have numeric values in both sides: my $isNum = 0; if ( ( ($val+0) eq $val ) && ( ($r+0) eq $r ) ) { $isNum = 1; } if ( $op eq "=" ) { return ( $val =~ m/^$r$/ ) }; if ( $op eq "!=" ) { return ( $val !~ m/^$r$/ ) }; if ( $op eq "=~" ) { return ( $val =~ m/$r/ ) }; #if ( $op eq ">" ) { return ( $val > $r ) }; #if ( $op eq "<" ) { return ( $val < $r ) }; #if ( $op eq ">=" ) { return ( $val >= $r ) }; #if ( $op eq "<=" ) { return ( $val <= $r ) }; if ( $op eq ">" ) { if ( $isNum ) { return ( $val > $r ); } else { return ( $val gt $r ); } } if ( $op eq "<" ) { if ( $isNum ) { return ( $val < $r ); } else { return ( $val lt $r ); } } if ( $op eq ">=" ) { if ( $isNum ) { return ( $val >= $r ); } else { return ( $val ge $r ); } } if ( $op eq "<=" ) { if ( $isNum ) { return ( $val <= $r ); } else { return ( $val le $r ); } } my $lval = Time::ParseDate::parsedate( $val ); return 0 unless ( defined( $lval )); if ( $op eq "WITHIN_DAYS" ) { return ( $lval >= $now && workingDays( $now, $lval ) <= $r ); } my $rval = Time::ParseDate::parsedate( $r ); return 0 unless ( defined( $rval )); if ( $op eq "LATER_THAN" ) { return ( $lval > $rval ) }; if ( $op eq "EARLIER_THAN" ) { return ( $lval < $rval ) }; if ( $op eq "IS_DATE") { return ( $lval == $rval ) }; return 0; } # PUBLIC STATIC calculate working days between two times # Published because it's useful elsewhere sub workingDays { my ( $start, $end ) = @_; use integer; my $elapsed_days = ( $end - $start ) / ( 60 * 60 * 24 ); # total number of elapsed 7-day weeks my $whole_weeks = $elapsed_days / 7; my $extra_days = $elapsed_days - ( $whole_weeks * 7 ); if ( $extra_days > 0 ) { my @lt = localtime( $start ); my $wday = $lt[6]; # weekday, 0 is sunday if ($wday == 0) { $extra_days-- if ( $extra_days > 0 ); } else { $extra_days-- if ($extra_days > (6 - $wday)); $extra_days-- if ($extra_days > (6 - $wday)); } } return $whole_weeks * 5 + $extra_days; } =begin text ---+++ =toString()= -> string Generates a string representation of the object. =cut sub toString { my $this = shift; my $text = ""; if ( defined( $this->{left} )) { if ( !ref($this->{left}) ) { $text .= $this->{left}; } else { $text .= "(" . $this->{left}->toString() . ")"; } $text .= " "; } $text .= $this->{op} . " "; if ( !ref($this->{right}) ) { $text .= "'" . $this->{right} . "'"; } else { $text .= "(" . $this->{right}->toString() . ")"; } return $text; } } 1;