Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Binary Search for Sorted Collections #3207

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
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