diff --git a/src/dataset.js b/src/dataset.js index 036d83bb..818c74f1 100644 --- a/src/dataset.js +++ b/src/dataset.js @@ -10,7 +10,7 @@ var Dataset = (function() { utils.bindAll(this); if (o.template && !o.engine) { - throw new Error('no template engine specified'); + $.error('no template engine specified'); } this.name = o.name; @@ -130,65 +130,57 @@ var Dataset = (function() { }); }, - _getPotentiallyMatchingIds: function(terms) { - var potentiallyMatchingIds = []; - var lists = []; - utils.map(terms, utils.bind(function(term) { - var list = this.adjacencyList[term.charAt(0)]; - if (!list) { return; } - lists.push(list); - },this)); - if (lists.length === 1) { - return lists[0]; - } - var listLengths = []; - $.each(lists, function(i, list) { - listLengths.push(list.length); + _getLocalSuggestions: function(terms) { + var that = this, + firstChars = [], + lists = [], + shortestList, + suggestions = []; + + // create a unique array of the first chars in + // the terms this comes in handy when multiple + // terms start with the same letter + utils.each(terms, function(i, term) { + var firstChar = term.charAt(0); + !~firstChars.indexOf(firstChar) && firstChars.push(firstChar); }); - var shortestListIndex = utils.indexOf(listLengths, Math.min.apply(null, listLengths)) || 0; - var shortestList = lists[shortestListIndex] || []; - potentiallyMatchingIds = utils.map(shortestList, function(item) { - var idInEveryList = utils.every(lists, function(list) { - return utils.indexOf(list, item) > -1; - }); - if (idInEveryList) { - return item; + + utils.each(firstChars, function(i, firstChar) { + var list = that.adjacencyList[firstChar]; + + // break out of the loop early + if (!list) { return false; } + + lists.push(list); + + if (!shortestList || list.length < shortestList.length) { + shortestList = list; } }); - return potentiallyMatchingIds; - }, - _getItemsFromIds: function(ids) { - var items = []; - utils.map(ids, utils.bind(function(id) { - var item = this.itemHash[id]; - if (item) { - items.push(item); - } - }, this)); - return items; - }, + // no suggestions :( + if (lists.length < firstChars.length) { + return []; + } - _matcher: function(terms) { - if (this._customMatcher) { - var customMatcher = this._customMatcher; - return function(item) { - return customMatcher(item); - }; - } else { - return function(item) { - var tokens = item.tokens; - var allTermsMatched = utils.every(terms, function(term) { - var tokensMatched = utils.filter(tokens, function(token) { - return token.indexOf(term) === 0; - }); - return tokensMatched.length; + // populate suggestions + utils.each(shortestList, function(i, id) { + var item = that.itemHash[id], isCandidate, isMatch; + + isCandidate = utils.every(lists, function(list) { + return ~utils.indexOf(list, id); + }); + + isMatch = isCandidate && utils.every(terms, function(term) { + return utils.some(item.tokens, function(token) { + return token.indexOf(term) === 0; }); - if (allTermsMatched) { - return item; - } - }; - } + }); + + isMatch && suggestions.push(item); + }); + + return suggestions; }, _compareItems: function(a, b, areLocalItems) { @@ -224,33 +216,6 @@ var Dataset = (function() { _processRemoteSuggestions: function(callback, matchedItems) { var that = this; - return function(data) { - //convert remote suggestions to object - utils.each(data, function(i, remoteItem) { - var isDuplicate = false; - - remoteItem = utils.isString(remoteItem) ? - { value: remoteItem } : remoteItem; - - // checks for duplicates - utils.each(matchedItems, function(i, localItem) { - if (remoteItem.value === localItem.value) { - isDuplicate = true; - - // break out of each loop - return false; - } - }); - - !isDuplicate && matchedItems.push(remoteItem); - - // if we're at the limit, we no longer need to process - // the remote results and can break out of the each loop - return matchedItems.length < that.limit; - }); - - callback && callback(matchedItems); - }; }, // public methods @@ -271,15 +236,48 @@ var Dataset = (function() { return this; }, - getSuggestions: function(query, callback) { - var terms = utils.tokenizeQuery(query); - var potentiallyMatchingIds = this._getPotentiallyMatchingIds(terms); - var potentiallyMatchingItems = this._getItemsFromIds(potentiallyMatchingIds); - var matchedItems = utils.filter(potentiallyMatchingItems, this._matcher(terms)); - matchedItems.sort(this._ranker); - callback && callback(matchedItems); - if (matchedItems.length < this.limit && this.transport) { - this.transport.get(query, this._processRemoteSuggestions(callback, matchedItems)); + getSuggestions: function(query, cb) { + var that = this, + terms = utils.tokenizeQuery(query), + suggestions = this._getLocalSuggestions(terms) + .sort(this._ranker) + .slice(0, this.limit); + + cb && cb(suggestions); + + if (suggestions.length < this.limit && this.transport) { + this.transport.get(query, processRemoteData); + } + + // callback for transport.get + function processRemoteData(data) { + suggestions = suggestions.slice(0); + + // convert remote suggestions to object + utils.each(data, function(i, remoteItem) { + var isDuplicate = false; + + remoteItem = utils.isString(remoteItem) ? + { value: remoteItem } : remoteItem; + + // checks for duplicates + utils.each(suggestions, function(i, suggestion) { + if (remoteItem.value === suggestion.value) { + isDuplicate = true; + + // break out of each loop + return false; + } + }); + + !isDuplicate && suggestions.push(remoteItem); + + // if we're at the limit, we no longer need to process + // the remote results and can break out of the each loop + return suggestions.length < that.limit; + }); + + cb && cb(suggestions); } } }); diff --git a/src/utils.js b/src/utils.js index 41c2ee59..b631dc04 100644 --- a/src/utils.js +++ b/src/utils.js @@ -67,6 +67,20 @@ var utils = { return !!result; }, + some: function(obj, test) { + var result = false; + + if (!obj) { return result; } + + $.each(obj, function(key, val) { + if (result = test.call(null, val, key, obj)) { + return false; + } + }); + + return !!result; + }, + mixin: $.extend, getUniqueId: (function() { @@ -128,19 +142,6 @@ var utils = { }; }, - uniqueArray: function(array) { - var u = {}, a = []; - - for(var i = 0, l = array.length; i < l; ++i) { - if(u.hasOwnProperty(array[i])) { continue; } - - a.push(array[i]); - u[array[i]] = 1; - } - - return a; - }, - tokenizeQuery: function(str) { return $.trim(str).toLowerCase().split(/[\s]+/); }, diff --git a/test/dataset_spec.js b/test/dataset_spec.js index 7f341a9b..a6c39b81 100644 --- a/test/dataset_spec.js +++ b/test/dataset_spec.js @@ -242,18 +242,6 @@ describe('Dataset', function() { this.dataset = new Dataset({}).initialize({ local: fixtureData }); }); - it('allow for a custom matching function to be defined', function() { - this.dataset._customMatcher = function(item) { return item; }; - - this.dataset.getSuggestions('ca', function(items) { - expect(items).toEqual([ - { tokens: ['coconut'], value: 'coconut' }, - { tokens: ['cake'], value: 'cake' }, - { tokens: ['coffee'], value: 'coffee' } - ]); - }); - }); - it('allow for a custom ranking function to be defined', function() { this.dataset._customRanker = function(a, b) { return a.value.length > b.value.length ? @@ -277,7 +265,7 @@ describe('Dataset', function() { }); it('network requests are not triggered with enough local results', function() { - this.dataset.limit = 1; + this.dataset.limit = 3; this.dataset.getSuggestions('c', function(items) { expect(items).toEqual([ { tokens: ['coconut'], value: 'coconut' }, @@ -323,16 +311,29 @@ describe('Dataset', function() { }); it('concatenates local and remote results and dedups them', function() { - var local = [expectedItemHash.cake, expectedItemHash.coffee], - remote = [expectedItemHash.coconut, expectedItemHash.cake]; + var spy = jasmine.createSpy(), + remote = [expectedItemHash.grape, expectedItemHash.cake]; - this.dataset._processRemoteSuggestions(function(items) { - expect(items).toEqual([ - expectedItemHash.cake, - expectedItemHash.coffee, - expectedItemHash.coconut - ]); - }, local)(remote); + this.dataset.transport.get.andCallFake(function(q, cb) { cb(remote); }); + + this.dataset.getSuggestions('c', spy); + + expect(spy.callCount).toBe(2); + + // local suggestions + expect(spy.argsForCall[0]).toContain([ + expectedItemHash.coconut, + expectedItemHash.cake, + expectedItemHash.coffee + ]); + + // local + remote suggestions + expect(spy.argsForCall[1]).toContain([ + expectedItemHash.coconut, + expectedItemHash.cake, + expectedItemHash.coffee, + expectedItemHash.grape + ]); }); it('sorts results: local first, then remote, sorted by graph weight / score within each local/remote section', function() { @@ -350,11 +351,6 @@ describe('Dataset', function() { { id: 4, weight: 0, score: 100000 } ]); }); - - it('only returns unique ids when looking up potentially matching ids', function() { - this.dataset.adjacencyList = { a: [1, 2, 3, 4], b: [3, 4, 5, 6] }; - expect(this.dataset._getPotentiallyMatchingIds(['a','b'])).toEqual([3, 4]); - }); }); describe('tokenization', function() {