liqrrdmetal 0.5.1 → 0.6
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/liqrrdmetal.rb +1 -1
- data/lib/liqrrdmetal/liqrrdmetal.rb +53 -22
- metadata +36 -24
data/lib/liqrrdmetal.rb
CHANGED
@@ -1,2 +1,2 @@
|
|
1
|
-
# encoding: UTF-8
|
1
|
+
# encoding: UTF-8
|
2
2
|
require_relative 'liqrrdmetal/liqrrdmetal'
|
@@ -80,7 +80,7 @@ require 'strscan'
|
|
80
80
|
#
|
81
81
|
# Copyright (c) 2011, Gavin Kistner (!@phrogz.net)
|
82
82
|
module LiqrrdMetal
|
83
|
-
VERSION = "0.
|
83
|
+
VERSION = "0.6"
|
84
84
|
|
85
85
|
# If you want score_with_parts to be accurate, the MATCH score must be unique
|
86
86
|
MATCH = 0.00 #:nodoc:
|
@@ -89,6 +89,7 @@ module LiqrrdMetal
|
|
89
89
|
BUFFER = [0.15] #:nodoc:
|
90
90
|
TRAILING = [0.20] #:nodoc:
|
91
91
|
NO_MATCH = [1.00] #:nodoc:
|
92
|
+
RE_CACHE = {} #:nodoc:
|
92
93
|
|
93
94
|
# Used to identify substrings and whether or not they were matched directly by the search string.
|
94
95
|
class MatchPart
|
@@ -147,6 +148,8 @@ module LiqrrdMetal
|
|
147
148
|
# using the supplied block to find the string to match against,
|
148
149
|
# receiving an array of your objects with the MatchResult module mixed in.
|
149
150
|
#
|
151
|
+
# Non-matching entries (score of 1.0) will never be included in the results, no matter the value of score_threshold
|
152
|
+
#
|
150
153
|
# User = Struct.new :name, :email, :id
|
151
154
|
# users = [ User.new( "Gavin Kistner", "!@phrogz.net", 42 ),
|
152
155
|
# User.new( "David Letterman", "lateshow@pipeline.com", 17 ),
|
@@ -163,21 +166,27 @@ module LiqrrdMetal
|
|
163
166
|
# #=> ["<span class='match'>s</span>cottadams@aol<span class='match'>.com</span>",
|
164
167
|
# #=> "late<span class='match'>s</span>how@pipeline<span class='match'>.com</span>"]
|
165
168
|
def results_by_score( search, objects, score_threshold=1.0 )
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
169
|
+
re = RE_CACHE[search] ||= /#{[*search.chars].join('.*?')}/i
|
170
|
+
objects.map{ |o|
|
171
|
+
m = yield(o)
|
172
|
+
if m=~re
|
173
|
+
score,parts = score_with_parts(search,m)
|
174
|
+
if score<score_threshold
|
175
|
+
o.extend MatchResult
|
176
|
+
o.liqrrd_match = m
|
177
|
+
o.liqrrd_score, o.liqrrd_parts = score,parts
|
178
|
+
o
|
179
|
+
end
|
180
|
+
end
|
181
|
+
}.compact.sort_by{ |o|
|
173
182
|
[ o.liqrrd_score, o.liqrrd_match ]
|
174
183
|
}
|
175
184
|
end
|
176
185
|
|
177
186
|
# Match a single search term against an array of possible results,
|
178
187
|
# receiving an array sorted by score (descending) of the matched text parts.
|
179
|
-
#
|
180
|
-
#
|
188
|
+
#
|
189
|
+
# Non-matching entries (score of 1.0) will never be included in the results, no matter the value of score_threshold
|
181
190
|
#
|
182
191
|
# items = ["FooBar","Foo Bar","For the Love of Big Cars"]
|
183
192
|
# hits = LiqrrdMetal.parts_by_score( "foobar", items )
|
@@ -186,11 +195,15 @@ module LiqrrdMetal
|
|
186
195
|
# #=> _Foo_ _Bar_
|
187
196
|
# #=> _Fo_r the L_o_ve of _B_ig C_ar_s
|
188
197
|
def parts_by_score( search, actuals, score_threshold=1.0 )
|
198
|
+
re = RE_CACHE[search] ||= /#{[*search.chars].join('.*?')}/i
|
189
199
|
actuals.map{ |actual|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
200
|
+
if actual=~re
|
201
|
+
score,parts = score_with_parts(search,actual)
|
202
|
+
if score<score_threshold
|
203
|
+
[ actual, score, parts ]
|
204
|
+
end
|
205
|
+
end
|
206
|
+
}.compact.sort_by{ |actual,score,parts|
|
194
207
|
[ score, actual ]
|
195
208
|
}.map{ |actual,score,parts|
|
196
209
|
parts
|
@@ -208,12 +221,13 @@ module LiqrrdMetal
|
|
208
221
|
# p parts.map(&:to_html).join
|
209
222
|
# #=> "A <span class='match'>Foo</span>l in Lo<span class='match'>v</span>e"
|
210
223
|
def score_with_parts( search, actual )
|
224
|
+
re = RE_CACHE[search] ||= /#{[*search.chars].join('.*?')}/i
|
211
225
|
if search.length==0
|
212
226
|
[ TRAILING[0], [MatchPart.new(actual)] ]
|
213
|
-
elsif search.length > actual.length
|
227
|
+
elsif (search.length > actual.length) || (search !~ re)
|
214
228
|
[ NO_MATCH[0], [MatchPart.new(actual)] ]
|
215
229
|
else
|
216
|
-
values =
|
230
|
+
values = letter_scores( search, actual )
|
217
231
|
score = values.inject{ |sum,score| sum+score } / values.length
|
218
232
|
was_matching,start = nil
|
219
233
|
parts = []
|
@@ -229,23 +243,42 @@ module LiqrrdMetal
|
|
229
243
|
[ score, parts ]
|
230
244
|
end
|
231
245
|
end
|
246
|
+
|
247
|
+
# Returns an array of score/string tuples, sorted by score, below the <code>score_threshold</code>
|
248
|
+
#
|
249
|
+
# Non-matching entries (score of 1.0) will never be included in the results, no matter the value of <code>score_threshold</code>
|
250
|
+
def sorted_with_scores( search, actuals, score_threshold=1.0 )
|
251
|
+
if search.length==0
|
252
|
+
[]
|
253
|
+
else
|
254
|
+
re = RE_CACHE[search] ||= /#{[*search.chars].join('.*?')}/i
|
255
|
+
actuals.map{ |actual|
|
256
|
+
if actual=~re
|
257
|
+
values = letter_scores( search, actual )
|
258
|
+
score = values.inject{ |sum,score| sum+score } / values.length
|
259
|
+
[score,actual] if score < score_threshold
|
260
|
+
end
|
261
|
+
}.compact.sort
|
262
|
+
end
|
263
|
+
end
|
232
264
|
|
233
265
|
# Return a score for matching the search term against the actual text.
|
234
266
|
# A score of <code>1.0</code> indicates no match. A score of <code>0.0</code> is a perfect match.
|
235
267
|
def score( search, actual )
|
268
|
+
re = RE_CACHE[search] ||= /#{[*search.chars].join('.*?')}/i
|
236
269
|
if search.length==0
|
237
270
|
TRAILING[0]
|
238
|
-
elsif search.length > actual.length
|
271
|
+
elsif (search.length > actual.length) || (search !~ re)
|
239
272
|
NO_MATCH[0]
|
240
273
|
else
|
241
|
-
values =
|
274
|
+
values = letter_scores( search, actual )
|
242
275
|
values.inject{ |sum,score| sum+score } / values.length
|
243
276
|
end
|
244
277
|
end
|
245
278
|
|
246
279
|
# Return an aray of scores for each letter in the actual text.
|
247
280
|
# Returns a single-value array of <code>[0.0]</code> if no match exists.
|
248
|
-
def
|
281
|
+
def letter_scores( search, actual )
|
249
282
|
actual_length = actual.length
|
250
283
|
scores = Array.new(actual_length)
|
251
284
|
|
@@ -270,6 +303,4 @@ module LiqrrdMetal
|
|
270
303
|
scores[ (last+1)...scores.length ] = (started ? TRAILING_BUT_STARTED : TRAILING) * (scores.length-last-1)
|
271
304
|
scores
|
272
305
|
end
|
273
|
-
end
|
274
|
-
|
275
|
-
|
306
|
+
end
|
metadata
CHANGED
@@ -1,52 +1,64 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: liqrrdmetal
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 6
|
8
|
+
version: "0.6"
|
6
9
|
platform: ruby
|
7
|
-
authors:
|
10
|
+
authors:
|
8
11
|
- Gavin Kistner
|
9
12
|
autorequire:
|
10
13
|
bindir: bin
|
11
14
|
cert_chain: []
|
12
|
-
|
15
|
+
|
16
|
+
date: 2011-04-19 00:00:00 -06:00
|
13
17
|
default_executable:
|
14
18
|
dependencies: []
|
15
|
-
|
16
|
-
|
17
|
-
quickly find the most relevant items by typing in portions of the string, while
|
18
|
-
seeing the portions of the substring that are being matched.
|
19
|
+
|
20
|
+
description: Derived from the LiquidMetal JavaScript library, LiqrrdMetal brings substring scoring to Ruby. Similar to Quicksilver, LiqrrdMetal gives users the ability to quickly find the most relevant items by typing in portions of the string, while seeing the portions of the substring that are being matched.
|
19
21
|
email: gavin@phrogz.net
|
20
22
|
executables: []
|
23
|
+
|
21
24
|
extensions: []
|
25
|
+
|
22
26
|
extra_rdoc_files: []
|
23
|
-
|
27
|
+
|
28
|
+
files:
|
24
29
|
- lib/liqrrdmetal/liqrrdmetal.rb
|
25
30
|
- lib/liqrrdmetal.rb
|
26
31
|
has_rdoc: true
|
27
32
|
homepage: http://github.com/Phrogz/liqrrdmetal
|
28
33
|
licenses: []
|
34
|
+
|
29
35
|
post_install_message:
|
30
36
|
rdoc_options: []
|
31
|
-
|
37
|
+
|
38
|
+
require_paths:
|
32
39
|
- lib
|
33
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
34
41
|
none: false
|
35
|
-
requirements:
|
36
|
-
- -
|
37
|
-
- !ruby/object:Gem::Version
|
38
|
-
|
39
|
-
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
segments:
|
46
|
+
- 0
|
47
|
+
version: "0"
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
49
|
none: false
|
41
|
-
requirements:
|
42
|
-
- -
|
43
|
-
- !ruby/object:Gem::Version
|
44
|
-
|
45
|
-
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
segments:
|
54
|
+
- 0
|
55
|
+
version: "0"
|
56
|
+
requirements:
|
46
57
|
- StringScanner (part of the Ruby Standard Library)
|
47
58
|
rubyforge_project:
|
48
|
-
rubygems_version: 1.
|
59
|
+
rubygems_version: 1.3.7
|
49
60
|
signing_key:
|
50
61
|
specification_version: 3
|
51
62
|
summary: Calculate scoring of autocomplete-style substring matches.
|
52
63
|
test_files: []
|
64
|
+
|