Skip to content
78 changes: 71 additions & 7 deletions backbone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, {

Expand Down Expand Up @@ -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');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this broken for the _.isFunction(toFind) case

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well no, if the collection isn't sorted then it isn't sorted. I suppose you could allow this to pass in the case that the user has sorted the collection himself. I believe the thinking behind the _.isFunction(toFind) case was that if you were, for example, looking for a model who had a timestamp within a certain range (say a week or day) in a collection sorted by the timestamps property.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really like the toFind function feature, to easy to introduce inconsistencies in dev code if theres any inconsitency with toFind and comparitor. 👎 on that

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose I also thought that this was a good idea for the case that the comparator is a function. The user needs to have a model which matches equal to that he is searching for, whereas supplying the compare function, allows him to search specifically for models which match certain criteria without having to create a model to do it if one is not at hand.


// 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clever!

} 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
Expand Down
56 changes: 53 additions & 3 deletions test/collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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() {
Expand Down