Skip to content

Commit a70ea9b

Browse files
committed
Add QuickSilver port
1 parent a29815e commit a70ea9b

File tree

6 files changed

+153
-34
lines changed

6 files changed

+153
-34
lines changed

README.md

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,41 @@
11
String Score
22
============
33

4-
Port of [LiquidMetal](https://github.com/rmm5t/liquidmetal) from JavaScript to Python.
5-
64
An algorithm provides scores between 0.0 (no match) to 1.0 (perfect match) for a comparison of two strings.
75

6+
The algorithm is designed for auto-completion. For string similarity, please check [Levenshtein distance](https://en.wikipedia.org/wiki/Levenshtein_distance) ([Wagner–Fischer algorithm](https://en.wikipedia.org/wiki/Wagner-Fischer_algorithm)).
7+
88
Usage
99
-----
1010

1111
Include the library:
1212

1313
```python
14-
import stringscore
14+
from stringscore import liquidmetal
1515
```
1616

1717
Score any string against an abbreviation:
1818

1919
```python
20-
>>> stringscore.score('FooBar', 'foo')
20+
>>> liquidmetal.score('FooBar', 'foo')
2121
0.95
22-
>>> stringscore.score('FooBar', 'fb')
22+
>>> liquidmetal.score('FooBar', 'fb')
2323
0.916666666667
24-
>>> stringscore.score('Foo Bar', 'fb')
24+
>>> liquidmetal.score('Foo Bar', 'fb')
2525
0.928571428571
26-
>>> stringscore.score('Foo Bar', 'baz')
26+
>>> liquidmetal.score('Foo Bar', 'baz')
2727
0.0
28-
>>> stringscore.score('Foo Bar', '')
28+
>>> liquidmetal.score('Foo Bar', '')
2929
0.8
3030
```
3131

3232
Similar Works
3333
-------------
3434

35-
- Quicksilver's [scoreForAbbreviation](https://github.com/quicksilver/Quicksilver/blob/master/Quicksilver/Code-QuickStepFoundation/NSString_BLTRExtensions.m#L53) algorithm by Alcor
35+
- Quicksilver's [scoreForAbbreviation](https://github.com/quicksilver/Quicksilver/blob/master/Quicksilver/Code-QuickStepFoundation/NSString_BLTRExtensions.m#L53) algorithm by Alcor (Blacktree, Inc)
36+
- [LiquidMetal](https://github.com/rmm5t/liquidmetal) by Ryan McGeary
3637
- [string_score](https://github.com/joshaven/string_score) by Joshaven Potter
38+
- [jQuery.fuzzyMatch](https://github.com/rapportive-oss/jquery-fuzzymatch) by [Rapportive](http://rapportive.com/)
3739

3840
License
3941
-------
@@ -43,5 +45,6 @@ String Score is released under the [MIT License](http://opensource.org/licenses/
4345
Credits
4446
-------
4547

48+
Copyright © 2003 Blacktree, Inc (Original author of [Quicksilver](https://github.com/quicksilver/Quicksilver))
4649
Copyright © 2009 Ryan McGeary (Author of [LiquidMetal](https://github.com/rmm5t/liquidmetal))
4750
Copyright © 2013 Grey Lee

setup.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from distutils.core import setup
22
setup(name='stringscore',
3-
version='1.2.1',
3+
version='1.0',
44
description='An algorithm provides scores between 0.0 (no match) to 1.0 (perfect match) for a comparison of two strings.',
55
classifiers=[
66
'Development Status :: 3 - Alpha',
77
'Intended Audience :: Developers',
88
'License :: OSI Approved :: MIT License',
9+
'Operating System :: OS Independent',
910
'Programming Language :: Python',
1011
'Programming Language :: Python :: 2',
1112
'Programming Language :: Python :: 2.5',
@@ -22,5 +23,6 @@
2223
url='https://github.com/bcse/stringscore',
2324
license='MIT License',
2425
packages=['stringscore'],
26+
package_dir={'stringscore': 'stringscore'},
2527
platforms=['any'],
2628
)

stringscore/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
String Score
3+
~~~~~~~~~~~~
4+
5+
An algorithm provides scores between 0.0 (no match) to 1.0 (perfect match) for a comparison of two strings.
6+
7+
:copyright: (c) 2013 by Grey Lee.
8+
:license: MIT License.
9+
"""
10+
11+
__version__ = '1.0'
12+
__author__ = 'Grey Lee <bcse@bcse.tw>'

stringscore.py renamed to stringscore/liquidmetal.py

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
1-
"""
2-
String Score
3-
~~~~~~~~~~~~
4-
5-
Port of [LiquidMetal](https://github.com/rmm5t/liquidmetal) from JavaScript to Python.
6-
7-
An algorithm provides scores between 0.0 (no match) to 1.0 (perfect match) for a comparison of two strings.
8-
9-
:copyright: (c) 2013 by Grey Lee.
10-
:license: MIT License.
11-
"""
12-
13-
__version__ = '1.2.1'
14-
__author__ = 'Grey Lee <bcse@bcse.tw>'
15-
1+
# Ported from LiquidMetal
2+
# https://github.com/rmm5t/liquidmetal
163

174
SCORE_NO_MATCH = 0.0
185
SCORE_MATCH = 1.0
@@ -130,4 +117,4 @@ def fill_array(array, value, begin, end):
130117
if len(abbrev) > len(string):
131118
string, abbrev = abbrev, string
132119
score(string, abbrev)
133-
print 'Benchmark: %ss' % (clock() - t0)
120+
print('Benchmark: %ss' % (clock() - t0))

stringscore/quicksilver.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Ported from Quicksilver's scoreForAbbreviation algorithm
2+
# https://github.com/quicksilver/Quicksilver/
3+
4+
SCORE_NO_MATCH = 0.0
5+
SCORE_MATCH = 1.0
6+
SCORE_TRAILING = 0.9
7+
SCORE_BUFFER = 0.15
8+
WHITESPACE_CHARACTERS = ' \t'
9+
10+
11+
def score(string, abbrev):
12+
abbrev = abbrev.lower()
13+
abbrev_len = len(abbrev)
14+
string_len = len(string)
15+
16+
# deduct some points for all remaining letters
17+
if abbrev_len == 0:
18+
return SCORE_TRAILING
19+
if abbrev_len > string_len:
20+
return SCORE_NO_MATCH
21+
22+
# Search for steadily smaller portions of the abbreviation
23+
for i in range(abbrev_len, 0, -1):
24+
try:
25+
index = string.lower().index(abbrev[:i])
26+
except ValueError:
27+
continue # Not found
28+
29+
if index + abbrev_len > string_len:
30+
continue
31+
32+
next_string = string[index + i:]
33+
next_abbrev = abbrev[i:]
34+
35+
# Search what is left of the string with the rest of the abbreviation
36+
remaining_score = score(next_string, next_abbrev)
37+
38+
if remaining_score > 0:
39+
result_score = index + i
40+
41+
# ignore skipped characters if is first letter of a word
42+
if index > 0: # if some letters were skipped
43+
if string[index - 1] in WHITESPACE_CHARACTERS:
44+
for j in range(index - 1):
45+
c = string[j]
46+
result_score -= SCORE_MATCH \
47+
if c in WHITESPACE_CHARACTERS \
48+
else SCORE_BUFFER
49+
elif 'A' <= string[index] <= 'Z':
50+
for j in range(index):
51+
c = string[j]
52+
result_score -= SCORE_MATCH \
53+
if 'A' <= c <= 'Z' \
54+
else SCORE_BUFFER
55+
else:
56+
result_score -= index
57+
58+
result_score += remaining_score * len(next_string)
59+
result_score /= string_len
60+
return result_score
61+
62+
return SCORE_NO_MATCH
63+
64+
65+
if __name__ == '__main__':
66+
from time import clock
67+
t0 = clock()
68+
test_string = 'a'
69+
for string in open('/usr/share/dict/words'):
70+
abbrev = test_string
71+
test_string = string
72+
if len(abbrev) > len(string):
73+
string, abbrev = abbrev, string
74+
score(string, abbrev)
75+
print('Benchmark: %ss' % (clock() - t0))

test_stringscore.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import unittest
22

3-
import stringscore
3+
from stringscore import liquidmetal
4+
from stringscore import quicksilver
45

56

6-
class StringScoreTestCase(unittest.TestCase):
7+
class LiquidMetalTestCase(unittest.TestCase):
78

89
def test_score(self):
9-
n = stringscore.SCORE_NO_MATCH
10-
m = stringscore.SCORE_MATCH
11-
t = stringscore.SCORE_TRAILING
12-
s = stringscore.SCORE_TRAILING_BUT_STARTED
13-
b = stringscore.SCORE_BUFFER
10+
n = liquidmetal.SCORE_NO_MATCH
11+
m = liquidmetal.SCORE_MATCH
12+
t = liquidmetal.SCORE_TRAILING
13+
s = liquidmetal.SCORE_TRAILING_BUT_STARTED
14+
b = liquidmetal.SCORE_BUFFER
1415

1516
tests = {
1617
('', ''): [t],
@@ -46,10 +47,49 @@ def test_score(self):
4647
}
4748

4849
for k, v in tests.items():
49-
score = round(stringscore.score(*k), 12)
50+
score = round(liquidmetal.score(*k), 12)
5051
expected_score = round(sum(v) / len(v), 12)
5152
self.assertEqual(score, expected_score)
5253

5354

55+
class QuickSilverTestCase(unittest.TestCase):
56+
57+
def test_score(self):
58+
tests = [
59+
(('', ''), 0.9),
60+
(('', 'a'), 0.0),
61+
(('a', ''), 0.9),
62+
(('a', 'toolong'), 0.0),
63+
(('a', 'a'), 1.0),
64+
(('a', 'b'), 0.0),
65+
(('abc', ''), 0.9),
66+
(('abc', 'a'), 0.933333333333),
67+
(('abc', 'b'), 0.633333333333),
68+
(('abc', 'c'), 0.333333333333),
69+
(('abc', 'd'), 0.0),
70+
(('A', 'a'), 1.0),
71+
(('A', 'b'), 0.0),
72+
(('FooBar', ''), 0.9),
73+
(('FooBar', 'foo'), 0.95),
74+
(('FooBar', 'fb'), 0.916666666667),
75+
(('foobar', 'fb'), 0.633333333333),
76+
(('FooBar', 'b'), 0.75),
77+
(('FooBar', 'ooar'), 0.666666666667),
78+
(('FooBar', 'bab'), 0.0),
79+
(('Foo Bar', ''), 0.9),
80+
(('Foo Bar', 'foo'), 0.942857142857),
81+
(('Foo Bar', 'fb'), 0.928571428571),
82+
(('Foo-Bar', 'fb'), 0.907142857143),
83+
(('Foo_Bar', 'fb'), 0.907142857143),
84+
(('Foo Bar', 'b'), 0.907142857143),
85+
(('Foo Bar', 'ooar'), 0.571428571429),
86+
(('Foo Bar', 'bab'), 0.0),
87+
(('gnu\'s Not Unix', 'nu'), 0.85),
88+
]
89+
90+
for k, v in tests:
91+
self.assertEqual(round(quicksilver.score(*k), 12), v)
92+
93+
5494
if __name__ == '__main__':
5595
unittest.main()

0 commit comments

Comments
 (0)