Skip to content

Commit

Permalink
Merge pull request #110 from jharding/dataset-refactor
Browse files Browse the repository at this point in the history
Dataset refactoring
  • Loading branch information
jharding committed Mar 13, 2013
2 parents 16e888c + 8d0aeaf commit 7fd872d
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 130 deletions.
178 changes: 88 additions & 90 deletions src/dataset.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
}
});
Expand Down
27 changes: 14 additions & 13 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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]+/);
},
Expand Down
50 changes: 23 additions & 27 deletions test/dataset_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?
Expand All @@ -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' },
Expand Down Expand Up @@ -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() {
Expand All @@ -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() {
Expand Down

0 comments on commit 7fd872d

Please sign in to comment.