gbdev-scoped_search 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/TODO CHANGED
@@ -5,15 +5,12 @@ Contact willem AT vanbergen DOT org if you want to help out
5
5
 
6
6
  New features:
7
7
  - Search fields of associations as well
8
- - Allow other search operators than %LIKE%
8
+ - Allow smart searches. Example: "90 days ago", "Yesterday", "10 days from now"
9
9
 
10
10
  Refactoring:
11
- - Create a separate class to build the actual SQL queries.
12
11
  - For searchable_on(*fields) make it so that instead of fields it accepts options (a hash) where
13
12
  :only and :except can be values. That way it is possible for all fields to be loaded except
14
13
  the ones specified with :except.
15
- - Add checks for field types because the latest version of PostgreSQL (version 8.3.3) is more
16
- strict about searching for strings in columns that are not string types.
17
14
 
18
15
  Documentation & testing:
19
16
  - Put something useful in the wiki
@@ -3,7 +3,9 @@ module ScopedSearch
3
3
  module ClassMethods
4
4
 
5
5
  def self.extended(base)
6
+ require 'scoped_search/reg_tokens'
6
7
  require 'scoped_search/query_language_parser'
8
+ require 'scoped_search/query_conditions_builder'
7
9
  end
8
10
 
9
11
  # Creates a named scope in the class it was called upon
@@ -16,49 +18,19 @@ module ScopedSearch
16
18
  # Build a hash that is used for the named_scope search_for.
17
19
  # This function will split the search_string into keywords, and search for all the keywords
18
20
  # in the fields that were provided to searchable_on
19
- def build_scoped_search_conditions(search_string)
21
+ def build_scoped_search_conditions(search_string)
20
22
  if search_string.nil? || search_string.strip.blank?
21
- return { :conditions => nil }
22
- else
23
- conditions = []
24
- query_params = {}
25
-
26
- QueryLanguageParser.parse(search_string).each_with_index do |search_condition, index|
27
- keyword_name = "keyword_#{index}".to_sym
28
- query_params[keyword_name] = "%#{search_condition.first}%"
29
-
30
- # a keyword may be found in any of the provided fields, so join the conitions with OR
31
- if search_condition.length == 2 && search_condition.last == :not
32
- keyword_conditions = self.scoped_search_fields.map do |field|
33
- field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
34
- "(#{field_name} NOT LIKE :#{keyword_name.to_s} OR #{field_name} IS NULL)"
35
- end
36
- conditions << "(#{keyword_conditions.join(' AND ')})"
37
- elsif search_condition.length == 2 && search_condition.last == :or
38
- word1, word2 = query_params[keyword_name].split(' OR ')
39
-
40
- query_params.delete(keyword_name)
41
- keyword_name_a = "#{keyword_name.to_s}a".to_sym
42
- keyword_name_b = "#{keyword_name.to_s}b".to_sym
43
- query_params[keyword_name_a] = word1
44
- query_params[keyword_name_b] = word2
45
-
46
- keyword_conditions = self.scoped_search_fields.map do |field|
47
- field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
48
- "(#{field_name} LIKE :#{keyword_name_a.to_s} OR #{field_name} LIKE :#{keyword_name_b.to_s})"
49
- end
50
- conditions << "(#{keyword_conditions.join(' OR ')})"
51
- else
52
- keyword_conditions = self.scoped_search_fields.map do |field|
53
- field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
54
- "#{field_name} LIKE :#{keyword_name.to_s}"
55
- end
56
- conditions << "(#{keyword_conditions.join(' OR ')})"
57
- end
23
+ return {:conditions => nil}
24
+ else
25
+ query_fields = {}
26
+ self.scoped_search_fields.each do |field|
27
+ field_name = connection.quote_table_name(table_name) + "." + connection.quote_column_name(field)
28
+ query_fields[field_name] = self.columns_hash[field.to_s].type
58
29
  end
59
-
60
- # all keywords must be matched, so join the conditions with AND
61
- return { :conditions => [conditions.join(' AND '), query_params] }
30
+
31
+ search_conditions = QueryLanguageParser.parse(search_string)
32
+ conditions = QueryConditionsBuilder.build_query(search_conditions, query_fields)
33
+ return {:conditions => conditions}
62
34
  end
63
35
  end
64
36
  end
@@ -23,38 +23,65 @@ module ScopedSearch
23
23
  else
24
24
  if /^.+[ ]OR[ ].+$/ =~ item
25
25
  conditions_tree << [item, :or]
26
+ elsif /^#{RegTokens::BetweenDateFormatMMDDYYYY}$/ =~ item or
27
+ /^#{RegTokens::BetweenDateFormatYYYYMMDD}$/ =~ item or
28
+ /^#{RegTokens::BetweenDatabaseFormat}$/ =~ item
29
+ conditions_tree << [item, :between_dates]
30
+ elsif /^#{RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
31
+ /^#{RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
32
+ /^#{RegTokens::GreaterThanOrEqualToDatabaseFormat}$/ =~ item
33
+ conditions_tree << [item, :greater_than_or_equal_to_date]
34
+ elsif /^#{RegTokens::LessThanOrEqualToDateFormatMMDDYYYY}$/ =~ item or
35
+ /^#{RegTokens::LessThanOrEqualToDateFormatYYYYMMDD}$/ =~ item or
36
+ /^#{RegTokens::LessThanOrEqualToDatabaseFormat}$/ =~ item
37
+ conditions_tree << [item, :less_than_or_equal_to_date]
38
+ elsif /^#{RegTokens::GreaterThanDateFormatMMDDYYYY}$/ =~ item or
39
+ /^#{RegTokens::GreaterThanDateFormatYYYYMMDD}$/ =~ item or
40
+ /^#{RegTokens::GreaterThanDatabaseFormat}$/ =~ item
41
+ conditions_tree << [item, :greater_than_date]
42
+ elsif /^#{RegTokens::LessThanDateFormatMMDDYYYY}$/ =~ item or
43
+ /^#{RegTokens::LessThanDateFormatYYYYMMDD}$/ =~ item or
44
+ /^#{RegTokens::LessThanDatabaseFormat}$/ =~ item
45
+ conditions_tree << [item, :less_than_date]
46
+ elsif /^#{RegTokens::DateFormatMMDDYYYY}$/ =~ item or
47
+ /^#{RegTokens::DateFormatYYYYMMDD}$/ =~ item or
48
+ /^#{RegTokens::DatabaseFormat}$/ =~ item
49
+ conditions_tree << [item, :as_of_date]
26
50
  else
27
51
  conditions_tree << (negate ? [item, :not] : [item, :like])
28
52
  negate = false
29
53
  end
30
54
  end
31
55
  end
56
+
32
57
  return conditions_tree
33
58
  end
34
-
35
- # **Patterns**
36
- # Each pattern is sperated by a "|". With regular expressions the order of the expression does matter.
37
- #
38
- # ([\w]+[ ]OR[ ][\w]+)
39
- # ([\w]+[ ]OR[ ]["][\w ]+["])
40
- # (["][\w ]+["][ ]OR[ ][\w]+)
41
- # (["][\w ]+["][ ]OR[ ]["][\w ]+["])
42
- # Any two combinations of letters, numbers and underscores that are seperated by " OR " (a single space must
43
- # be on each side of the "OR").
44
- # THESE COULD BE COMBINED BUT BECAUSE OF THE WAY PARSING WORKS THIS IS NOT DONE ON PURPOSE!!
45
- #
46
- # ([-]?[\w]+)
47
- # Any combination of letters, numbers and underscores that may or may not have a dash in front.
48
- #
49
- # ([-]?["][\w ]+["])
50
- # Any combination of letters, numbers, underscores and spaces within double quotes that may or may not have a dash in front.
59
+
51
60
  def tokenize(query)
52
- pattern = ['([\w]+[ ]OR[ ][\w]+)',
53
- '([\w]+[ ]OR[ ]["][\w ]+["])',
54
- '(["][\w ]+["][ ]OR[ ][\w]+)',
55
- '(["][\w ]+["][ ]OR[ ]["][\w ]+["])',
56
- '([-]?[\w]+)',
57
- '([-]?["][\w ]+["])']
61
+ pattern = [RegTokens::BetweenDateFormatMMDDYYYY,
62
+ RegTokens::BetweenDateFormatYYYYMMDD,
63
+ RegTokens::BetweenDatabaseFormat,
64
+ RegTokens::GreaterThanOrEqualToDateFormatMMDDYYYY,
65
+ RegTokens::GreaterThanOrEqualToDateFormatYYYYMMDD,
66
+ RegTokens::GreaterThanOrEqualToDatabaseFormat,
67
+ RegTokens::LessThanOrEqualToDateFormatMMDDYYYY,
68
+ RegTokens::LessThanOrEqualToDateFormatYYYYMMDD,
69
+ RegTokens::LessThanOrEqualToDatabaseFormat,
70
+ RegTokens::GreaterThanDateFormatMMDDYYYY,
71
+ RegTokens::GreaterThanDateFormatYYYYMMDD,
72
+ RegTokens::GreaterThanDatabaseFormat,
73
+ RegTokens::LessThanDateFormatMMDDYYYY,
74
+ RegTokens::LessThanDateFormatYYYYMMDD,
75
+ RegTokens::LessThanDatabaseFormat,
76
+ RegTokens::DateFormatMMDDYYYY,
77
+ RegTokens::DateFormatYYYYMMDD,
78
+ RegTokens::DatabaseFormat,
79
+ RegTokens::WordOrWord,
80
+ RegTokens::WordOrString,
81
+ RegTokens::StringOrWord,
82
+ RegTokens::StringOrString,
83
+ RegTokens::PossiblyNegatedWord,
84
+ RegTokens::PossiblyNegatedString]
58
85
  pattern = Regexp.new(pattern.join('|'))
59
86
 
60
87
  tokens = []
@@ -51,7 +51,7 @@ class QueryLanguageTest < Test::Unit::TestCase
51
51
  assert_equal 2, parsed.length
52
52
  assert_equal 'hallo', parsed[0].first
53
53
  assert_equal 'willem', parsed[1].first
54
-
54
+
55
55
  parsed = parse_query(' "hallo wi"llem"')
56
56
  assert_equal 2, parsed.length
57
57
  assert_equal 'hallo wi', parsed[0].first
@@ -108,6 +108,28 @@ class QueryLanguageTest < Test::Unit::TestCase
108
108
  assert_equal :or, parsed[0][1]
109
109
  end
110
110
 
111
+ def test_as_of_date
112
+ # parsed = parse_query('9/27/1980')
113
+ # assert_equal 1, parsed.length
114
+ # assert_equal '9/27/1980', parsed[0][0]
115
+ # assert_equal :as_of, parsed[0][1]
116
+
117
+ # parsed = parse_query('"Man made" OR Dogs')
118
+ # assert_equal 1, parsed.length
119
+ # assert_equal 'Man made OR Dogs', parsed[0][0]
120
+ # assert_equal :or, parsed[0][1]
121
+ #
122
+ # parsed = parse_query('Cows OR "Frog Toys"')
123
+ # assert_equal 1, parsed.length
124
+ # assert_equal 'Cows OR Frog Toys', parsed[0][0]
125
+ # assert_equal :or, parsed[0][1]
126
+ #
127
+ # parsed = parse_query('"Happy cow" OR "Sad Frog"')
128
+ # assert_equal 1, parsed.length
129
+ # assert_equal 'Happy cow OR Sad Frog', parsed[0][0]
130
+ # assert_equal :or, parsed[0][1]
131
+ end
132
+
111
133
  def test_long_string
112
134
  str = 'Wes -Hays "Hello World" -"Goodnight Moon" Bob OR Wes "Happy cow" OR "Sad Frog" "Man made" OR Dogs Cows OR "Frog Toys"'
113
135
  parsed = parse_query(str)
@@ -11,31 +11,55 @@ class ScopedSearchTest < Test::Unit::TestCase
11
11
  teardown_db
12
12
  end
13
13
 
14
- def test_enabling
15
- assert !SearchTestModel.respond_to?(:search_for)
16
- SearchTestModel.searchable_on :string_field, :text_field
17
- assert SearchTestModel.respond_to?(:search_for)
18
-
19
- assert_equal ActiveRecord::NamedScope::Scope, SearchTestModel.search_for('test').class
20
-
21
- end
22
-
14
+ # def test_enabling
15
+ # assert !SearchTestModel.respond_to?(:search_for)
16
+ # SearchTestModel.searchable_on :string_field, :text_field, :date_field
17
+ # assert SearchTestModel.respond_to?(:search_for)
18
+ #
19
+ # assert_equal ActiveRecord::NamedScope::Scope, SearchTestModel.search_for('test').class
20
+ # end
21
+ #
23
22
  def test_search
24
- SearchTestModel.searchable_on :string_field, :text_field
23
+ SearchTestModel.searchable_on :string_field, :text_field, :date_field
25
24
 
26
- assert_equal 15, SearchTestModel.search_for('').count
25
+ assert_equal 16, SearchTestModel.search_for('').count
27
26
  assert_equal 0, SearchTestModel.search_for('456').count
28
27
  assert_equal 2, SearchTestModel.search_for('hays').count
29
28
  assert_equal 1, SearchTestModel.search_for('hay ob').count
30
- assert_equal 13, SearchTestModel.search_for('o').count
29
+ assert_equal 14, SearchTestModel.search_for('o').count
31
30
  assert_equal 2, SearchTestModel.search_for('-o').count
32
- assert_equal 13, SearchTestModel.search_for('-Jim').count
31
+ assert_equal 14, SearchTestModel.search_for('-Jim').count
33
32
  assert_equal 1, SearchTestModel.search_for('Jim -Bush').count
34
33
  assert_equal 1, SearchTestModel.search_for('"Hello World" -"Goodnight Moon"').count
35
34
  assert_equal 2, SearchTestModel.search_for('Wes OR Bob').count
36
35
  assert_equal 3, SearchTestModel.search_for('"Happy cow" OR "Sad Frog"').count
37
36
  assert_equal 3, SearchTestModel.search_for('"Man made" OR Dogs').count
38
37
  assert_equal 2, SearchTestModel.search_for('Cows OR "Frog Toys"').count
38
+
39
+ # ** DATES **
40
+ #
41
+ # The next two dates are invalid therefore it will be ignored.
42
+ # Thus it would be the same as searching for an empty string
43
+ assert_equal 16, SearchTestModel.search_for('2/30/1980').count
44
+ assert_equal 16, SearchTestModel.search_for('99/99/9999').count
45
+
46
+ assert_equal 1, SearchTestModel.search_for('9/27/1980').count
47
+ assert_equal 1, SearchTestModel.search_for('hays 9/27/1980').count
48
+ assert_equal 2, SearchTestModel.search_for('hays 2/30/1980').count
49
+ assert_equal 1, SearchTestModel.search_for('2006/07/15').count
50
+
51
+ assert_equal 1, SearchTestModel.search_for('< 12/01/1980').count
52
+ assert_equal 5, SearchTestModel.search_for('> 1/1/2006').count
53
+
54
+ assert_equal 5, SearchTestModel.search_for('< 12/26/2002').count
55
+ assert_equal 6, SearchTestModel.search_for('<= 12/26/2002').count
56
+
57
+ assert_equal 5, SearchTestModel.search_for('> 2/5/2005').count
58
+ assert_equal 6, SearchTestModel.search_for('>= 2/5/2005').count
59
+
60
+ assert_equal 3, SearchTestModel.search_for('1/1/2005 TO 1/1/2007').count
61
+
62
+ assert_equal 2, SearchTestModel.search_for('Happy 1/1/2005 TO 1/1/2007').count
39
63
  end
40
64
 
41
65
  end
@@ -1,6 +1,7 @@
1
1
  require 'test/unit'
2
2
  require 'rubygems'
3
3
  require 'active_record'
4
+ require 'ruby-debug'
4
5
 
5
6
  require "#{File.dirname(__FILE__)}/../lib/scoped_search"
6
7
 
@@ -11,6 +12,7 @@ def setup_db
11
12
  t.string :string_field
12
13
  t.text :text_field
13
14
  t.string :ignored_field
15
+ t.date :date_field
14
16
  t.timestamps
15
17
  end
16
18
  end
@@ -22,20 +24,21 @@ end
22
24
 
23
25
  class SearchTestModel < ActiveRecord::Base
24
26
  def self.create_corpus!
25
- create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456")
26
- create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a")
27
- create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b")
28
- create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c")
29
- create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d")
30
- create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e")
31
- create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f")
32
- create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g")
33
- create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h")
34
- create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i")
35
- create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j")
36
- create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k")
37
- create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l")
38
- create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m")
39
- create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n")
27
+ create!(:string_field => "Programmer 123", :text_field => nil, :ignored_field => "123456", :date_field => '2000-01-01')
28
+ create!(:string_field => "Jim", :text_field => "Henson", :ignored_field => "123456a", :date_field => '2001-04-15')
29
+ create!(:string_field => "Jim", :text_field => "Bush", :ignored_field => "123456b", :date_field => '2001-04-17')
30
+ create!(:string_field => "Wes", :text_field => "Hays", :ignored_field => "123456c", :date_field => '1980-09-27')
31
+ create!(:string_field => "Bob", :text_field => "Hays", :ignored_field => "123456d", :date_field => '2002-11-09')
32
+ create!(:string_field => "Dogs", :text_field => "Pit Bull", :ignored_field => "123456e", :date_field => '2002-12-26')
33
+ create!(:string_field => "Dogs", :text_field => "Eskimo", :ignored_field => "123456f", :date_field => '2003-03-19')
34
+ create!(:string_field => "Cows", :text_field => "Farms", :ignored_field => "123456g", :date_field => '2004-05-01')
35
+ create!(:string_field => "Hello World", :text_field => "Hello Moon", :ignored_field => "123456h", :date_field => '2004-07-11')
36
+ create!(:string_field => "Hello World", :text_field => "Goodnight Moon", :ignored_field => "123456i", :date_field => '2004-09-12')
37
+ create!(:string_field => "Happy Cow", :text_field => "Sad Cow", :ignored_field => "123456j", :date_field => '2005-02-05')
38
+ create!(:string_field => "Happy Frog", :text_field => "Sad Frog", :ignored_field => "123456k", :date_field => '2006-03-09')
39
+ create!(:string_field => "Excited Frog", :text_field => "Sad Frog", :ignored_field => "123456l", :date_field => '2006-07-15')
40
+ create!(:string_field => "Man made", :text_field => "Woman made", :ignored_field => "123456m", :date_field => '2007-06-13')
41
+ create!(:string_field => "Cat Toys", :text_field => "Frog Toys", :ignored_field => "123456n", :date_field => '2008-03-04')
42
+ create!(:string_field => "Happy Toys", :text_field => "Sad Toys", :ignored_field => "123456n", :date_field => '2008-05-12')
40
43
  end
41
44
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gbdev-scoped_search
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2008-09-13 00:00:00 -07:00
13
+ date: 2008-09-21 00:00:00 -07:00
14
14
  default_executable:
15
15
  dependencies: []
16
16