japetheape-scoped_search 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Willem van Bergen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,78 @@
1
+ = scoped_search
2
+
3
+ The <b>scoped_search</b> Rails plugin makes it easy to search your ActiveRecord models. Searching is
4
+ performed using a query string, which should be passed to the named_scope *search_for* that uses SQL
5
+ <tt>LIKE %keyword%</tt> conditions for searching (ILIKE for Postgres). You can specify what fields
6
+ should be used for searching.
7
+
8
+ == Installing
9
+
10
+ The recommended method to enable scoped_search in your project is adding the scoped_search gem to your environment. Add the following code to your Rails configuration in <tt>config/environment.rb</tt>:
11
+
12
+ Rails::Initializer.run do |config|
13
+ ...
14
+ config.gem 'wvanbergen-scoped_search', :lib => 'scoped_search',
15
+ source => 'http://gems.github.com/'
16
+ end
17
+
18
+ Run <tt>sudo rake gems:install</tt> to install the gem.
19
+
20
+ Another alternative is to install scoped_search as a Rails plugin:
21
+
22
+ script/plugin install git://github.com/wvanbergen/scoped_search.git
23
+
24
+ == Usage
25
+
26
+ First, you have to specify in what columns should be searched:
27
+
28
+ class User < ActiveRecord::Base
29
+ searchable_on :first_name, :last_name
30
+ end
31
+
32
+
33
+ Now, the <b>search_for</b> scope is available for queries. You should pass a query string to the scope.
34
+ This can be empty or nil, in which case all no search conditions are set (and all records will be returned).
35
+
36
+ User.search_for(params[:q]).each { |project| ... }
37
+
38
+
39
+ You can also search on associate models. This works with <b>belongs_to</b>, <b>has_one</b>, <b>has_many</b>,
40
+ <b>has_many :through</b>, and <b>HABTM</b>. For example if a User <b>has_many</b> Notes (title, content, created_at, updated_at)
41
+
42
+ class User < ActiveRecord::Base
43
+ has_many: notes
44
+ searchable_on :first_name, :last_name, :notes_title, :notes_content
45
+ end
46
+
47
+ The search query language is simple. It supports these constructs:
48
+ * <b>words:</b> <tt>some search keywords</tt>
49
+ * <b>phrases:</b> <tt>"a single search phrase"</tt>
50
+ * <b>negation:</b> <tt>"look for this" -"but do not look for this phrase and this" -word</tt>
51
+ * <b>OR words/phrases:</b> word/phrase OR word/phrase. Example: <tt>"Hello World" OR "Hello Moon"</tt>
52
+ * <b>dates:</b> mm/dd/yyyy, dd/mm/yyyy, yyyy/mm/dd, yyyy-mm-dd
53
+ * <b>date ranges:</b> > date, >= date, < date, <= date, date TO date. Examples: <tt>> 30/05/1983</tt>, <tt>< 2009-01-30</tt>
54
+
55
+ This functionality is build on <tt>named_scope</tt>. The searchable_on statement creates
56
+ a named_scope *search_for*. Because of this, you can actually chain the call with
57
+ other scopes. For example, this can be very useful if you only want to search in
58
+ projects that are accessible by a given user.
59
+
60
+ class Project < ActiveRecord::Base
61
+ searchable_on :name, :description
62
+ named_scope :accessible_by, lambda { |user| ... }
63
+ end
64
+
65
+ # using chained named_scopes and will_paginate
66
+ Project.accessible_by(current_user).search_for(params[:q]).paginate(:page => params[:page], :include => :tasks)
67
+
68
+ == Additional resources
69
+
70
+ * Source code: http://github.com/wvanbergen/scoped_search/tree/master
71
+ * Project wiki: http://wiki.github.com/wvanbergen/scoped_search
72
+ * RDoc documentation: http://wvanbergen.github.com/scoped_search
73
+ * wvanbergen's blog posts: http://techblog.floorplanner.com/tag/scoped_search
74
+
75
+ == License
76
+
77
+ This plugin is released under the MIT license. Please contact weshays (http://github.com/weshays)
78
+ or wvanbergen (http://github.com/wvanbergen) for any questions.
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ Dir['tasks/*.rake'].each { |file| load(file) }
2
+
3
+ desc 'Default: run unit tests for only sqlite.'
4
+ task :default => [:test]
5
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'scoped_search'
@@ -0,0 +1,108 @@
1
+ module ScopedSearch
2
+
3
+ module ClassMethods
4
+
5
+ def self.extended(base) # :nodoc:
6
+ require 'scoped_search/reg_tokens'
7
+ require 'scoped_search/query_language_parser'
8
+ require 'scoped_search/query_conditions_builder'
9
+ end
10
+
11
+ # Creates a named scope in the class it was called upon.
12
+ #
13
+ # fields:: The fields to search on.
14
+ def searchable_on(*fields)
15
+ # Make sure that the table to be searched actually exists
16
+ if self.table_exists?
17
+
18
+ # Get a collection of fields to be searched on.
19
+ if fields.first.class.to_s == 'Hash'
20
+ if fields.first.has_key?(:only)
21
+ # only search on these fields.
22
+ fields = fields.first[:only]
23
+ elsif fields.first.has_key?(:except)
24
+ # Get all the fields and remove any that are in the -except- list.
25
+ fields = self.column_names.collect { |column| fields.first[:except].include?(column.to_sym) ? nil : column.to_sym }.compact
26
+ end
27
+ end
28
+
29
+ # Get an array of associate modules.
30
+ assoc_models = self.reflections.collect { |key,value| key }
31
+
32
+ # Subtract out the fields to be searched on that are part of *this* model.
33
+ # Any thing left will be associate module fields to be searched on.
34
+ assoc_fields = fields - self.column_names.collect { |column| column.to_sym }
35
+
36
+ # Subtraced out the associated fields from the fields so that you are only left
37
+ # with fields in *this* model.
38
+ fields -= assoc_fields
39
+
40
+ # Loop through each of the associate models and group accordingly each
41
+ # associate model field to search. Assuming the following relations:
42
+ # has_many :clients
43
+ # has_many :notes,
44
+ # belongs_to :user_type
45
+ # assoc_groupings will look like
46
+ # assoc_groupings = {:clients => [:first_name, :last_name],
47
+ # :notes => [:descr],
48
+ # :user_type => [:identifier]}
49
+ assoc_groupings = {}
50
+ assoc_models.each do |assoc_model|
51
+ assoc_groupings[assoc_model] = []
52
+ assoc_fields.each do |assoc_field|
53
+ unless assoc_field.to_s.match(/^#{assoc_model.to_s}_/).nil?
54
+ assoc_groupings[assoc_model] << assoc_field.to_s.sub(/^#{assoc_model.to_s}_/, '').to_sym
55
+ end
56
+ end
57
+ end
58
+
59
+ # If a grouping does not contain any fields to be searched on then remove it.
60
+ assoc_groupings = assoc_groupings.delete_if {|group, field_group| field_group.empty?}
61
+
62
+ # Set the appropriate class attributes.
63
+ self.cattr_accessor :scoped_search_fields, :scoped_search_assoc_groupings
64
+ self.scoped_search_fields = fields
65
+ self.scoped_search_assoc_groupings = assoc_groupings
66
+ self.named_scope :search_for, lambda { |keywords| self.build_scoped_search_conditions(keywords) }
67
+ end
68
+ end
69
+
70
+ # Build a hash that is used for the named_scope search_for.
71
+ # This function will split the search_string into keywords, and search for all the keywords
72
+ # in the fields that were provided to searchable_on.
73
+ #
74
+ # search_string:: The search string to parse.
75
+ def build_scoped_search_conditions(search_string)
76
+ if search_string.nil? || search_string.strip.blank?
77
+ return {:conditions => nil}
78
+ else
79
+ query_fields = {}
80
+ self.scoped_search_fields.each do |field|
81
+ field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
82
+ query_fields[field_name] = self.columns_hash[field.to_s].type
83
+ end
84
+
85
+ assoc_model_indx = 0
86
+ assoc_fields_indx = 1
87
+ assoc_models_to_include = []
88
+ self.scoped_search_assoc_groupings.each do |group|
89
+ assoc_models_to_include << group[assoc_model_indx]
90
+ group[assoc_fields_indx].each do |group_field|
91
+ field_name = connection.quote_table_name(group[assoc_model_indx].to_s.pluralize) + "." + connection.quote_column_name(group_field)
92
+ query_fields[field_name] = self.reflections[group[assoc_model_indx]].klass.columns_hash[group_field.to_s].type
93
+ end
94
+ end
95
+
96
+ search_conditions = QueryLanguageParser.parse(search_string)
97
+ conditions = QueryConditionsBuilder.build_query(search_conditions, query_fields)
98
+
99
+ retVal = {:conditions => conditions}
100
+ retVal[:include] = assoc_models_to_include unless assoc_models_to_include.empty?
101
+
102
+ return retVal
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ ActiveRecord::Base.send(:extend, ScopedSearch::ClassMethods)
@@ -0,0 +1,209 @@
1
+ module ScopedSearch
2
+
3
+ class QueryConditionsBuilder
4
+
5
+ # Builds the query string by calling the build method on a new instances of QueryConditionsBuilder.
6
+ def self.build_query(search_conditions, query_fields)
7
+ self.new.build(search_conditions, query_fields)
8
+ end
9
+
10
+ # Initializes the default class variables.
11
+ def initialize
12
+ @query_fields = nil
13
+ @query_params = {}
14
+
15
+ @sql_like = 'LIKE'
16
+
17
+ if ActiveRecord::Base.connected? and ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
18
+ @sql_like = 'ILIKE'
19
+ end
20
+ end
21
+
22
+
23
+ # Build the query based on the search conditions and the fields to query.
24
+ #
25
+ # Hash query_options : A hash of fields and field types.
26
+ #
27
+ # Example:
28
+ #
29
+ # search_conditions = [["Wes", :like], ["Hays", :not], ["Hello World", :like], ["Goodnight Moon", :not],
30
+ # ["Bob OR Wes", :or], ["Happy cow OR Sad Frog", :or], ["Man made OR Dogs", :or],
31
+ # ["Cows OR Frog Toys", :or], ['9/28/1980, :datetime]]
32
+ # query_fields = {:first_name => :string, :created_at => :datetime}
33
+ #
34
+ # Exceptons :
35
+ # 1) If search_conditions is not an array
36
+ # 2) If query_fields is not a Hash
37
+ def build(search_conditions, query_fields)
38
+ raise 'search_conditions must be a hash' unless search_conditions.class.to_s == 'Array'
39
+ raise 'query_fields must be a hash' unless query_fields.class.to_s == 'Hash'
40
+ @query_fields = query_fields
41
+
42
+ conditions = []
43
+
44
+ search_conditions.each_with_index do |search_condition, index|
45
+ keyword_name = "keyword_#{index}".to_sym
46
+ conditions << case search_condition.last
47
+ # :like also handles integers
48
+ when :like then like_condition(keyword_name, search_condition.first)
49
+ when :not then not_like_condition(keyword_name, search_condition.first)
50
+
51
+ when :or then or_condition(keyword_name, search_condition.first)
52
+
53
+ when :less_than_date then less_than_date(keyword_name, search_condition.first)
54
+ when :less_than_or_equal_to_date then less_than_or_equal_to_date(keyword_name, search_condition.first)
55
+ when :as_of_date then as_of_date(keyword_name, search_condition.first)
56
+ when :greater_than_date then greater_than_date(keyword_name, search_condition.first)
57
+ when :greater_than_or_equal_to_date then greater_than_or_equal_to_date(keyword_name, search_condition.first)
58
+
59
+ when :between_dates then between_dates(keyword_name, search_condition.first)
60
+ end
61
+ end
62
+
63
+ [conditions.compact.join(' AND '), @query_params]
64
+ end
65
+
66
+
67
+ private
68
+
69
+ def like_condition(keyword_name, value)
70
+ retVal = []
71
+ @query_fields.each do |field, field_type| #|key,value|
72
+ if field_type == :string or field_type == :text
73
+ @query_params[keyword_name] = "%#{value}%"
74
+ retVal << "#{field} #{@sql_like} :#{keyword_name.to_s}"
75
+ end
76
+ if value.strip =~ /^[0-9]+$/ and (field_type == :int or field_type == :integer)
77
+ qkey = "#{keyword_name}_#{value.strip}"
78
+ @query_params[qkey.to_sym] = value.strip.to_i
79
+ retVal << "#{field} = :#{qkey}"
80
+ end
81
+ end
82
+ "(#{retVal.join(' OR ')})"
83
+ end
84
+
85
+ def not_like_condition(keyword_name, value)
86
+ @query_params[keyword_name] = "%#{value}%"
87
+ retVal = []
88
+ @query_fields.each do |field, field_type| #|key,value|
89
+ if field_type == :string or field_type == :text
90
+ retVal << "(#{field} NOT #{@sql_like} :#{keyword_name.to_s} OR #{field} IS NULL)"
91
+ end
92
+ end
93
+ "(#{retVal.join(' AND ')})"
94
+ end
95
+
96
+ def or_condition(keyword_name, value)
97
+ retVal = []
98
+ word1, word2 = value.split(' OR ')
99
+ keyword_name_a = "#{keyword_name.to_s}a".to_sym
100
+ keyword_name_b = "#{keyword_name.to_s}b".to_sym
101
+ @query_params[keyword_name_a] = "%#{word1}%"
102
+ @query_params[keyword_name_b] = "%#{word2}%"
103
+ @query_fields.each do |field, field_type| #|key,value|
104
+ if field_type == :string or field_type == :text
105
+ retVal << "(#{field} #{@sql_like} :#{keyword_name_a.to_s} OR #{field} #{@sql_like} :#{keyword_name_b.to_s})"
106
+ end
107
+ if (word1.strip =~ /^[0-9]+$/ and word2.strip =~ /^[0-9]+$/) and (field_type == :int or field_type == :integer)
108
+ qkeya = "#{keyword_name}_a_#{word1.strip}"
109
+ qkeyb = "#{keyword_name}_b_#{word2.strip}"
110
+ @query_params[qkeya] = word1.strip.to_i
111
+ @query_params[qkeyb] = word2.strip.to_i
112
+ retVal << "(#{field} = :#{qkeya} OR #{field} = :#{qkeyb})"
113
+ elsif (word1.strip =~ /^[0-9]+$/ or word2.strip =~ /^[0-9]+$/) and (field_type == :int or field_type == :integer)
114
+ num_word = word1.strip =~ /^[0-9]+$/ ? word1.strip.to_i : word2.strip.to_i
115
+ qkey = "#{keyword_name}_#{num_word}"
116
+ @query_params[qkey.to_sym] = num_word
117
+ retVal << "(#{field} = :#{qkey})"
118
+ end
119
+ end
120
+ "(#{retVal.join(' OR ')})"
121
+ end
122
+
123
+ def less_than_date(keyword_name, value)
124
+ helper_date_operation('<', keyword_name, value)
125
+ end
126
+
127
+ def less_than_or_equal_to_date(keyword_name, value)
128
+ helper_date_operation('<=', keyword_name, value)
129
+ end
130
+
131
+ def as_of_date(keyword_name, value)
132
+ retVal = []
133
+ begin
134
+ dt = Date.parse(value) # This will throw an exception if it is not valid
135
+ @query_params[keyword_name] = dt.to_s
136
+ @query_fields.each do |field, field_type| #|key,value|
137
+ if field_type == :date or field_type == :datetime or field_type == :timestamp
138
+ retVal << "#{field} = :#{keyword_name.to_s}"
139
+ end
140
+ end
141
+ rescue
142
+ # do not search on any date columns since the date is invalid
143
+ retVal = [] # Reset just in case
144
+ end
145
+
146
+ # Search the text fields for the date as well as it could be in text.
147
+ # Also still search on the text columns for an invalid date as it could
148
+ # have a different meaning.
149
+ found_text_fields_to_search = false
150
+ keyword_name_b = "#{keyword_name}b".to_sym
151
+ @query_fields.each do |field, field_type| #|key,value|
152
+ if field_type == :string or field_type == :text
153
+ found_text_fields_to_search = true
154
+ retVal << "#{field} #{@sql_like} :#{keyword_name_b.to_s}"
155
+ end
156
+ end
157
+ if found_text_fields_to_search
158
+ @query_params[keyword_name_b] = "%#{value}%"
159
+ end
160
+
161
+ retVal.empty? ? '' : "(#{retVal.join(' OR ')})"
162
+ end
163
+
164
+ def greater_than_date(keyword_name, value)
165
+ helper_date_operation('>', keyword_name, value)
166
+ end
167
+
168
+ def greater_than_or_equal_to_date(keyword_name, value)
169
+ helper_date_operation('>=', keyword_name, value)
170
+ end
171
+
172
+ def between_dates(keyword_name, value)
173
+ date1, date2 = value.split(' TO ')
174
+ dt1 = Date.parse(date1) # This will throw an exception if it is not valid
175
+ dt2 = Date.parse(date2) # This will throw an exception if it is not valid
176
+ keyword_name_a = "#{keyword_name.to_s}a".to_sym
177
+ keyword_name_b = "#{keyword_name.to_s}b".to_sym
178
+ @query_params[keyword_name_a] = dt1.to_s
179
+ @query_params[keyword_name_b] = dt2.to_s
180
+
181
+ retVal = []
182
+ @query_fields.each do |field, field_type| #|key,value|
183
+ if field_type == :date or field_type == :datetime or field_type == :timestamp
184
+ retVal << "(#{field} BETWEEN :#{keyword_name_a.to_s} AND :#{keyword_name_b.to_s})"
185
+ end
186
+ end
187
+ "(#{retVal.join(' OR ')})"
188
+ rescue
189
+ # The date is not valid so just ignore it
190
+ return nil
191
+ end
192
+
193
+
194
+ def helper_date_operation(operator, keyword_name, value)
195
+ dt = Date.parse(value) # This will throw an exception if it is not valid
196
+ @query_params[keyword_name] = dt.to_s
197
+ retVal = []
198
+ @query_fields.each do |field, field_type| #|key,value|
199
+ if field_type == :date or field_type == :datetime or field_type == :timestamp
200
+ retVal << "#{field} #{operator} :#{keyword_name.to_s}"
201
+ end
202
+ end
203
+ "(#{retVal.join(' OR ')})"
204
+ rescue
205
+ # The date is not valid so just ignore it
206
+ return nil
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,117 @@
1
+ module ScopedSearch
2
+
3
+ # Used to parse and build the conditions tree for the search.
4
+ class QueryLanguageParser
5
+
6
+ # Parses the query string by calling the parse_query method on a new instances of QueryLanguageParser.
7
+ #
8
+ # query:: The query string to parse. If nil the all values will be returned (default is nil)
9
+ # If the query string is longer then 300 characters then it will be truncated to a
10
+ # length of 300.
11
+ def self.parse(query)
12
+ self.new.parse_query(query)
13
+ end
14
+
15
+ # Parse the query string.
16
+ #
17
+ # query:: The query string to parse. If nil the all values will be returned (default is nil)
18
+ # If the query string is longer then 300 characters then it will be truncated to a
19
+ # length of 300.
20
+ def parse_query(query = nil)
21
+ # truncate query string at 300 characters
22
+ query = query[0...300] if query.length > 300
23
+ return build_conditions_tree(tokenize(query))
24
+ end
25
+
26
+ protected
27
+
28
+ # Build the conditions tree based on the tokens found.
29
+ #
30
+ # tokens:: An array of tokens.
31
+ def build_conditions_tree(tokens)
32
+ conditions_tree = []
33
+
34
+ negate = false
35
+ tokens.each do |item|
36
+ case item
37
+ when :not
38
+ negate = true
39
+ else
40
+ if /^.+[ ]OR[ ].+$/ =~ item
41
+ conditions_tree << [item, :or]
42
+ elsif /^#{RegTokens::BetweenDateFormatMMDDYYYY}$/ =~ item or
43
+ /^#{RegTokens::BetweenDateFormatYYYYMMDD}$/ =~ item or
44
+ /^#{RegTokens::BetweenDatabaseFormat}$/ =~ item
45
+ conditions_tree << [item, :between_dates]
46
+ elsif /^#{RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
47
+ /^#{RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
48
+ /^#{RegTokens::GreaterThanOrEqualToDatabaseFormat}$/ =~ item
49
+ conditions_tree << [item, :greater_than_or_equal_to_date]
50
+ elsif /^#{RegTokens::LessThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
51
+ /^#{RegTokens::LessThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
52
+ /^#{RegTokens::LessThanOrEqualToDatabaseFormat}$/ =~ item
53
+ conditions_tree << [item, :less_than_or_equal_to_date]
54
+ elsif /^#{RegTokens::GreaterThanDateFormatMMDDYYYY}$/ =~ item or
55
+ /^#{RegTokens::GreaterThanDateFormatYYYYMMDD}$/ =~ item or
56
+ /^#{RegTokens::GreaterThanDatabaseFormat}$/ =~ item
57
+ conditions_tree << [item, :greater_than_date]
58
+ elsif /^#{RegTokens::LessThanDateFormatMMDDYYYY}$/ =~ item or
59
+ /^#{RegTokens::LessThanDateFormatYYYYMMDD}$/ =~ item or
60
+ /^#{RegTokens::LessThanDatabaseFormat}$/ =~ item
61
+ conditions_tree << [item, :less_than_date]
62
+ elsif /^#{RegTokens::DateFormatMMDDYYYY}$/ =~ item or
63
+ /^#{RegTokens::DateFormatYYYYMMDD}$/ =~ item or
64
+ /^#{RegTokens::DatabaseFormat}$/ =~ item
65
+ conditions_tree << [item, :as_of_date]
66
+ else
67
+ conditions_tree << (negate ? [item, :not] : [item, :like])
68
+ negate = false
69
+ end
70
+ end
71
+ end
72
+
73
+ return conditions_tree
74
+ end
75
+
76
+ # Tokenize the query based on the different RegTokens.
77
+ def tokenize(query)
78
+ pattern = [RegTokens::BetweenDateFormatMMDDYYYY,
79
+ RegTokens::BetweenDateFormatYYYYMMDD,
80
+ RegTokens::BetweenDatabaseFormat,
81
+ RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY,
82
+ RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD,
83
+ RegTokens::GreaterThanOrEqualToDatabaseFormat,
84
+ RegTokens::LessThanOrEqualToDateFormatMMDDYYYY,
85
+ RegTokens::LessThanOrEqualToDateFormatYYYYMMDD,
86
+ RegTokens::LessThanOrEqualToDatabaseFormat,
87
+ RegTokens::GreaterThanDateFormatMMDDYYYY,
88
+ RegTokens::GreaterThanDateFormatYYYYMMDD,
89
+ RegTokens::GreaterThanDatabaseFormat,
90
+ RegTokens::LessThanDateFormatMMDDYYYY,
91
+ RegTokens::LessThanDateFormatYYYYMMDD,
92
+ RegTokens::LessThanDatabaseFormat,
93
+ RegTokens::DateFormatMMDDYYYY,
94
+ RegTokens::DateFormatYYYYMMDD,
95
+ RegTokens::DatabaseFormat,
96
+ RegTokens::WordOrWord,
97
+ RegTokens::WordOrString,
98
+ RegTokens::StringOrWord,
99
+ RegTokens::StringOrString,
100
+ RegTokens::PossiblyNegatedWord,
101
+ RegTokens::PossiblyNegatedString]
102
+ pattern = Regexp.new(pattern.join('|'))
103
+
104
+ tokens = []
105
+ matches = query.scan(pattern).flatten.compact
106
+ matches.each { |match|
107
+ tokens << :not if match.index('-') == 0
108
+ # Remove any escaped quotes
109
+ # Remove any dashes preceded by a space or at the beginning of a token
110
+ # Remove any additional spaces - more that one.
111
+ cleaned_token = match.gsub(/"/,'').gsub(/^-| -/,'').gsub(/[ ]{2,}/, ' ')
112
+ tokens << cleaned_token if cleaned_token.length > 0
113
+ }
114
+ return tokens
115
+ end
116
+ end
117
+ end