Last active
August 29, 2015 13:56
-
-
Save insin/9251866 to your computer and use it in GitHub Desktop.
OrderedObject.js - http://bl.ocks.org/insin/9251866/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<title>OrderedObject QUnit Tests</title> | |
<!-- Test framework --> | |
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.14.0.css"> | |
<script src="//code.jquery.com/qunit/qunit-1.14.0.js"></script> | |
<!-- Code under test --> | |
<script src="OrderedObject.js"></script> | |
<!-- Test cases --> | |
<script src="tests.js"></script> | |
</head> | |
<body> | |
<div id="qunit"></div> | |
<div id="qunit-fixture"></div> | |
<a href="https://gist.github.com/insin/9251866"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub"></a> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Hash and OrderedObject initially based on http://dailyjs.com/2012/09/24/linkedhashmap/ | |
;(function (root, factory) { | |
if (typeof define === 'function' && define.amd) { | |
// AMD. Register as an anonymous module. | |
define([], factory) | |
} | |
else if (typeof exports === 'object') { | |
// Node. Does not work with strict CommonJS, but | |
// only CommonJS-like environments that support module.exports, | |
// like Node. | |
module.exports = factory() | |
} | |
else { | |
// Browser globals (root is window) | |
root.OrderedObject = factory() | |
} | |
}(this, function() { | |
'use strict'; | |
var hasOwn = Object.prototype.hasOwnProperty | |
function extend(dest, src) { | |
var keys = Object.keys(src) | |
for (var i = 0, l = keys.length; i < l; i++) { | |
dest[keys[i]] = src[keys[i]] | |
} | |
return dest | |
} | |
function inherits(childConstructor, parentConstructor) { | |
var F = function() {} | |
F.prototype = parentConstructor.prototype | |
childConstructor.prototype = new F() | |
childConstructor.prototype.constructor = childConstructor | |
return childConstructor | |
} | |
/** | |
* @constructor | |
* @param {(Hash|array|object)=} initial | |
*/ | |
function Hash(initial) { | |
if (!(this instanceof Hash)) { return new Hash(initial) } | |
this._size = 0 | |
this._map = {} | |
if (initial) { | |
this.update(initial) | |
} | |
} | |
/** | |
* @param {string} key | |
* @param {*} value | |
*/ | |
Hash.prototype.put = function(key, value) { | |
if (!this.containsKey(key)) { | |
this._size++ | |
} | |
this._map[key] = value | |
} | |
/** | |
* @param {string} key | |
* @param {*=} defaultValue | |
* @return {*} | |
*/ | |
Hash.prototype.putDefault = function(key, defaultValue) { | |
if (this.containsKey(key)) { | |
return this._map[key] | |
} | |
if (arguments.length == 1) { | |
defaultValue = null | |
} | |
this.put(key, defaultValue) | |
return defaultValue | |
} | |
/** | |
* @param {(Hash|array|object)} obj | |
*/ | |
Hash.prototype.update = function(obj) { | |
var keys, i, l | |
if (obj instanceof Hash) { | |
keys = Object.keys(obj._map) | |
for (i = 0, l = keys.length; i < l; i++) { | |
this.put(keys[i], obj._map[keys[i]]) | |
} | |
} | |
else if (Array.isArray(obj)) { | |
for (i = 0, l = obj.length; i < l; i++) { | |
if (obj[i].length != 2) { | |
throw new Error("update was given an Array which didn't have a pair at index " + i) | |
} | |
this.put(obj[i][0], obj[i][1]) | |
} | |
} | |
else { | |
keys = Object.keys(obj) | |
for (i = 0, l = keys.length; i < l; i++) { | |
this.put(keys[i], obj[keys[i]]) | |
} | |
} | |
} | |
/** | |
* @param {string} key | |
* @return {boolean} | |
*/ | |
Hash.prototype.containsKey = function(key) { | |
return hasOwn.call(this._map, key) | |
} | |
/** | |
* @param {*} value | |
* @return {boolean} | |
*/ | |
Hash.prototype.containsValue = function(value) { | |
for (var key in this._map) { | |
if (hasOwn.call(this._map, key)) { | |
return (this._map[key] === value) | |
} | |
} | |
return false | |
} | |
/** | |
* @param {string} key | |
* @param {*=} defaultValue | |
* @return {*} | |
*/ | |
Hash.prototype.get = function(key, defaultValue) { | |
if (this.containsKey(key)) { | |
return this._map[key] | |
} | |
else if (arguments.length == 1) { | |
throw new Error('KeyError: ' + key) | |
} | |
return defaultValue | |
} | |
/** | |
* @param {string} key | |
* @param {*=} defaultValue | |
* @return {*} | |
*/ | |
Hash.prototype.pop = function(key, defaultValue) { | |
if (this.containsKey(key)) { | |
this._size -- | |
var value = this._map[key] | |
delete this._map[key] | |
return value | |
} | |
else if (arguments.length == 1) { | |
throw new Error('KeyError: ' + key) | |
} | |
return defaultValue | |
} | |
/** | |
* @param {string} key | |
* @return {*} | |
*/ | |
Hash.prototype.remove = function(key) { | |
if (!this.containsKey(key)) { | |
throw new Error('KeyError: ' + key) | |
} | |
this._size-- | |
var value = this._map[key] | |
delete this._map[key] | |
return value | |
} | |
Hash.prototype.clear = function() { | |
this._size = 0 | |
this._map = {} | |
} | |
/** | |
* @return {Array.<string>} keys. | |
*/ | |
Hash.prototype.keys = function() { | |
return Object.keys(this._map) | |
} | |
/** | |
* @return {Array.<*>} values. | |
*/ | |
Hash.prototype.values = function() { | |
var keys = this.keys() | |
var values = [] | |
for (var i = 0, l = keys.length; i < l; i++) { | |
values.push(this._map[keys[i]]) | |
} | |
return values | |
} | |
/** | |
* @return {Array.<Array>} [key, value] pairs. | |
*/ | |
Hash.prototype.items = function() { | |
var keys = this.keys() | |
var items = [] | |
for (var i = 0, l = keys.length; i < l; i++) { | |
items.push([keys[i], this._map[keys[i]]]) | |
} | |
return items | |
} | |
/** | |
* @return {Object.<string, *>} key => value object | |
*/ | |
Hash.prototype.toObject = function() { | |
return extend({}, this._map) | |
} | |
/** | |
* @return {number} | |
*/ | |
Hash.prototype.size = function() { | |
return this._size | |
} | |
/** | |
* @constructor | |
* @param {*} value | |
*/ | |
function Entry(value) { | |
this.prev = null | |
this.next = null | |
this.value = value | |
} | |
/** | |
* Based on http://dailyjs.com/2012/09/24/linkedhashmap/ | |
* @constructor | |
* @param {(OrderedObject|Hash|array|object)=} initial | |
*/ | |
var OrderedObject = function OrderedObject(initial) { | |
if (!(this instanceof OrderedObject)) { return new OrderedObject(initial) } | |
this._head = this._tail = null | |
Hash.apply(this, arguments) | |
} | |
inherits(OrderedObject, Hash) | |
/** | |
* @param {string} key | |
* @param {*} value | |
*/ | |
OrderedObject.prototype.put = function(key, value) { | |
if (!this.containsKey(key)) { | |
var entry = new Entry(key) | |
if (this.size() === 0) { | |
this._head = entry | |
this._tail = entry | |
} | |
else { | |
this._tail.next = entry | |
entry.prev = this._tail | |
this._tail = entry | |
} | |
Hash.prototype.put.call(this, key, {value: value, entry: entry}) | |
} | |
else { | |
// Update the stored value directly | |
Hash.prototype.get.call(this, key).value = value | |
} | |
} | |
/** | |
* @param {string} key | |
* @param {*=} defaultValue | |
* @return {*} | |
*/ | |
OrderedObject.prototype.putDefault = function(key, defaultValue) { | |
var value = Hash.prototype.putDefault.apply(this, arguments) | |
return (value !== null && value !== defaultValue ? value.value : value) | |
} | |
/** | |
* @param {(OrderedObject|Hash|array|object)} obj | |
*/ | |
OrderedObject.prototype.update = function(obj) { | |
if (obj instanceof OrderedObject) { | |
for (var cur = obj._head; cur !== null; cur = cur.next) { | |
this.put(cur.value, obj.get(cur.value)) | |
} | |
} | |
else { | |
Hash.prototype.update.apply(this, arguments) | |
} | |
} | |
/** | |
* @param {string} key | |
* @param {*=} defaultValue | |
* @return {*} | |
*/ | |
OrderedObject.prototype.get = function(key, defaultValue) { | |
var value = Hash.prototype.get.apply(this, arguments) | |
return (value !== defaultValue ? value.value : value) | |
} | |
/** | |
* @param {string} key | |
* @param {*=} defaultValue | |
* @return {*} | |
*/ | |
OrderedObject.prototype.pop = function(key, defaultValue) { | |
var value = Hash.prototype.pop.apply(this, arguments) | |
return (value !== defaultValue ? value.value : value) | |
} | |
/** | |
* @return {*} | |
*/ | |
OrderedObject.prototype.remove = function(key) { | |
var value = Hash.prototype.remove.apply(this, arguments) | |
var entry = value.entry | |
if (entry === this._head) { | |
this._head = entry.next | |
this._head.prev = null | |
} | |
else if (entry === this._tail) { | |
this._tail = entry.prev | |
this._tail.next = null | |
} | |
else { | |
entry.prev.next = entry.next | |
entry.next.prev = entry.prev | |
} | |
return value.value | |
} | |
OrderedObject.prototype.clear = function() { | |
this._head = this._tail = null | |
Hash.prototype.clear.apply(this, arguments) | |
} | |
/** | |
* @return {Array.<string>} keys in insertion order. | |
*/ | |
OrderedObject.prototype.keys = function() { | |
var keys = [] | |
for (var cur = this._head; cur !== null; cur = cur.next) { | |
keys.push(cur.value) | |
} | |
return keys | |
} | |
/** | |
* @return {Array.<*>} values in key insertion order. | |
*/ | |
OrderedObject.prototype.values = function() { | |
var values = [] | |
for (var cur = this._head; cur !== null; cur = cur.next) { | |
values.push(this.get(cur.value)) | |
} | |
return values | |
} | |
/** | |
* @return {Array.<Array>} [key, value] pairs in key insertion order. | |
*/ | |
OrderedObject.prototype.items = function() { | |
var items = [] | |
for (var cur = this._head; cur !== null; cur = cur.next) { | |
items.push([cur.value, this.get(cur.value)]) | |
} | |
return items | |
} | |
/** | |
* @return {Object.<string, *>} key => value object with properties added in key | |
* insertion order. | |
*/ | |
OrderedObject.prototype.toObject = function() { | |
var obj = {} | |
for (var cur = this._head; cur !== null; cur = cur.next) { | |
obj[cur.value] = this.get(cur.value) | |
} | |
return obj | |
} | |
return OrderedObject | |
})) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
QUnit.test("OrderedOject", function() { | |
var oo | |
function assertEmpty(oo) { | |
strictEqual(oo._size, 0) | |
deepEqual(oo._map, {}) | |
strictEqual(oo._head, null) | |
strictEqual(oo._tail, null) | |
strictEqual(oo.containsKey('missing'), false) | |
strictEqual(oo.containsValue('missing'), false) | |
throws(oo.get.bind(oo, 'missing')) | |
throws(oo.pop.bind(oo, 'missing')) | |
equal(oo.pop('missing', 'default'), 'default') | |
throws(oo.remove.bind(oo, 'missing')) | |
deepEqual(oo.keys(), []) | |
deepEqual(oo.values(), []) | |
deepEqual(oo.items(), []) | |
deepEqual(oo.toObject(), {}) | |
strictEqual(oo.size(), 0) | |
} | |
// Creation - implicitly tests updating and toObject | |
oo = OrderedObject({test1: 1, test2: 2}) | |
deepEqual(oo.toObject(), {test1: 1, test2: 2}, 'created with object') | |
oo = OrderedObject([['test1', 1], ['test2', 2]]) | |
deepEqual(oo.toObject(), {test1: 1, test2: 2}, 'created with initial pairs') | |
oo = OrderedObject(OrderedObject({test1: 1, test2: 2})) | |
deepEqual(oo.toObject(), {test1: 1, test2: 2}, 'created with initial OrderedObject') | |
var oo = OrderedObject() | |
assertEmpty(oo) | |
// Putting and re-putting to update | |
oo.put('firstName', '0') | |
equal(oo.size(), 1) | |
strictEqual(oo.get('firstName'), '0') | |
strictEqual(oo.putDefault('firstName', '1'), '0', 'putDefault returns existing value if present') | |
strictEqual(oo.get('firstName'), '0', 'existing value not touched after putDefault returned default value') | |
oo.put('firstName', '1') | |
strictEqual(oo.get('firstName'), '1') | |
// Updating existing | |
oo = OrderedObject({firstName: '1'}) | |
oo.update({lastName: '2' , phoneNumber: '3'}) | |
equal(oo.size(), 3) | |
deepEqual(oo.keys(), ['firstName', 'lastName', 'phoneNumber']) | |
deepEqual(oo.values(), ['1', '2', '3']) | |
// Numeric keys should keep insertion order | |
oo.put('123', 'test1') | |
deepEqual(oo.keys(), ['firstName', 'lastName', 'phoneNumber', '123'], 'numeric key in insertion order') | |
oo.remove('phoneNumber') | |
deepEqual(oo.keys(), ['firstName', 'lastName', '123'], 'remove') | |
oo.put('456', 'test2') | |
deepEqual(oo.keys(), ['firstName', 'lastName', '123', '456'], 'numeric key in insertion order') | |
// Popping | |
var defaultArray = [] | |
strictEqual(oo.pop('missing', defaultArray), defaultArray, 'pop returned default for missing key') | |
equal(oo.pop('lastName'), '2', 'pop returned removed value') | |
/// XXX It was about here that I discovered my project's unit tests were wrong and I didn't need an OrderedObject yet after all... | |
throws(function() { oo.pop('no default provided') }) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment