From 94f734b9d8ecc878c4ffa1ba7ea5899b349bf8db Mon Sep 17 00:00:00 2001 From: Flyr1Q Date: Tue, 30 Aug 2016 16:44:20 +0600 Subject: [PATCH] Provide best match in a given string --- README.md | 2 +- lib/fuzzy.js | 96 +++++++++++++++++++++++++++++----------------- test/fuzzy.test.js | 4 +- 3 files changed, 63 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index d6f97b2..ea92ba0 100644 --- a/README.md +++ b/README.md @@ -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 `active re` + Also, "reactive rice" would be `active re - [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? diff --git a/lib/fuzzy.js b/lib/fuzzy.js index 9914065..be6298f 100644 --- a/lib/fuzzy.js +++ b/lib/fuzzy.js @@ -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) { @@ -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: @@ -139,4 +164,3 @@ fuzzy.filter = function(pattern, arr, opts) { }()); - diff --git a/test/fuzzy.test.js b/test/fuzzy.test.js index d1bc253..9059743 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');