Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide best match in a given string #23

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ Licensed under the MIT license.
a test already written, just need to implement it. Naive O(n^2) worst
case: find every match in the string, then select the highest scoring
match. Should benchmark this against current implementation once implemented
Also, "reactive rice" would be `<r><e>active r<i><c>e`
Also, "reactive rice" would be `<r><e>active r<i><c>e - [x] DONE`
2. Search feature: Work on multiple strings in a match. For example, be able
to match against 'stth' against an object { folder: 'stuff', file: 'thing' }
3. Async batch updates so the UI doesn't block for huge sets. Or maybe Web Workers?
Expand Down
96 changes: 60 additions & 36 deletions lib/fuzzy.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,39 @@ if (typeof exports !== 'undefined') {
root.fuzzy = fuzzy;
}

// prefix & suffix for score calculation
// need this in order to split matching & scoring in two phases
var PREFIX = '<';
var SUFFIX = '>';

var calculateScore = function(string) {
return string.split(PREFIX).length - 1 + (string.split(SUFFIX + PREFIX).length - 1) * 10;
};

var recursiveMatch = function(pattern, string, compareString) {
if (pattern.length === 0 || string.length === 0 || pattern.length > string.length) {
return [string];
}

var result = [];

for(var idx = 0; idx < string.length; idx++) {
if (pattern[0] === compareString[idx]) {
var ch = PREFIX + string[idx] + SUFFIX;

var arr = recursiveMatch(pattern.slice(1), string.slice(idx + 1), compareString.slice(idx + 1));

arr = arr.map(function(str){
return string.slice(0, idx) + ch + str;
});

result[result.length] = arr;
}
}

return [].concat.apply([], result); // flatten
};

// Return all elements of `array` that have a fuzzy
// match against `pattern`.
fuzzy.simpleFilter = function(pattern, array) {
Expand All @@ -32,50 +65,42 @@ fuzzy.test = function(pattern, string) {
return fuzzy.match(pattern, string) !== null;
};

// If `pattern` matches `string`, wrap each matching character
// in `opts.pre` and `opts.post`. If no match, return null
fuzzy.match = function(pattern, string, opts) {
opts = opts || {};
var patternIdx = 0
, result = []
, len = string.length
, totalScore = 0
, currScore = 0
// prefix
, pre = opts.pre || ''
// suffix

/**
pre - prefix
post - suffix
compareString - String to compare against. This might be a
lowercase version of the raw string
**/
var pre = opts.pre || ''
, post = opts.post || ''
// String to compare against. This might be a lowercase version of the
// raw string
, compareString = opts.caseSensitive && string || string.toLowerCase()
, ch, compareChar;
, compareString = opts.caseSensitive && string || string.toLowerCase();

pattern = opts.caseSensitive && pattern || pattern.toLowerCase();

// For each character in the string, either add it to the result
// or wrap in template if it's the next string in the pattern
for(var idx = 0; idx < len; idx++) {
ch = string[idx];
if(compareString[idx] === pattern[patternIdx]) {
ch = pre + ch + post;
patternIdx += 1;

// consecutive characters should increase the score more than linearly
currScore += 1 + currScore;
} else {
currScore = 0;
}
totalScore += currScore;
result[result.length] = ch;
}
var result = recursiveMatch(pattern, string, compareString)
.filter(function(el) {
return el.split(PREFIX).length - 1 === pattern.length;
});

// return rendered string if we have a match for every char
if(patternIdx === pattern.length) {
return {rendered: result.join(''), score: totalScore};
if (result.length === 0) {
return null;
}

return null;
};
return result
.map(function(el) {
return {
rendered: el.split(PREFIX).join(pre).split(SUFFIX).join(post),
score: calculateScore(el),
};
})

.reduce(function(prev, next) {
return prev.score > next.score ? prev : next;
});
}

// The normal entry point. Filters `arr` for matches against `pattern`.
// It returns an array with matching values of the type:
Expand Down Expand Up @@ -139,4 +164,3 @@ fuzzy.filter = function(pattern, arr, opts) {


}());

4 changes: 2 additions & 2 deletions test/fuzzy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ describe('fuzzy', function(){
// appear toward the beginning of the string a bit higher
});
// TODO: implement this test
xit('should prefer consecutive characters even if they come after the first match', function(){
it('should prefer consecutive characters even if they come after the first match', function(){
var opts = {pre: '<', post: '>'};
var result = fuzzy.match('bass', 'bodacious bass', opts).rendered;
expect(result).to.equal('bodacious <b><a><s><s>');
});
xit('should prefer consecutive characters in a match even if we need to break up into a substring', function(){
it('should prefer consecutive characters in a match even if we need to break up into a substring', function(){
var opts = {pre: '<', post: '>'};
var result = fuzzy.match('reic', 'reactive rice', opts).rendered;
expect(result).to.equal('<r><e>active r<i><c>e');
Expand Down