todomvc
Version:
> Helping you select an MV\* framework
275 lines (234 loc) • 7.17 kB
JavaScript
/** MIT License (c) copyright B Cavalier & J Hann */
(function (define) {
define(function () {
var undef, missing = {};
/**
* @constructor
* @param identifier {Function}
* @param comparator {Function}
*/
function SortedMap (identifier, comparator) {
// identifier is required, comparator is optional
this.clear();
/**
* Fetches a value item for the given key item or the special object,
* missing, if the value item was not found.
* @private
* @param keyItem
* @returns {Object} the value item that was set for the supplied
* key item or the special object, missing, if it was not found.
*/
this._fetch = function (keyItem) {
var symbol = identifier(keyItem);
return symbol in this._index ? this._index[symbol] : missing;
};
/**
* Performs a binary search to find the bucket position of a
* key item within the key items list. Only used if we have a
* comparator.
* @private
* @param keyItem
* @param exactMatch {Boolean} if true, must be an exact match to the key
* item, not just the correct position for a key item that sorts
* the same.
* @returns {Number|Undefined}
*/
this._pos = function (keyItem, exactMatch) {
var pos, sorted, symbol;
sorted = this._sorted;
symbol = identifier(keyItem);
function getKey (pos) { return sorted[pos] ? sorted[pos][0].key : {}; }
pos = binarySearch(0, sorted.length, keyItem, getKey, comparator);
if (exactMatch) {
if (symbol != identifier(sorted[pos][0].key)) {
pos = -1;
}
}
return pos;
};
this._bucketOffset = function (bucketPos) {
var total, i;
total = 0;
for (i = 0; i < bucketPos; i++) {
total += this._sorted[i].length;
}
return total;
};
if (!comparator) {
this._pos = function (keyItem, exact) {
return exact ? -1 : this._sorted.length;
};
}
/**
* Given a keyItem and its bucket position in the list of key items,
* inserts an value item into the bucket of value items.
* This method can be overridden by other objects that need to
* have objects in the same order as the key values.
* @private
* @param valueItem
* @param keyItem
* @param pos
* @returns {Number} the absolute position of this item amongst
* all items in all buckets.
*/
this._insert = function (keyItem, pos, valueItem) {
var pair, symbol, entry, absPos;
// insert into index
pair = { key: keyItem, value: valueItem };
symbol = identifier(keyItem);
this._index[symbol] = pair;
// insert into sorted table
if (pos >= 0) {
absPos = this._bucketOffset(pos);
entry = this._sorted[pos] && this._sorted[pos][0];
// is this a new row (at end of array)?
if (!entry) {
this._sorted[pos] = [pair];
}
// are there already items of the same sort position here?
else if (comparator(entry.key, keyItem) == 0) {
absPos += this._sorted[pos].push(pair) - 1;
}
// or do we need to insert a new row?
else {
this._sorted.splice(pos, 0, [pair]);
}
}
else {
absPos = -1;
}
return absPos;
};
/**
* Given a key item and its bucket position in the list of key items,
* removes a value item from the bucket of value items.
* This method can be overridden by other objects that need to
* have objects in the same order as the key values.
* @private
* @param keyItem
* @param pos
* @returns {Number} the absolute position of this item amongst
* all items in all buckets.
*/
this._remove = function remove (keyItem, pos) {
var symbol, entries, i, entry, absPos;
symbol = identifier(keyItem);
// delete from index
delete this._index[symbol];
// delete from sorted table
if (pos >= 0) {
absPos = this._bucketOffset(pos);
entries = this._sorted[pos] || [];
i = entries.length;
// find it and remove it
while ((entry = entries[--i])) {
if (symbol == identifier(entry.key)) {
entries.splice(i, 1);
break;
}
}
absPos += i;
// if we removed all pairs at this position
if (entries.length == 0) {
this._sorted.splice(pos, 1);
}
}
else {
absPos = -1;
}
return absPos;
};
this._setComparator = function (newComparator) {
var p, pair, pos;
comparator = newComparator;
this._sorted = [];
for (p in this._index) {
pair = this._index[p];
pos = this._pos(pair.key);
this._insert(pair.key, pos, pair.value);
}
};
}
SortedMap.prototype = {
get: function (keyItem) {
var pair;
pair = this._fetch(keyItem);
return pair == missing ? undef : pair.value;
},
add: function (keyItem, valueItem) {
var pos, absPos;
if (arguments.length < 2) throw new Error('SortedMap.add: must supply keyItem and valueItem args');
// don't insert twice. bail if we already have it
if (this._fetch(keyItem) != missing) return;
// find pos and insert
pos = this._pos(keyItem);
absPos = this._insert(keyItem, pos, valueItem);
return absPos;
},
remove: function (keyItem) {
var valueItem, pos, absPos;
// don't remove if we don't already have it
valueItem = this._fetch(keyItem);
if (valueItem == missing) return;
// find positions and delete
pos = this._pos(keyItem, true);
absPos = this._remove(keyItem, pos);
return absPos;
},
forEach: function (lambda) {
var i, j, len, len2, entries;
for (i = 0, len = this._sorted.length; i < len; i++) {
entries = this._sorted[i];
for (j = 0, len2 = entries.length; j < len2; j++) {
lambda(entries[j].value, entries[j].key);
}
}
},
clear: function() {
// hashmap of object-object pairs
this._index = {};
// 2d array of objects
this._sorted = [];
},
setComparator: function (comparator) {
this._setComparator(comparator);
}
};
return SortedMap;
/**
* Searches through a list of items, looking for the correct slot
* for a new item to be added.
* @param min {Number} points at the first possible slot
* @param max {Number} points at the slot after the last possible slot
* @param item anything comparable via < and >
* @param getter {Function} a function to retrieve a item at a specific
* slot: function (pos) { return items[pos]; }
* @param comparator {Function} function to compare to items. must return
* a number.
* @returns {Number} returns the slot where the item should be placed
* into the list.
*/
function binarySearch (min, max, item, getter, comparator) {
var mid, compare;
if (max <= min) return min;
do {
mid = Math.floor((min + max) / 2);
compare = comparator(item, getter(mid));
if (isNaN(compare)) throw new Error('SortedMap: invalid comparator result ' + compare);
// if we've narrowed down to a choice of just two slots
if (max - min <= 1) {
return compare == 0 ? mid : compare > 0 ? max : min;
}
// don't use mid +/- 1 or we may miss in-between values
if (compare > 0) min = mid;
else if (compare < 0) max = mid;
else return mid;
}
while (true);
}
});
}(
typeof define == 'function'
? define
: function (factory) { module.exports = factory(require); }
));