picky 4.0.0pre6 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/lib/picky/api/category/partial.rb +26 -0
  2. data/lib/picky/api/category/similarity.rb +26 -0
  3. data/lib/picky/api/category/weight.rb +26 -0
  4. data/lib/picky/api/search/boost.rb +28 -0
  5. data/lib/picky/api/source.rb +35 -0
  6. data/lib/picky/api/tokenizer/character_substituter.rb +22 -0
  7. data/lib/picky/api/tokenizer.rb +37 -0
  8. data/lib/picky/bundle.rb +1 -1
  9. data/lib/picky/bundle_realtime.rb +2 -2
  10. data/lib/picky/category.rb +12 -6
  11. data/lib/picky/category_indexing.rb +2 -8
  12. data/lib/picky/generators/similarity/double_metaphone.rb +1 -1
  13. data/lib/picky/generators/similarity/metaphone.rb +1 -1
  14. data/lib/picky/generators/similarity/none.rb +1 -1
  15. data/lib/picky/generators/similarity/soundex.rb +1 -1
  16. data/lib/picky/index_indexing.rb +5 -25
  17. data/lib/picky/loader.rb +15 -5
  18. data/lib/picky/query/allocation.rb +1 -1
  19. data/lib/picky/query/{weights.rb → boosts.rb} +17 -17
  20. data/lib/picky/query/combinations.rb +2 -2
  21. data/lib/picky/search.rb +17 -19
  22. data/lib/picky/tokenizer.rb +7 -4
  23. data/spec/lib/api/category/partial_spec.rb +49 -0
  24. data/spec/lib/api/category/similarity_spec.rb +50 -0
  25. data/spec/lib/api/category/weight_spec.rb +47 -0
  26. data/spec/lib/api/search/boost_spec.rb +44 -0
  27. data/spec/lib/api/source_spec.rb +68 -0
  28. data/spec/lib/api/tokenizer/character_substituter_spec.rb +34 -0
  29. data/spec/lib/api/tokenizer_spec.rb +42 -0
  30. data/spec/lib/category_indexed_spec.rb +2 -2
  31. data/spec/lib/category_indexing_spec.rb +11 -24
  32. data/spec/lib/category_spec.rb +48 -11
  33. data/spec/lib/generators/similarity/double_metaphone_spec.rb +1 -1
  34. data/spec/lib/generators/similarity/metaphone_spec.rb +1 -1
  35. data/spec/lib/generators/similarity/none_spec.rb +1 -1
  36. data/spec/lib/generators/similarity/soundex_spec.rb +1 -1
  37. data/spec/lib/index_indexing_spec.rb +10 -14
  38. data/spec/lib/index_spec.rb +1 -1
  39. data/spec/lib/query/allocation_spec.rb +2 -2
  40. data/spec/lib/query/boosts_spec.rb +79 -0
  41. data/spec/lib/query/combinations_spec.rb +3 -3
  42. data/spec/lib/search_spec.rb +13 -13
  43. data/spec/lib/tokenizer_spec.rb +12 -8
  44. metadata +44 -23
  45. data/spec/lib/query/weights_spec.rb +0 -81
@@ -0,0 +1,26 @@
1
+ module Picky
2
+ module API
3
+ module Category
4
+
5
+ module Partial
6
+
7
+ def extract_partial thing
8
+ return Generators::Partial::Default unless thing
9
+
10
+ if thing.respond_to? :each_partial
11
+ thing
12
+ else
13
+ raise <<-ERROR
14
+ partial options for #{index_name}:#{name} should be either
15
+ * for example a Partial::Substring.new(from: m, to: n), Partial::Postfix.new(from: n), Partial::Infix.new(min: m, max: n) etc.
16
+ or
17
+ * an object that responds to #each_partial(str_or_sym) and yields each partial
18
+ ERROR
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Picky
2
+ module API
3
+ module Category
4
+
5
+ module Similarity
6
+
7
+ def extract_similarity thing
8
+ return Generators::Similarity::Default unless thing
9
+
10
+ if thing.respond_to?(:encode) && thing.respond_to?(:prioritize)
11
+ thing
12
+ else
13
+ raise <<-ERROR
14
+ similarity options for #{index_name}:#{name} should be either
15
+ * for example a Similarity::Phonetic.new(n), Similarity::Metaphone.new(n), Similarity::DoubleMetaphone.new(n) etc.
16
+ or
17
+ * an object that responds to #encode(text) => encoded_text and #prioritize(array_of_encoded, encoded)
18
+ ERROR
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Picky
2
+ module API
3
+ module Category
4
+
5
+ module Weight
6
+
7
+ def extract_weight thing
8
+ return Generators::Weights::Default unless thing
9
+
10
+ if thing.respond_to? :weight_for
11
+ thing
12
+ else
13
+ raise <<-ERROR
14
+ weight options for #{index_name}:#{name} should be either
15
+ * for example a Weights::Logarithmic.new, Weights::Constant.new(int = 0), Weights::Dynamic.new(&block) etc.
16
+ or
17
+ * an object that responds to #weight_for(amount_of_ids_for_token) => float
18
+ ERROR
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ module Picky
2
+ module API
3
+ module Search
4
+
5
+ module Boost
6
+
7
+ def extract_boosts thing
8
+ if thing.respond_to?(:boost_for)
9
+ thing
10
+ else
11
+ if thing.respond_to?(:[])
12
+ Query::Boosts.new thing
13
+ else
14
+ raise <<-ERROR
15
+ boost options for a Search should be either
16
+ * for example a Hash { [:name, :surname] => +3 }
17
+ or
18
+ * an object that responds to #boost_for(combinations) and returns a boost float
19
+ ERROR
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ module Picky
2
+ module API
3
+ module Source
4
+
5
+ def extract_source thing, options = {}
6
+ if thing.respond_to?(:each) || thing.respond_to?(:call)
7
+ thing
8
+ else
9
+ return if options[:nil_ok]
10
+ if respond_to? :name
11
+ if @index
12
+ location = " #{@index.name}:#{name}"
13
+ else
14
+ location = " #{name}"
15
+ end
16
+ else
17
+ location = ''
18
+ end
19
+ raise ArgumentError.new(<<-ERROR)
20
+ The#{location} source should respond to either the method #each or
21
+ it can be a lambda/block, returning such a source.
22
+ ERROR
23
+ end
24
+ end
25
+
26
+ # Get the actual source if it is wrapped in a time
27
+ # capsule, i.e. a block/lambda.
28
+ #
29
+ def unblock_source
30
+ @source.respond_to?(:call) ? @source.call : @source
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,22 @@
1
+ module Picky
2
+ module API
3
+ module Tokenizer
4
+
5
+ module CharacterSubstituter
6
+
7
+ def extract_character_substituter thing
8
+ if thing.respond_to? :substitute
9
+ thing
10
+ else
11
+ raise ArgumentError.new <<-ERROR
12
+ The substitutes_characters_with option needs a character substituter,
13
+ which responds to #substitute(text) and returns substituted_text."
14
+ ERROR
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ module Picky
2
+ module API
3
+
4
+ module Tokenizer
5
+
6
+ def extract_tokenizer thing
7
+ return unless thing
8
+ if thing.respond_to? :tokenize
9
+ thing
10
+ else
11
+ if thing.respond_to? :[]
12
+ Picky::Tokenizer.new thing
13
+ else
14
+ if respond_to? :name
15
+ location = ' for '
16
+ if @index
17
+ location += "#{@index.name}:#{name}"
18
+ else
19
+ location += "#{name}"
20
+ end
21
+ else
22
+ location = ''
23
+ end
24
+ raise <<-ERROR
25
+ indexing options#{location} should be either
26
+ * a Hash
27
+ or
28
+ * an object that responds to #tokenize(text) => [[token1, ...], [original1, ...]]
29
+ ERROR
30
+ end
31
+ end
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
data/lib/picky/bundle.rb CHANGED
@@ -126,7 +126,7 @@ module Picky
126
126
  # Note: Does not return itself.
127
127
  #
128
128
  def similar text
129
- code = similarity_strategy.encoded text
129
+ code = similarity_strategy.encode text
130
130
  return [] unless code
131
131
  similar_codes = @similarity[code]
132
132
  if similar_codes.blank?
@@ -22,7 +22,7 @@ module Picky
22
22
  # Since no element uses this sym anymore, we can delete the similarity for it.
23
23
  # TODO Not really. Since multiple syms can point to the same encoded.
24
24
  #
25
- @similarity.delete self.similarity_strategy.encoded(str_or_sym)
25
+ @similarity.delete self.similarity_strategy.encode(str_or_sym)
26
26
  else
27
27
  @weights[str_or_sym] = self.weight_strategy.weight_for ids.size
28
28
  end
@@ -67,7 +67,7 @@ module Picky
67
67
  # Add string/symbol to similarity index.
68
68
  #
69
69
  def add_similarity str_or_sym, where = :unshift
70
- if encoded = self.similarity_strategy.encoded(str_or_sym)
70
+ if encoded = self.similarity_strategy.encode(str_or_sym)
71
71
  similars = @similarity[encoded] ||= []
72
72
 
73
73
  # Not completely correct, as others will also be affected, but meh.
@@ -2,6 +2,12 @@ module Picky
2
2
 
3
3
  class Category
4
4
 
5
+ include API::Tokenizer
6
+ include API::Source
7
+ include API::Category::Weight
8
+ include API::Category::Partial
9
+ include API::Category::Similarity
10
+
5
11
  attr_accessor :exact,
6
12
  :partial
7
13
  attr_reader :name,
@@ -33,9 +39,9 @@ module Picky
33
39
 
34
40
  # Indexing.
35
41
  #
36
- @source = options[:source]
37
- @from = options[:from]
38
- @tokenizer = options[:tokenizer]
42
+ @source = extract_source options[:source], nil_ok: true
43
+ @from = options[:from]
44
+ @tokenizer = extract_tokenizer options[:indexing]
39
45
 
40
46
  @key_format = options.delete :key_format
41
47
  @backend = options.delete :backend
@@ -44,9 +50,9 @@ module Picky
44
50
 
45
51
  # @symbols = options[:use_symbols] || index.use_symbols? # TODO Symbols.
46
52
 
47
- weights = options[:weight] || Generators::Weights::Default
48
- partial = options[:partial] || Generators::Partial::Default
49
- similarity = options[:similarity] || Generators::Similarity::Default
53
+ weights = extract_weight options[:weight]
54
+ partial = extract_partial options[:partial]
55
+ similarity = extract_similarity options[:similarity]
50
56
 
51
57
  no_partial = Generators::Partial::None.new
52
58
  no_similarity = Generators::Similarity::None.new
@@ -78,13 +78,7 @@ module Picky
78
78
  # If we have no explicit source, we'll check the index for one.
79
79
  #
80
80
  def source
81
- extract_source || @index.source
82
- end
83
- # Extract the actual source if it is wrapped in a time
84
- # capsule, i.e. a block/lambda.
85
- #
86
- def extract_source
87
- @source = @source.respond_to?(:call) ? @source.call : @source
81
+ (@source = extract_source(@source, nil_ok: true)) || @index.source
88
82
  end
89
83
 
90
84
  # Return the key format.
@@ -96,7 +90,7 @@ module Picky
96
90
  # Default is to_i.
97
91
  #
98
92
  def key_format
99
- @key_format ||= source.respond_to?(:key_format) && source.key_format || @index.key_format || :to_i
93
+ @key_format ||= @index.key_format || :to_i
100
94
  end
101
95
 
102
96
  # Where the data is taken from.
@@ -18,7 +18,7 @@ module Picky
18
18
  #
19
19
  # Returns a symbol.
20
20
  #
21
- def encoded str_or_sym
21
+ def encode str_or_sym
22
22
  str_or_sym.double_metaphone
23
23
  end
24
24
 
@@ -18,7 +18,7 @@ module Picky
18
18
  #
19
19
  # Returns a symbol.
20
20
  #
21
- def encoded str_or_sym
21
+ def encode str_or_sym
22
22
  str_or_sym.metaphone
23
23
  end
24
24
 
@@ -10,7 +10,7 @@ module Picky
10
10
 
11
11
  # Does not encode text. Just returns nil.
12
12
  #
13
- def encoded text
13
+ def encode text
14
14
  nil
15
15
  end
16
16
 
@@ -18,7 +18,7 @@ module Picky
18
18
  #
19
19
  # Returns a symbol.
20
20
  #
21
- def encoded str_or_sym
21
+ def encode str_or_sym
22
22
  str_or_sym.soundex
23
23
  end
24
24
 
@@ -4,6 +4,9 @@ module Picky
4
4
  #
5
5
  class Index
6
6
 
7
+ include API::Tokenizer
8
+ include API::Source
9
+
7
10
  include Helpers::Indexing
8
11
 
9
12
  # Delegators for indexing.
@@ -17,11 +20,7 @@ module Picky
17
20
  # Parameters are the exact same as for indexing.
18
21
  #
19
22
  def indexing options = {}
20
- @tokenizer = if options.respond_to?(:tokenize)
21
- options
22
- else
23
- options && Tokenizer.new(options)
24
- end
23
+ @tokenizer = extract_tokenizer options
25
24
  end
26
25
 
27
26
  #
@@ -99,26 +98,7 @@ module Picky
99
98
  #
100
99
  def source some_source = nil, &block
101
100
  some_source ||= block
102
- some_source ? (check_source(some_source); @source = some_source) : (@source && extract_source)
103
- end
104
- # Extract the actual source if it is wrapped in a time
105
- # capsule, i.e. a block/lambda.
106
- #
107
- def extract_source
108
- @source.respond_to?(:call) ? @source.call : @source
109
- end
110
- def check_source source # :nodoc:
111
- raise ArgumentError.new(<<-SOURCE
112
-
113
-
114
- The index "#{name}" should use a data source that responds to either the method #each, or the method #harvest, which yields(id, text), OR it can be a lambda/block, returning such a source.
115
- Or it could use one of the built-in sources:
116
- Sources::#{(Sources.constants - [:Base, :Wrappers, :NoCSVFileGiven, :NoCouchDBGiven]).join(',
117
- Sources::')}
118
-
119
-
120
- SOURCE
121
- ) unless source.respond_to?(:each) || source.respond_to?(:harvest) || source.respond_to?(:call)
101
+ some_source ? (@source = extract_source(some_source)) : unblock_source
122
102
  end
123
103
 
124
104
  # Define a key_format on the index.
data/lib/picky/loader.rb CHANGED
@@ -165,10 +165,6 @@ module Picky
165
165
  load_relative 'query/token'
166
166
  load_relative 'query/tokens'
167
167
 
168
- # Tokenizer.
169
- #
170
- load_relative 'tokenizer'
171
-
172
168
  # Query combinations, qualifiers, weigher.
173
169
  #
174
170
  load_relative 'query/combination'
@@ -179,7 +175,7 @@ module Picky
179
175
 
180
176
  load_relative 'query/qualifier_category_mapper'
181
177
 
182
- load_relative 'query/weights'
178
+ load_relative 'query/boosts'
183
179
 
184
180
  load_relative 'query/indexes'
185
181
  load_relative 'query/indexes_check'
@@ -187,6 +183,20 @@ module Picky
187
183
  # Loads the user interface parts.
188
184
  #
189
185
  def load_user_interface
186
+ # Load API parts.
187
+ #
188
+ load_relative 'api/tokenizer'
189
+ load_relative 'api/tokenizer/character_substituter'
190
+ load_relative 'api/source'
191
+ load_relative 'api/category/weight'
192
+ load_relative 'api/category/partial'
193
+ load_relative 'api/category/similarity'
194
+ load_relative 'api/search/boost'
195
+
196
+ # Tokenizer.
197
+ #
198
+ load_relative 'tokenizer'
199
+
190
200
  # Load harakiri.
191
201
  #
192
202
  load_relative 'rack/harakiri'
@@ -40,7 +40,7 @@ module Picky
40
40
  0
41
41
  else
42
42
  @backend.weight(@combinations) +
43
- @combinations.weighted_score(weights)
43
+ @combinations.boost_for(weights)
44
44
  end
45
45
  end
46
46
 
@@ -2,7 +2,7 @@ module Picky
2
2
 
3
3
  module Query
4
4
 
5
- # Calculates scores/weights for combinations.
5
+ # Calculates boosts for combinations.
6
6
  #
7
7
  # Example:
8
8
  # Someone searches for peter fish.
@@ -11,34 +11,34 @@ module Picky
11
11
  # and
12
12
  # [:name, :surname]
13
13
  #
14
- # This class is concerned with calculating scores
14
+ # This class is concerned with calculating boosts
15
15
  # for the category combinations.
16
16
  #
17
17
  # Implement either
18
- # #score_for(combinations)
18
+ # #boost_for(combinations)
19
19
  # or
20
- # #weight_for(category_names) # Subclass this class for this.
20
+ # #boost_for_categories(category_names) # Subclass this class for this.
21
21
  #
22
- # And return a weight.
22
+ # And return a boost (float).
23
23
  #
24
- class Weights
24
+ class Boosts
25
25
 
26
- attr_reader :weights
26
+ attr_reader :boosts
27
27
 
28
28
  delegate :empty?,
29
- :to => :weights
29
+ :to => :boosts
30
30
 
31
31
  # Needs a Hash of
32
32
  # [:category_name1, :category_name2] => +3
33
33
  # (some positive or negative weight)
34
34
  #
35
- def initialize weights = {}
36
- @weights = weights
35
+ def initialize boosts = {}
36
+ @boosts = boosts
37
37
  end
38
38
 
39
39
  # API.
40
40
  #
41
- # Get the weight for an array of category names.
41
+ # Get the boost for an array of category names.
42
42
  #
43
43
  # Example:
44
44
  # [:name, :height, :color] returns +3, but
@@ -47,8 +47,8 @@ module Picky
47
47
  # Note: Use Array#clustered_uniq_fast to make
48
48
  # [:a, :a, :b, :a] => [:a, :b, :a]
49
49
  #
50
- def weight_for category_names
51
- @weights[category_names.clustered_uniq_fast] || 0
50
+ def boost_for_categories names
51
+ @boosts[names.clustered_uniq_fast] || 0
52
52
  end
53
53
 
54
54
  # API.
@@ -60,22 +60,22 @@ module Picky
60
60
  # Note: Cache this if more complicated weighings become necessary.
61
61
  # Note: Maybe make combinations comparable to Symbols?
62
62
  #
63
- def score_for combinations
64
- weight_for combinations.map(&:category_name)
63
+ def boost_for combinations
64
+ boost_for_categories combinations.map(&:category_name)
65
65
  end
66
66
 
67
67
  # A Weights instance is == to another if
68
68
  # the weights are the same.
69
69
  #
70
70
  def == other
71
- @weights == other.weights
71
+ @boosts == other.boosts
72
72
  end
73
73
 
74
74
  # Prints out a nice representation of the
75
75
  # configured weights.
76
76
  #
77
77
  def to_s
78
- "#{self.class}(#{@weights})"
78
+ "#{self.class}(#{@boosts})"
79
79
  end
80
80
 
81
81
  end
@@ -28,8 +28,8 @@ module Picky
28
28
  def score
29
29
  @combinations.sum &:weight
30
30
  end
31
- def weighted_score weights
32
- weights.score_for @combinations
31
+ def boost_for weights
32
+ weights.boost_for @combinations
33
33
  end
34
34
 
35
35
  # Filters the tokens and categories such that categories
data/lib/picky/search.rb CHANGED
@@ -15,11 +15,13 @@ module Picky
15
15
  #
16
16
  class Search
17
17
 
18
+ include API::Search::Boost
19
+
18
20
  include Helpers::Measuring
19
21
 
20
22
  attr_reader :indexes
21
23
  attr_accessor :tokenizer,
22
- :weights
24
+ :boosts
23
25
 
24
26
  delegate :ignore,
25
27
  :to => :indexes
@@ -27,11 +29,11 @@ module Picky
27
29
  # Takes:
28
30
  # * A number of indexes
29
31
  #
30
- # It is also possible to define the tokenizer and weights like so.
32
+ # It is also possible to define the tokenizer and boosts like so.
31
33
  # Example:
32
34
  # search = Search.new(index1, index2, index3) do
33
35
  # searching removes_characters: /[^a-z]/ # etc.
34
- # weights [:author, :title] => +3,
36
+ # boosts [:author, :title] => +3,
35
37
  # [:title, :isbn] => +1
36
38
  # end
37
39
  #
@@ -41,7 +43,7 @@ module Picky
41
43
  instance_eval(&Proc.new) if block_given?
42
44
 
43
45
  @tokenizer ||= Tokenizer.searching # THINK Not dynamic. Ok?
44
- @weights ||= Query::Weights.new
46
+ @boosts ||= Query::Boosts.new
45
47
  @ignore_unassigned = false if @ignore_unassigned.nil?
46
48
 
47
49
  self
@@ -87,7 +89,7 @@ module Picky
87
89
  # Note: When using the Picky interface, do not terminate too
88
90
  # early as this will kill off the allocation selections.
89
91
  # A value of
90
- # early_terminate 5
92
+ # terminate_early 5
91
93
  # is probably a good idea to show the user 5 extra
92
94
  # beyond the needed ones.
93
95
  #
@@ -117,29 +119,25 @@ module Picky
117
119
  #
118
120
  # or
119
121
  #
120
- # # Explicitly add a random number (0...1) to the weights.
122
+ # # Explicitly add a random number (0...1) to the boosts.
121
123
  # #
122
- # my_weights = Class.new do
124
+ # my_boosts = Class.new do
123
125
  # # Instance only needs to implement
124
- # # score_for combinations
126
+ # # boost_for combinations
125
127
  # # and return a number that is
126
- # # added to the weight.
128
+ # # added to the score.
127
129
  # #
128
- # def score_for combinations
130
+ # def boost_for combinations
129
131
  # rand
130
132
  # end
131
133
  # end.new
132
134
  #
133
135
  # search = Search.new(books_index, dvd_index, mp3_index) do
134
- # boost my_weights
136
+ # boost my_boosts
135
137
  # end
136
138
  #
137
- def boost weights
138
- @weights = if weights.respond_to?(:score_for)
139
- weights
140
- else
141
- Query::Weights.new weights
142
- end
139
+ def boost boosts
140
+ @boosts = extract_boosts boosts
143
141
  end
144
142
 
145
143
  # Ignore the given token if it cannot be matched to a category.
@@ -225,7 +223,7 @@ module Picky
225
223
  # Gets sorted allocations for the tokens.
226
224
  #
227
225
  def sorted_allocations tokens, amount = nil # :nodoc:
228
- indexes.prepared_allocations_for tokens, weights, amount
226
+ indexes.prepared_allocations_for tokens, boosts, amount
229
227
  end
230
228
 
231
229
  # Display some nice information for the user.
@@ -234,7 +232,7 @@ module Picky
234
232
  s = "#{self.class}("
235
233
  ary = []
236
234
  ary << @indexes.indexes.map(&:name).join(', ') unless @indexes.indexes.empty?
237
- ary << "weights: #{@weights}" if @weights
235
+ ary << "boosts: #{@boosts}" if @boosts
238
236
  s << ary.join(', ')
239
237
  s << ")"
240
238
  s
@@ -6,15 +6,19 @@ module Picky
6
6
  #
7
7
  class Tokenizer
8
8
 
9
+ extend API::Tokenizer
10
+
11
+ include API::Tokenizer::CharacterSubstituter
12
+
9
13
  def self.default_indexing_with options = {}
10
- @indexing = options.respond_to?(:tokenize) ? options : new(options)
14
+ @indexing = extract_tokenizer options
11
15
  end
12
16
  def self.indexing
13
17
  @indexing ||= new
14
18
  end
15
19
 
16
20
  def self.default_searching_with options = {}
17
- @searching = options.respond_to?(:tokenize) ? options : new(options)
21
+ @searching = extract_tokenizer options
18
22
  end
19
23
  def self.searching
20
24
  @searching ||= new
@@ -108,8 +112,7 @@ Case sensitive? #{@case_sensitive ? "Yes." : "-"}
108
112
  # Default is European Character substitution.
109
113
  #
110
114
  def substitutes_characters_with substituter = CharacterSubstituters::WestEuropean.new
111
- raise ArgumentError.new "The substitutes_characters_with option needs a character substituter, which responds to #substitute." unless substituter.respond_to?(:substitute)
112
- @substituter = substituter
115
+ @substituter = extract_character_substituter substituter
113
116
  end
114
117
  def substitute_characters text
115
118
  substituter?? substituter.substitute(text) : text