fuzzy_match 1.2.1 → 1.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,35 +1,44 @@
1
1
  # fuzzy_match
2
2
 
3
- Find a needle in a haystack based on string similarity (using the Pair Distance algorithm and Levenshtein distance) and regular expressions.
3
+ Find a needle in a haystack based on string similarity and regular expression rules.
4
4
 
5
5
  Replaces [`loose_tight_dictionary`](https://github.com/seamusabshere/loose_tight_dictionary) because that was a confusing name.
6
6
 
7
7
  ## Quickstart
8
8
 
9
9
  >> require 'fuzzy_match'
10
- => true
11
- >> FuzzyMatch.new(['seamus', 'andy', 'ben']).find('Shamus')
10
+ => true
11
+ >> matcher = FuzzyMatch.new(['seamus', 'andy', 'ben'])
12
+ => #<FuzzyMatch: [...]>
13
+ >> matcher.find('Shamus')
12
14
  => "seamus"
13
15
 
14
16
  ## Default matching (string similarity)
15
17
 
16
- If you configure nothing else, string similarity matching is used. That's why we call it fuzzy matching.
18
+ At the core, and even if you configure nothing else, string similarity (calculated by "pair distance" aka Dice's) is used to compare records.
17
19
 
18
- The algorithm is [Dice's Coefficient](http://en.wikipedia.org/wiki/Dice's_coefficient) (aka Pair Distance) because it seemed to work better than Jaro Winkler, etc.
20
+ You can tell `FuzzyMatch` what field or method to use via the `:read` option... for example, let's say you want to match a `Country` object like `#<Country name:"Uruguay" iso_3166_code:"UY">`
19
21
 
20
- ## Rules (regular expressions)
22
+ >> matcher = FuzzyMatch.new(Country.all, :read => :name) # Country#name will be called when comparing
23
+ => #<FuzzyMatch: [...]>
24
+ >> matcher.find('youruguay')
25
+ => #<Country name:"Uruguay" iso_3166_code:"UY"> # the matcher returns a Country object
21
26
 
22
- You can improve the default matchings with rules, which are generally regular expressions.
27
+ ## Optional rules (regular expressions)
23
28
 
24
- >> require 'fuzzy_match'
25
- => true
26
- >> matcher = FuzzyMatch.new(['Ford F-150', 'Ford F-250', 'GMC 1500', 'GMC 2500'], :blockings => [ /ford/i, /gmc/i ], :normalizers => [ /K(\d500)/i ], :identities => [ /(f)-?(\d\d\d)/i ])
29
+ You can improve the default matchings with rules. There are 4 different kinds of rules. Each rule is a regular expression. Depending on the kind of rule, the results of running the regular expression are used for a particular purpose.
30
+
31
+ We suggest that you **first try without any rules** and only define them to improve matching, prevent false positives, etc.
32
+
33
+ >> matcher = FuzzyMatch.new(['Ford F-150', 'Ford F-250', 'GMC 1500', 'GMC 2500'], :blockings => [ /ford/i, /gmc/i ], :normalizers => [ /K(\d500)/i ], :identities => [ /(f)-?(\d50)/i ])
27
34
  => #<FuzzyMatch: [...]>
28
35
  >> matcher.find('fordf250')
29
36
  => "Ford F-250"
30
37
  >> matcher.find('gmc truck k1500')
31
38
  => "GMC 1500"
32
39
 
40
+ For identities and normalizers (see below), **only the captures are used.** For example, `/(f)-?(\d50)/i` captures the "F" and the "250" but ignores the dash. So place your parentheses carefully! Blockings work the same way, except that if you don't have any captures, a simple match will pass.
41
+
33
42
  ### Blockings
34
43
 
35
44
  Group records together.
@@ -40,13 +49,13 @@ Setting a blocking of `/Airbus/` ensures that strings containing "Airbus" will o
40
49
 
41
50
  Strip strings down to the essentials.
42
51
 
43
- Adding a normalizer like `/(boeing).*(7\d\d)/i` will cause "BOEING COMPANY 747" and "boeing747" to be scored as if they were "BOEING 747" and "boeing 747", respectively. See also "Case sensitivity" below.
52
+ Adding a normalizer like `/(boeing).*(7\d\d)/i` will cause "BOEING COMPANY 747" and "boeing747" to be normalized to "BOEING 747" and "boeing 747", respectively. Since things are generally downcased before they are compared, these would be an exact match.
44
53
 
45
54
  ### Identities
46
55
 
47
56
  Prevent impossible matches.
48
57
 
49
- Adding an identity like `/(F)\-?(\d50)/` ensures that "Ford F-150" and "Ford F-250" never match.
58
+ Adding an identity like `/(f)-?(\d50)/i` ensures that "Ford F-150" and "Ford F-250" never match.
50
59
 
51
60
  ### Stop words
52
61
 
@@ -58,37 +67,48 @@ Adding a stop word like `THE` ensures that it is not taken into account when com
58
67
 
59
68
  * `read`: how to interpret each record in the 'haystack', either a Proc or a symbol
60
69
  * `must_match_blocking`: don't return a match unless the needle fits into one of the blockings you specified
61
- * `must_match_at_least_one_word`: don't return a match unless the needle shares at least one word with the match
70
+ * `must_match_at_least_one_word`: don't return a match unless the needle shares at least one word with the match. Note that "Foo's" is treated like one word (so that it won't match "'s") and "Bolivia," is treated as just "bolivia"
62
71
  * `first_blocking_decides`: force records into the first blocking they match, rather than choosing a blocking that will give them a higher score
63
72
  * `gather_last_result`: enable `last_result`
64
73
 
65
- ### `:read`
66
-
67
- So, what if your needle is a string like `youruguay` and your haystack is full of `Country` objects like `<Country name:"Uruguay">`?
68
-
69
- >> FuzzyMatch.new(Country.all, :read => :name).find('youruguay')
70
- => <Country name:"Uruguay">
71
-
72
74
  ## Case sensitivity
73
75
 
74
76
  String similarity is case-insensitive. Everything is downcased before scoring. This is a change from previous versions.
75
77
 
76
78
  Be careful when trying to use case-sensitivity in your rules; in general, things are downcased before comparing.
77
79
 
78
- ## Dice's coefficient edge case
80
+ ## String similarity algorithm
81
+
82
+ The algorithm is [Dice's Coefficient](http://en.wikipedia.org/wiki/Dice's_coefficient) (aka Pair Distance) because it seemed to work better than Longest Substring, Hamming, Jaro Winkler, Levenshtein (although see edge case below) etc.
83
+
84
+ Here's a great explanation copied from [the wikipedia entry](http://en.wikipedia.org/wiki/Dice%27s_coefficient):
85
+
86
+ to calculate the similarity between:
87
+
88
+ night
89
+ nacht
90
+
91
+ We would find the set of bigrams in each word:
92
+
93
+ {ni,ig,gh,ht}
94
+ {na,ac,ch,ht}
95
+
96
+ Each set has four elements, and the intersection of these two sets has only one element: ht.
97
+
98
+ Inserting these numbers into the formula, we calculate, s = (2 · 1) / (4 + 4) = 0.25.
99
+
100
+ ### Edge case: when Dice's fails, use Levenshtein
79
101
 
80
102
  In edge cases where Dice's finds that two strings are equally similar to a third string, then Levenshtein distance is used. For example, pair distance considers "RATZ" and "CATZ" to be equally similar to "RITZ" so we invoke Levenshtein.
81
103
 
82
- >> require 'amatch'
83
- => true
84
104
  >> 'RITZ'.pair_distance_similar 'RATZ'
85
105
  => 0.3333333333333333
86
- >> 'RITZ'.pair_distance_similar 'CATZ' # <-- pair distance can't tell the difference, so we fall back to levenshtein...
87
- => 0.3333333333333333
106
+ >> 'RITZ'.pair_distance_similar 'CATZ'
107
+ => 0.3333333333333333 # pair distance can't tell the difference, so we fall back to levenshtein...
88
108
  >> 'RITZ'.levenshtein_similar 'RATZ'
89
109
  => 0.75
90
- >> 'RITZ'.levenshtein_similar 'CATZ' # <-- which properly shows that RATZ should win
91
- => 0.5
110
+ >> 'RITZ'.levenshtein_similar 'CATZ'
111
+ => 0.5 # which properly shows that RATZ should win
92
112
 
93
113
  ## Production use
94
114
 
data/Rakefile CHANGED
@@ -11,8 +11,4 @@ end
11
11
  task :default => :test
12
12
 
13
13
  require 'yard'
14
- require File.expand_path('../lib/fuzzy_match/version.rb', __FILE__)
15
- YARD::Rake::YardocTask.new do |t|
16
- t.files = ['lib/**/*.rb', 'README.markdown'] # optional
17
- # t.options = ['--any', '--extra', '--opts'] # optional
18
- end
14
+ YARD::Rake::YardocTask.new
@@ -26,7 +26,9 @@ Gem::Specification.new do |s|
26
26
  s.add_development_dependency 'cohort_scope'
27
27
  s.add_development_dependency 'weighted_average'
28
28
  s.add_development_dependency 'rake'
29
- # s.add_development_dependency 'amatch'
29
+ s.add_development_dependency 'yard'
30
+ s.add_development_dependency 'amatch'
31
+
30
32
  s.add_runtime_dependency 'activesupport', '>=3'
31
33
  s.add_runtime_dependency 'to_regexp', '>=0.0.3'
32
34
  end
@@ -262,7 +262,7 @@ EOS
262
262
  last_result.explain
263
263
  end
264
264
 
265
- # DEPRECATED - doesn't do anything
265
+ # DEPRECATED
266
266
  def free
267
267
  end
268
268
  end
@@ -22,7 +22,7 @@ class FuzzyMatch
22
22
  end
23
23
 
24
24
  def inspect
25
- "#<Normalizer regexp=#{regexp.inspect}>"
25
+ "#<FuzzyMatch::Normalizer regexp=#{regexp.inspect}>"
26
26
  end
27
27
  end
28
28
  end
@@ -1,22 +1,19 @@
1
- begin
2
- require 'amatch'
3
- rescue ::LoadError
4
- # using native ruby similarity scoring
5
- end
6
-
7
1
  class FuzzyMatch
8
2
  class Score
9
- attr_reader :str1, :str2
3
+ extend ::ActiveSupport::Memoizable
4
+
5
+ attr_reader :str1
6
+ attr_reader :str2
10
7
 
11
8
  def initialize(str1, str2)
12
9
  @str1 = str1.downcase
13
10
  @str2 = str2.downcase
14
11
  end
15
-
12
+
16
13
  def inspect
17
- %{#<Score: dices_coefficient=#{dices_coefficient_similar} levenshtein=#{levenshtein_similar}>}
14
+ %{#<FuzzyMatch::Score: str1=#{str1.inspect} str2=#{str2.inspect} dices_coefficient_similar=#{dices_coefficient_similar} levenshtein_similar=#{levenshtein_similar}>}
18
15
  end
19
-
16
+
20
17
  def <=>(other)
21
18
  by_dices_coefficient = (dices_coefficient_similar <=> other.dices_coefficient_similar)
22
19
  if by_dices_coefficient == 0
@@ -25,23 +22,26 @@ class FuzzyMatch
25
22
  by_dices_coefficient
26
23
  end
27
24
  end
28
-
29
- def utf8?
30
- (defined?(::Encoding) ? str1.encoding.to_s : $KCODE).downcase.start_with?('u')
31
- end
32
-
25
+
33
26
  if defined?(::Amatch)
34
-
27
+
35
28
  def dices_coefficient_similar
29
+ if str1 == str2
30
+ return 1.0
31
+ elsif str1.length == 1 and str2.length == 1
32
+ return 0.0
33
+ end
36
34
  str1.pair_distance_similar str2
37
35
  end
38
-
36
+ memoize :dices_coefficient_similar
37
+
39
38
  def levenshtein_similar
40
39
  str1.levenshtein_similar str2
41
40
  end
42
-
41
+ memoize :levenshtein_similar
42
+
43
43
  else
44
-
44
+
45
45
  SPACE = ' '
46
46
  # http://stackoverflow.com/questions/653157/a-better-similarity-ranking-algorithm-for-variable-length-strings
47
47
  def dices_coefficient_similar
@@ -60,19 +60,26 @@ class FuzzyMatch
60
60
  end.reject do |pair|
61
61
  pair.include? SPACE
62
62
  end
63
- union = pairs1.size + pairs2.size
64
- intersection = 0
65
- pairs1.each do |p1|
66
- 0.upto(pairs2.size-1) do |i|
67
- if p1 == pairs2[i]
68
- intersection += 1
69
- pairs2.slice!(i)
70
- break
71
- end
72
- end
73
- end
63
+ union = pairs1.size + pairs2.size
64
+ intersection = 0
65
+ pairs1.each do |p1|
66
+ 0.upto(pairs2.size-1) do |i|
67
+ if p1 == pairs2[i]
68
+ intersection += 1
69
+ pairs2.slice!(i)
70
+ break
71
+ end
72
+ end
73
+ end
74
74
  (2.0 * intersection) / union
75
75
  end
76
+ memoize :dices_coefficient_similar
77
+
78
+ # this seems like it would slow things down
79
+ def utf8?
80
+ (defined?(::Encoding) ? str1.encoding.to_s : $KCODE).downcase.start_with?('u')
81
+ end
82
+ memoize :utf8?
76
83
 
77
84
  # extracted/adapted from the text gem version 1.0.2
78
85
  # normalization added for utf-8 strings
@@ -114,12 +121,8 @@ class FuzzyMatch
114
121
  # }
115
122
  1.0 - x.to_f / [n, m].max
116
123
  end
117
-
124
+ memoize :levenshtein_similar
125
+
118
126
  end
119
-
120
- extend ::ActiveSupport::Memoizable
121
- memoize :dices_coefficient_similar
122
- memoize :levenshtein_similar
123
- memoize :utf8?
124
127
  end
125
128
  end
@@ -47,7 +47,7 @@ class FuzzyMatch
47
47
  end
48
48
 
49
49
  def inspect
50
- %{#<Similarity "#{wrapper2.render}"=>"#{best_wrapper2_variant}" versus "#{wrapper1.render}"=>"#{best_wrapper1_variant}" original_weight=#{"%0.5f" % original_weight} best_score=#{best_score.inspect}>}
50
+ %{#<FuzzyMatch::Similarity #{wrapper2.render.inspect}=>#{best_wrapper2_variant.inspect} versus #{wrapper1.render.inspect}=>#{best_wrapper1_variant.inspect} original_weight=#{"%0.5f" % original_weight} best_score=#{best_score.inspect}>}
51
51
  end
52
52
  end
53
53
  end
@@ -13,7 +13,7 @@ class FuzzyMatch
13
13
  end
14
14
 
15
15
  def inspect
16
- "#<StopWord regexp=#{regexp.inspect}>"
16
+ "#<FuzzyMatch::StopWord regexp=#{regexp.inspect}>"
17
17
  end
18
18
  end
19
19
  end
@@ -1,3 +1,3 @@
1
1
  class FuzzyMatch
2
- VERSION = '1.2.1'
2
+ VERSION = '1.2.2'
3
3
  end
@@ -13,7 +13,7 @@ class FuzzyMatch
13
13
  end
14
14
 
15
15
  def inspect
16
- "#<Wrapper render=#{render} variants=#{variants.length}>"
16
+ "#<FuzzyMatch::Wrapper render=#{render.inspect} variants=#{variants.length}>"
17
17
  end
18
18
 
19
19
  def read
@@ -47,8 +47,10 @@ class FuzzyMatch
47
47
 
48
48
  alias :to_str :render
49
49
 
50
- # "Foo's Bar" should be treated as [ "Foo's", "Bar" ], so we don't use traditional regexp word boundaries (\b)
51
- WORD_BOUNDARY = %r{\s+}
50
+ # "Foo's" is one word
51
+ # "North-west" is just one word
52
+ # "Bolivia," is just Bolivia
53
+ WORD_BOUNDARY = %r{\W*(?:\s+|$)}
52
54
  def words
53
55
  @words ||= render.downcase.split(WORD_BOUNDARY)
54
56
  end
@@ -4,6 +4,9 @@ Bundler.setup
4
4
  require 'test/unit'
5
5
  require 'stringio'
6
6
  require 'remote_table'
7
+ if ENV['AMATCH'] == 'true'
8
+ require 'amatch'
9
+ end
7
10
  $LOAD_PATH.unshift(File.dirname(__FILE__))
8
11
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
12
  require 'fuzzy_match'
@@ -156,8 +156,12 @@ class TestFuzzyMatch < Test::Unit::TestCase
156
156
  assert_equal nil, d.find('RITZ')
157
157
 
158
158
  d = FuzzyMatch.new ["Foo's Bar"], :must_match_at_least_one_word => true
159
- assert_equal nil, d.find("Jacob's")
160
159
  assert_equal "Foo's Bar", d.find("Foo's")
160
+ assert_equal nil, d.find("'s")
161
+ assert_equal nil, d.find("Foo")
162
+
163
+ d = FuzzyMatch.new ["Bolivia, Plurinational State of"], :must_match_at_least_one_word => true
164
+ assert_equal "Bolivia, Plurinational State of", d.find("Bolivia")
161
165
  end
162
166
 
163
167
  def test_020_stop_words
@@ -0,0 +1,29 @@
1
+ require 'helper'
2
+
3
+ class TestWrapper < Test::Unit::TestCase
4
+ def test_001_apostrophe_s_is_not_a_word
5
+ assert_split ["foo's", "bar"], "Foo's Bar"
6
+ end
7
+
8
+ def test_002_bolivia_comma_is_just_bolivia
9
+ assert_split ["bolivia", "plurinational", "state"], "Bolivia, Plurinational State"
10
+ end
11
+
12
+ def test_003_hyphenated_words_are_not_split_up
13
+ assert_split ['north-west'], "north-west"
14
+ end
15
+
16
+ def test_004_as_expected
17
+ assert_split ['the', 'quick', "fox's", 'mouth', 'is', 'always', 'full'], "the quick fox's mouth -- is always full."
18
+ end
19
+
20
+ private
21
+
22
+ def assert_split(ary, str)
23
+ assert_equal ary, FuzzyMatch::Wrapper.new(null_fuzzy_match, str, true).words
24
+ end
25
+
26
+ def null_fuzzy_match
27
+ FuzzyMatch.new []
28
+ end
29
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fuzzy_match
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.2.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-01-18 00:00:00.000000000Z
12
+ date: 2012-01-27 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: shoulda
16
- requirement: &2177380220 !ruby/object:Gem::Requirement
16
+ requirement: &2170570400 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :development
23
23
  prerelease: false
24
- version_requirements: *2177380220
24
+ version_requirements: *2170570400
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: remote_table
27
- requirement: &2177379700 !ruby/object:Gem::Requirement
27
+ requirement: &2170569000 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ! '>='
@@ -32,10 +32,10 @@ dependencies:
32
32
  version: '0'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *2177379700
35
+ version_requirements: *2170569000
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: activerecord
38
- requirement: &2177379100 !ruby/object:Gem::Requirement
38
+ requirement: &2170568220 !ruby/object:Gem::Requirement
39
39
  none: false
40
40
  requirements:
41
41
  - - ! '>='
@@ -43,10 +43,10 @@ dependencies:
43
43
  version: '3'
44
44
  type: :development
45
45
  prerelease: false
46
- version_requirements: *2177379100
46
+ version_requirements: *2170568220
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: mysql
49
- requirement: &2177378440 !ruby/object:Gem::Requirement
49
+ requirement: &2170567580 !ruby/object:Gem::Requirement
50
50
  none: false
51
51
  requirements:
52
52
  - - ! '>='
@@ -54,10 +54,10 @@ dependencies:
54
54
  version: '0'
55
55
  type: :development
56
56
  prerelease: false
57
- version_requirements: *2177378440
57
+ version_requirements: *2170567580
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: cohort_scope
60
- requirement: &2177377600 !ruby/object:Gem::Requirement
60
+ requirement: &2170566940 !ruby/object:Gem::Requirement
61
61
  none: false
62
62
  requirements:
63
63
  - - ! '>='
@@ -65,10 +65,10 @@ dependencies:
65
65
  version: '0'
66
66
  type: :development
67
67
  prerelease: false
68
- version_requirements: *2177377600
68
+ version_requirements: *2170566940
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: weighted_average
71
- requirement: &2177377020 !ruby/object:Gem::Requirement
71
+ requirement: &2170566060 !ruby/object:Gem::Requirement
72
72
  none: false
73
73
  requirements:
74
74
  - - ! '>='
@@ -76,10 +76,10 @@ dependencies:
76
76
  version: '0'
77
77
  type: :development
78
78
  prerelease: false
79
- version_requirements: *2177377020
79
+ version_requirements: *2170566060
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: rake
82
- requirement: &2177376420 !ruby/object:Gem::Requirement
82
+ requirement: &2170558600 !ruby/object:Gem::Requirement
83
83
  none: false
84
84
  requirements:
85
85
  - - ! '>='
@@ -87,10 +87,32 @@ dependencies:
87
87
  version: '0'
88
88
  type: :development
89
89
  prerelease: false
90
- version_requirements: *2177376420
90
+ version_requirements: *2170558600
91
+ - !ruby/object:Gem::Dependency
92
+ name: yard
93
+ requirement: &2170556920 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *2170556920
102
+ - !ruby/object:Gem::Dependency
103
+ name: amatch
104
+ requirement: &2170555740 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *2170555740
91
113
  - !ruby/object:Gem::Dependency
92
114
  name: activesupport
93
- requirement: &2177375240 !ruby/object:Gem::Requirement
115
+ requirement: &2170555200 !ruby/object:Gem::Requirement
94
116
  none: false
95
117
  requirements:
96
118
  - - ! '>='
@@ -98,10 +120,10 @@ dependencies:
98
120
  version: '3'
99
121
  type: :runtime
100
122
  prerelease: false
101
- version_requirements: *2177375240
123
+ version_requirements: *2170555200
102
124
  - !ruby/object:Gem::Dependency
103
125
  name: to_regexp
104
- requirement: &2177374500 !ruby/object:Gem::Requirement
126
+ requirement: &2170554500 !ruby/object:Gem::Requirement
105
127
  none: false
106
128
  requirements:
107
129
  - - ! '>='
@@ -109,7 +131,7 @@ dependencies:
109
131
  version: 0.0.3
110
132
  type: :runtime
111
133
  prerelease: false
112
- version_requirements: *2177374500
134
+ version_requirements: *2170554500
113
135
  description: Find a needle in a haystack using string similarity and (optionally)
114
136
  regexp rules. Replaces loose_tight_dictionary.
115
137
  email:
@@ -162,6 +184,7 @@ files:
162
184
  - test/test_fuzzy_match_convoluted.rb.disabled
163
185
  - test/test_identity.rb
164
186
  - test/test_normalizer.rb
187
+ - test/test_wrapper.rb
165
188
  homepage: https://github.com/seamusabshere/fuzzy_match
166
189
  licenses: []
167
190
  post_install_message:
@@ -195,4 +218,5 @@ test_files:
195
218
  - test/test_fuzzy_match_convoluted.rb.disabled
196
219
  - test/test_identity.rb
197
220
  - test/test_normalizer.rb
221
+ - test/test_wrapper.rb
198
222
  has_rdoc: