diff --git a/fuzzy-min.js b/fuzzy-min.js index 6c7e1c4..736e360 100644 --- a/fuzzy-min.js +++ b/fuzzy-min.js @@ -1 +1 @@ -(function(){var root=this;var fuzzy={};if(typeof exports!=="undefined"){module.exports=fuzzy}else{root.fuzzy=fuzzy}fuzzy.simpleFilter=function(pattern,array){return array.filter(function(string){return fuzzy.test(pattern,string)})};fuzzy.test=function(pattern,string){return fuzzy.match(pattern,string)!==null};fuzzy.match=function(pattern,string,opts){opts=opts||{};var patternIdx=0,result=[],len=string.length,totalScore=0,currScore=0,pre=opts.pre||"",post=opts.post||"",compareString=opts.caseSensitive&&string||string.toLowerCase(),ch,compareChar;pattern=opts.caseSensitive&&pattern||pattern.toLowerCase();for(var idx=0;idxstring.length-stringIndex){return undefined}var c=pattern[patternIndex];var index=string.indexOf(c,stringIndex);var best,temp;while(index>-1){patternCache.push(index);temp=fuzzy.traverse(string,pattern,index+1,patternIndex+1,patternCache);patternCache.pop();if(!temp){return best}if(!best||best.score0){if(patternCache[i-1]+1===index){temp+=temp+1}else{temp=1}}score+=temp});return score};fuzzy.render=function(string,indices,pre,post){var rendered=string.substring(0,indices[0]);indices.forEach(function(index,i){rendered+=pre+string[index]+post+string.substring(index+1,indices[i+1]?indices[i+1]:string.length)});return rendered};fuzzy.filter=function(pattern,arr,opts){opts=opts||{};return arr.reduce(function(prev,element,idx,arr){var str=element;if(opts.extract){str=opts.extract(element)}var rendered=fuzzy.match(pattern,str,opts);if(rendered!=null){prev[prev.length]={string:rendered.rendered,score:rendered.score,index:idx,original:element}}return prev},[]).sort(function(a,b){var compare=b.score-a.score;if(compare)return compare;return a.index-b.index})}})(); diff --git a/lib/fuzzy.js b/lib/fuzzy.js index a2a91eb..4399862 100644 --- a/lib/fuzzy.js +++ b/lib/fuzzy.js @@ -52,29 +52,76 @@ fuzzy.match = function(pattern, string, opts) { 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 patternCache = fuzzy.traverse(compareString, pattern, 0, 0, []); + if(!patternCache) { + return null; + } + + return {rendered: fuzzy.render(string, patternCache.cache, pre, post), score: patternCache.score}; +}; + +fuzzy.traverse = function(string, pattern, stringIndex, patternIndex, patternCache) { + + // if the pattern search at end + if(pattern.length === patternIndex) { + + // calculate score and copy the cache containing the indices where it's found + return {score : fuzzy.calculateScore(patternCache), cache : patternCache.slice()}; } - // return rendered string if we have a match for every char - if(patternIdx === pattern.length) { - return {rendered: result.join(''), score: totalScore}; + // if string at end or remaining pattern > remaining string + if(string.length === stringIndex || pattern.length - patternIndex > string.length - stringIndex) { + return null; } - return null; + var c = pattern[patternIndex]; + var index = string.indexOf(c, stringIndex); + var best, temp; + + while(index > -1) { + patternCache.push(index); + temp = fuzzy.traverse(string, pattern, index+1, patternIndex+1, patternCache); + patternCache.pop(); + + // if downstream traversal failed, return best answer so far + if(!temp) { + return best; + } + + if(!best || best.score < temp.score) { + best = temp; + } + + index = string.indexOf(c, index+1); + } + + return best; +}; + +fuzzy.calculateScore = function(patternCache) { + var score = 0; + var temp = 1; + patternCache.forEach(function(index, i) { + if(i > 0) { + if(patternCache[i-1] + 1 === index) { + temp += temp + 1; + } else { + temp = 1; + } + } + + score += temp; + }); + return score; +}; + +fuzzy.render = function(string, indices, pre, post) { + var rendered = string.substring(0, indices[0]); + indices.forEach(function(index, i) { + rendered += pre + string[index] + post + + string.substring(index + 1, (indices[i+1]) ? indices[i+1] : string.length); + }); + return rendered; }; // The normal entry point. Filters `arr` for matches against `pattern`. @@ -108,6 +155,10 @@ fuzzy.filter = function(pattern, arr, opts) { var str = element; if(opts.extract) { str = opts.extract(element); + + if(!str) { // take care of undefineds / nulls / etc. + str = ''; + } } var rendered = fuzzy.match(pattern, str, opts); if(rendered != null) { diff --git a/test/fuzzy.test.js b/test/fuzzy.test.js index 8a0d9c2..582d844 100644 --- a/test/fuzzy.test.js +++ b/test/fuzzy.test.js @@ -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 '); }); - 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('active re');