File Coverage

blib/lib/DBIx/TextIndex/QueryParser.pm
Criterion Covered Total %
statement 15 105 14.2
branch 0 46 0.0
condition 0 8 0.0
subroutine 5 11 45.4
pod 5 5 100.0
total 25 175 14.2


line stmt bran cond sub pod time code
1             package DBIx::TextIndex::QueryParser;
2              
3 8     8   47 use strict;
  8         13  
  8         1986  
4 8     8   1581 use warnings;
  8         1414  
  8         1959  
5              
6             our $VERSION = '0.26';
7              
8 8     8   42 use base qw(DBIx::TextIndex);
  8         14  
  8         673  
9              
10 8     8   47 use DBIx::TextIndex::Exception;
  8         1642  
  8         382  
11 8     8   13570 use Text::Balanced qw(extract_bracketed extract_delimited);
  8         218553  
  8         13250  
12              
13             my $QRY = 'DBIx::TextIndex::Exception::Query';
14              
15             sub new {
16 0     0 1   my $pkg = shift;
17 0   0       my $class = ref($pkg) || $pkg;
18 0           my $self = bless {}, $class;
19 0   0       my $args = shift || {};
20 0           foreach my $field (keys %$args) {
21 0           $self->{uc($field)} = $args->{$field};
22             }
23 0           return $self;
24             }
25              
26             sub term_fields {
27 0     0 1   my $self = shift;
28 0           return sort { $a cmp $b } keys %{$self->{TERM_FIELDS}};
  0            
  0            
29             }
30              
31             sub parse {
32 0     0 1   my $self = shift;
33 0           delete($self->{TERM_FIELDS});
34 0           delete($self->{STOPLISTED_QUERY});
35 0           $self->_parse(@_);
36             }
37              
38             sub _parse {
39 0     0     my $self = shift;
40 0           my $q = shift;
41 0           my @clauses;
42             my $term;
43              
44 0           $q =~ s/\s+$//;
45              
46 0           while ($q) {
47 0           my $clause;
48              
49 0 0         if ($q =~ s/^\s+//) {
50 0           next;
51             }
52              
53 0 0         if ($q =~ s/^(AND|OR|\&\&|\|\|)\s+//) {
54 0           $clause->{CONJ} = $1;
55 0 0         $clause->{CONJ} = 'AND' if $clause->{CONJ} eq '&&';
56 0 0         $clause->{CONJ} = 'OR' if $clause->{CONJ} eq '||';
57             }
58              
59 0 0         if ($q =~ s/^\+//) {
    0          
60 0           $clause->{MODIFIER} = 'AND';
61             } elsif ($q =~ s/^(-|NOT|!)\s*//) {
62 0           $clause->{MODIFIER} = 'NOT';
63             } else {
64 0           $clause->{MODIFIER} = 'OR';
65             }
66              
67 0 0         if ($q =~ s/^(\w+)://) {
68 0           $clause->{FIELD} = $1;
69 0           $self->{TERM_FIELDS}->{$clause->{FIELD}}++;
70             } else {
71 0           $self->{TERM_FIELDS}->{__DEFAULT}++;
72             }
73              
74 0 0         if ($q =~ m/^\(/) {
    0          
    0          
    0          
75 0           my ($extract, $remain) = extract_bracketed($q, "(");
76 0 0         unless ($extract) {
77             # FIXME: hard coded error message
78 0           throw $QRY( error => 'Open and close parentheses are uneven.');
79             }
80 0           $q = $remain;
81 0           $extract =~ s/^\(//;
82 0           $extract =~ s/\)$//;
83 0           $clause->{TYPE} = 'QUERY';
84 0           $clause->{QUERY} = $self->_parse($extract);
85             } elsif ($q =~ m/^\"/) {
86 0           my ($extract, $remain) = extract_delimited($q, '"');
87 0 0         unless ($extract) {
88             # FIXME: hard coded error message
89 0           throw $QRY( error => 'Quotes must be used in matching pairs.')
90             }
91 0           $q = $remain;
92 0           $extract =~ s/^\"//;
93 0           $extract =~ s/\"$//;
94 0           $clause->{TYPE} = 'PHRASE';
95 0           $term = $extract;
96 0           $clause->{PHRASETERMS} = $self->_parse($extract);
97 0 0         if ($q =~ s/^~(\d+)//) {
98 0           $clause->{PROXIMITY} = $1;
99             } else {
100 0           $clause->{PROXIMITY} = 1;
101             }
102             } elsif ($q =~ s/^(\S+(?:[\+\-\&\.\@\']|\\\*)\S+)//) {
103 0           $clause->{TYPE} = 'IMPLICITPHRASE';
104 0           $term = $1;
105 0           $term =~ s:\\\*:\*:g;
106 0           $clause->{PHRASETERMS} =
107             $self->_parse(join(' ', split('[\+\-\&\.\@\'\*]',$term))); # FIXME: check for double characters, which would cause empty phrase terms
108             } elsif ($q =~ s/^(\S+)\+//) {
109 0           $clause->{TYPE} = 'PLURAL';
110 0           $term = $1;
111             } else {
112 0           $q =~ s/(\S+)//;
113 0           my $t = $1;
114 0 0         if ($t =~ m/[\?\*]/) {
115 0           $clause->{TYPE} = 'WILD';
116             } else {
117 0           $clause->{TYPE} = 'TERM';
118             }
119 0           $term = $t;
120             }
121 0 0         $clause->{TERM} = $self->_lc_and_unac($term) if $term;
122 0 0         if ($clause->{TERM}) {
123 0 0         next unless $clause->{TERM} =~ m/[a-z0-9]/;
124 0 0         next if $self->_stoplisted($clause->{TERM});
125             }
126 0           push @clauses, $clause;
127             }
128 0           my $folded = fold_nested_phrases(\@clauses);
129 0           return $folded;
130             }
131              
132             sub fold_nested_phrases {
133 0     0 1   my $clauses = shift;
134 0           my @folded;
135 0           foreach my $clause (@$clauses) {
136 0 0 0       if ($clause->{TYPE} eq 'PHRASE' ||
137             $clause->{TYPE} eq 'IMPLICITPHRASE') {
138 0           my @folded_terms;
139 0           foreach my $phraseterm (@{$clause->{PHRASETERMS}}) {
  0            
140 0 0         if ($phraseterm->{TYPE} eq 'IMPLICITPHRASE') {
141 0           push @folded_terms,
142             fold_nested_phrases($phraseterm->{PHRASETERMS});
143             } else {
144 0           push @folded_terms, $phraseterm;
145             }
146             }
147 0           $clause->{PHRASETERMS} = \@folded_terms;
148             }
149 0           push @folded, $clause;
150             }
151 0 0         return wantarray ? @folded : \@folded;
152             }
153              
154             sub stoplisted_query {
155 0     0 1   my $self = shift;
156 0 0         return ref $self->{STOPLISTED_QUERY} eq 'ARRAY' ?
157             $self->{STOPLISTED_QUERY} : [];
158             }
159              
160             1;
161             __END__