diff --git a/backbone.js b/backbone.js index cedcf04a3..668a0db66 100644 --- a/backbone.js +++ b/backbone.js @@ -629,6 +629,18 @@ var setOptions = {add: true, remove: true, merge: true}; var addOptions = {add: true, remove: false}; + // Compare two values, in a `sort`-consistent way + var compareVals = function(a, b) { + if (a === b) return 0; + var isAComparable = a >= a, isBComparable = b >= b; + if (isAComparable || isBComparable) { + if (isAComparable && !isBComparable) return -1; + if (isBComparable && !isAComparable) return 1; + } + return a > b ? 1 : (b > a) ? -1 : 0; + }; + + // Define the Collection's inheritable methods. _.extend(Collection.prototype, Events, { @@ -828,16 +840,68 @@ return this.models[index]; }, + // Perform a binary search for a model in a sorted collection. + search: function (toFind, getMax) { + if (!this.comparator) throw new Error('Cannot search an unsorted Collection'); + + // Create `compare` function for searching. + var compare; + if (_.isFunction(toFind)) { + // The user has provided the `compare` function. + compare = _.bind(toFind, this); + } else if (_.isFunction(this.comparator)) { + // Use the `comparator` function, `toFind` is a model. + if (this.comparator.length === 2) { + compare = _.bind(this.comparator, this, toFind); + } else { + compare = _.bind(function (valResult, model) { + return compareVals(valResult, this.comparator(model)); + }, this, this.comparator(toFind)); + } + } else { + // `comparator` is a string indicating the model property used to sort + // `toFind` is the value sought. + compare = _.bind(function (model) { + return compareVals(toFind, model.get(this.comparator)); + }, this); + } + + // Perform binary search. + var found = false, max = this.length - 1, min = 0, index, relValue; + while (max >= min && !found) { + index = Math.floor((max + min) / 2); + relValue = compare(this.at(index)); + if (relValue > 0) { + min = index + 1; + } else if (relValue < 0) { + max = index - 1; + } else if (getMax && index < max && compare(this.at(index + 1)) === 0) { + min = index + 1; + } else if (!getMax && index > min && compare(this.at(index - 1)) === 0) { + max = index - 1; + } else { + found = true; + } + } + + return found ? index : -1; + }, + // Return models with matching attributes. Useful for simple cases of // `filter`. where: function(attrs, first) { - if (_.isEmpty(attrs)) return first ? void 0 : []; - return this[first ? 'find' : 'filter'](function(model) { - for (var key in attrs) { - if (attrs[key] !== model.get(key)) return false; - } - return true; - }); + if (_.isEmpty(attrs) || !_.isEqual(_.keys(attrs), [this.comparator])) { + var matches = _.matches(attrs); + return this[first ? 'find' : 'filter'](function(model) { + return matches(model.attributes); + }); + } else if (first) { + return this.at(this.search(attrs[this.comparator])); + } else { + var minIndex = this.search(attrs[this.comparator]); + var maxIndex = this.search(attrs[this.comparator], true); + return minIndex == null ? [] : this.models.slice(minIndex, maxIndex + 1); + } }, // Return the first model with matching attributes. Useful for simple cases diff --git a/test/collection.js b/test/collection.js index 8f518143d..31395b95e 100644 --- a/test/collection.js +++ b/test/collection.js @@ -547,15 +547,53 @@ equal(JSON.stringify(col), '[{"id":3,"label":"a"},{"id":2,"label":"b"},{"id":1,"label":"c"},{"id":0,"label":"d"}]'); }); - test("where and findWhere", 8, function() { + test("search", 7, function() { + var model1 = new Backbone.Model({a: 0}); + var model2 = new Backbone.Model({a: 3}); + var models = [ + model1, {a: 1}, {a: 1}, {a: 1}, {a: 2}, model2 + ]; + var coll = new Backbone.Collection(models, { + comparator: 'a' + }); + + equal(coll.at(coll.search(1)).get('a'), 1); + equal(coll.search(1), 1); + equal(coll.search(1, true), 3); + ok(coll.search(4) === -1); + + equal(coll.at(coll.search(function (model) { + var v = model.get('a'); + if (v < 2) return 1; + if (v > 2) return -1; + return 0; + })).get('a'), 2); + + var col2 = new Backbone.Collection(models, { + comparator: function (m) { + return 5 - m.get('a'); + } + }); + + var col3 = new Backbone.Collection(models, { + comparator: function (m1, m2) { + return m2.get('a') - m1.get('a'); + } + }); + equal(col3.at(col3.search(model1)), model1); + equal(col3.at(col3.search(model2)), model2); + }); + + test("where and findWhere", 16, function() { var model = new Backbone.Model({a: 1}); - var coll = new Backbone.Collection([ + var models = [ model, {a: 1}, {a: 1, b: 2}, {a: 2, b: 2}, {a: 3} - ]); + ]; + var coll = new Backbone.Collection(models); equal(coll.where({a: 1}).length, 3); equal(coll.where({a: 2}).length, 1); equal(coll.where({a: 3}).length, 1); @@ -564,6 +602,18 @@ equal(coll.where({a: 1, b: 2}).length, 1); equal(coll.findWhere({a: 1}), model); equal(coll.findWhere({a: 4}), void 0); + + var col2 = new Backbone.Collection(models, { + comparator: 'a' + }); + equal(col2.where({a: 1}).length, 3); + equal(col2.where({a: 2}).length, 1); + equal(col2.where({a: 3}).length, 1); + equal(col2.where({b: 1}).length, 0); + equal(col2.where({b: 2}).length, 2); + equal(col2.where({a: 1, b: 2}).length, 1); + equal(col2.findWhere({a: 1}), model); + equal(col2.findWhere({a: 4}), void 0); }); test("Underscore methods", 16, function() {