Skip to content
Browse files

Implementing forEach, iterator and toString

  • Loading branch information...
1 parent 87cacd9 commit 96b5eb6dfe1d436679bbc18155b0861c09cbdae1 @eriwen committed Nov 14, 2012
Showing with 209 additions and 45 deletions.
  1. +3 −0 Guardfile
  2. +58 −24 es6-map-shim.js
  3. +148 −21 spec/es6-map-shim-test.js
View
3 Guardfile
@@ -0,0 +1,3 @@
+guard 'shell' do
+ watch(/(spec\/)?(.*).js/) { `make phantom` }
+end
View
82 es6-map-shim.js
@@ -6,7 +6,7 @@
*
* This library assumes ES5 functionality: Object.create, Object.defineProperty, Array.indexOf, Function.bind
*/
-function Map() {
+function Map(iterable) {
var _items = [];
var _keys = [];
var _values = [];
@@ -26,31 +26,66 @@ function Map() {
return i;
};
- // TODO: look at other shims for this
- var MapIterator = function MapIterator(kind) {
- var _m;
- var _itemKind = kind;
+ var MapIterator = function MapIterator(map, kind) {
var _index = 0;
return Object.create({}, {
next: {
value: function() {
// check if index is within bounds
-
+ if (_index < map.items().length) {
+ switch(kind) {
+ case 'keys': return map.keys()[_index++];
+ case 'values': return map.values()[_index++];
+ case 'keys+values': return [].slice.call(map.items()[_index++]);
+ default: throw new TypeError('Invalid iterator type');
+ }
+ }
+ // TODO: make sure I'm interpreting the spec correctly here
+ throw new Error('Stop Iteration');
}
},
iterator: {
- value: this
+ value: function() {
+ return this;
+ }
},
toString: {
value: function() {
- return '[Object Map Iterator]';
+ return '[object Map Iterator]';
}
}
});
};
- // TODO: allow construction with arrays
+ var _set = function(key, value) {
+ // check if key exists and overwrite
+ var index = betterIndexOf.call(_keys, key);
+ if (index > -1) {
+ _items[index] = value;
+ _values[index] = value;
+ } else {
+ _items.push([key, value]);
+ _keys.push(key);
+ _values.push(value);
+ }
+ };
+
+ var setItem = function(item) {
+ if (item.length !== 2) {
+ throw new TypeError('Invalid iterable passed to Map constructor');
+ }
+
+ _set(item[0], item[1]);
+ };
+
+ // FIXME: accommodate any class that defines an @@iterator method that returns
+ // an iterator object that produces two element array-like objects
+ if (Array.isArray(iterable)) {
+ iterable.forEach(setItem);
+ } else if (iterable !== undefined) {
+ throw new TypeError('Invalid Map');
+ }
return Object.create({}, {
items:{
@@ -82,18 +117,7 @@ function Map() {
}
},
set:{
- value:function(key, value) {
- // check if key exists and overwrite
- var index = betterIndexOf.call(_keys, key);
- if (index > -1) {
- _items[index] = value;
- _values[index] = value;
- } else {
- _items.push([key, value]);
- _keys.push(key);
- _values.push(value);
- }
- }
+ value: _set
},
size:{
get:function() {
@@ -118,12 +142,22 @@ function Map() {
}
},
forEach:{
- value:function(callbackfn/*, thisArg*/) {
- // TODO: implement me
+ value:function(callbackfn /*, thisArg*/) {
+ if (typeof callbackfn != 'function') {
+ throw new TypeError('Invalid callback function given to forEach');
+ }
+
+ for (var i = 0; i < _keys.length; i++) {
+ if (this.has(_keys[i])) {
+ callbackfn.apply(arguments[1], [_values[i], _keys[i], this]);
+ }
+ }
}
},
iterator:{
- value: new MapIterator(this, 'keys+values')
+ value: function() {
+ return new MapIterator(this, 'keys+values');
+ }
},
toString:{
value: function() {
View
169 spec/es6-map-shim-test.js
@@ -1,5 +1,29 @@
describe('es6-map-shim', function() {
- // TODO: test constructor
+ describe('Map::constructor', function() {
+ it('should allow empty constructor args', function() {
+ expect(function() { new Map(); }).not.toThrow();
+ expect(function() { new Map([]); }).not.toThrow();
+ });
+
+ it('should populate items, keys, and values given a valid iterable', function() {
+ var iterable = [['foo', undefined], [42, {}]];
+ var map = new Map(iterable);
+ expect(map.items()).toEqual(iterable);
+ expect(map.keys()).toEqual(['foo', 42]);
+ expect(map.values()).toEqual([undefined, {}]);
+ });
+
+ it('should overwrite items with duplicate keys', function() {
+ var map = new Map([['foo', 'bar'], ['foo', Infinity]]);
+ expect(map.get('foo')).toEqual(Infinity);
+ expect(map.size).toEqual(1);
+ });
+
+ it('should throw a TypeError given an invalid iterable', function() {
+ expect(function() { new Map([[0]]); }).toThrow(new TypeError('Invalid iterable passed to Map constructor'));
+ expect(function() { new Map('foo'); }).toThrow(new TypeError('Invalid Map'));
+ });
+ });
describe('Map::items', function() {
it('should have empty items Array if Map was constructed with no arguments', function() {
@@ -16,7 +40,13 @@ describe('es6-map-shim', function() {
expect(map.items()).toEqual([[0, 5]]);
});
- // TODO: test to make sure keys, values, items stay in sync
+ it('should return a copy of items, not a reference to the Map items object', function() {
+ var map = new Map([['foo', 'bar'], [{}, 42]]);
+ expect(map.items()).toEqual([['foo', 'bar'], [{}, 42]]);
+ var keese = map.items();
+ keese = [['baz', 3]];
+ expect(map.items()).toEqual([['foo', 'bar'], [{}, 42]]);
+ });
});
describe('Map::keys', function() {
@@ -33,6 +63,14 @@ describe('es6-map-shim', function() {
map.delete('foo');
expect(map.keys()).toEqual([0]);
});
+
+ it('should return a copy of keys, not a reference to the Map keys object', function() {
+ var map = new Map([['foo', 'bar'], [{}, 42]]);
+ expect(map.keys()).toEqual(['foo', {}]);
+ var keese = map.keys();
+ keese = ['baz', 3];
+ expect(map.keys()).toEqual(['foo', {}]);
+ });
});
describe('Map::values', function() {
@@ -42,13 +80,19 @@ describe('es6-map-shim', function() {
});
it('should contain all values that have been added but not deleted', function() {
- var map = new Map();
- map.set(0, 5);
- map.set('foo', 'bar');
+ var map = new Map([[0, 5], ['foo', 'bar']]);
expect(map.values()).toEqual([5, 'bar']);
map.delete('foo');
expect(map.values()).toEqual([5]);
});
+
+ it('should return a copy of values, not a reference to the Map values object', function() {
+ var map = new Map([['foo', 'bar'], [{}, 42]]);
+ expect(map.values()).toEqual(['bar', 42]);
+ var vals = map.values();
+ vals = ['baz', 3];
+ expect(map.values()).toEqual(['bar', 42]);
+ });
});
describe('Map::has', function() {
@@ -58,12 +102,19 @@ describe('es6-map-shim', function() {
});
it('should return true given an existing key', function() {
- var map = new Map();
- map.set('foo', 'bar');
+ var map = new Map([['foo', 'bar']]);
expect(map.has('foo')).toBe(true);
});
- // TODO: edge cases with null, NaN etc.
+ it('should handle "interesting" keys', function() {
+ var obj = {'baz': []};
+ var map = new Map([['foo', 'bar'], [null, 42], [obj, function(){}], [-0, 'negative zero'], [0, 'zero']])
+ expect(map.has(null)).toBe(true);
+ expect(map.has(undefined)).toBe(false);
+ expect(map.has(obj)).toBe(true);
+ expect(map.has(-0)).toBe(true);
+ expect(map.has(0)).toBe(true);
+ });
});
describe('Map::get', function() {
@@ -73,11 +124,20 @@ describe('es6-map-shim', function() {
});
it('should return object value given an existing key', function() {
- var map = new Map();
- map.set('foo', 'bar');
+ var map = new Map([['foo', 'bar']]);
expect(map.get('foo')).toEqual('bar');
});
- // TODO: edge cases with null, NaN etc.
+
+ it('should handle "interesting" keys', function() {
+ var obj = {'baz': []};
+ var fn = function(){};
+ var map = new Map([['foo', 'bar'], [undefined, 42], [obj, fn], [-0, 'negative zero'], [0, 'zero']])
+ expect(map.get(null)).toBe(undefined);
+ expect(map.get(undefined)).toBe(42);
+ expect(map.get(obj)).toBe(fn);
+ expect(map.get(-0)).toBe('negative zero');
+ expect(map.get(0)).toBe('zero');
+ });
});
describe('Map::set', function() {
@@ -88,6 +148,14 @@ describe('es6-map-shim', function() {
expect(map.values()).toEqual(['bar']);
expect(map.items()).toEqual([['foo', 'bar']]);
});
+
+ it('should add entry for undefined if arguments are undefined', function() {
+ var map = new Map();
+ map.set();
+ expect(map.keys()).toEqual([undefined]);
+ expect(map.values()).toEqual([undefined]);
+ expect(map.items()).toEqual([[undefined, undefined]]);
+ });
});
describe('Map.size', function() {
@@ -97,10 +165,7 @@ describe('es6-map-shim', function() {
});
it('should return the length of items()', function() {
- var map = new Map();
- map.set(function(){}, 'foo');
- map.set([], 'bar');
- map.set(3, 'baz');
+ var map = new Map([[function(){}, 'foo'], [[], 'bar'], [3, 'baz']]);
expect(map.items().length).toEqual(3);
expect(map.size).toEqual(3);
})
@@ -113,17 +178,15 @@ describe('es6-map-shim', function() {
});
it('should return true and remove item given existing key', function() {
- var map = new Map();
- map.set('foo', 'bar');
+ var map = new Map([['foo', 'bar']]);
expect(map.delete('foo')).toBe(true);
expect(map.items()).toEqual([]);
});
});
describe('Map::clear', function() {
it('should remove all items', function() {
- var map = new Map();
- map.set('foo', 'bar');
+ var map = new Map([['foo', 'bar']]);
map.set(5, 43);
expect(map.keys().length).toEqual(2);
map.clear();
@@ -139,12 +202,76 @@ describe('es6-map-shim', function() {
});
describe('Map::forEach', function() {
- it('should...', function() {
+ it('should throw TypeError given non-callable argument', function() {
+ expect(function(){ new Map().forEach([]); }).toThrow(new TypeError('Invalid callback function given to forEach'));
+ });
+
+ it('should operate on each value and key', function() {
+ var map = new Map([['foo', 'bar'], [65, 42]]);
+ var sums = [];
+ map.forEach(function(value, key, mapRef) {
+ sums.push(value + key);
+ });
+ expect(sums[0]).toEqual('barfoo');
+ expect(sums[1]).toEqual(107);
+ });
+
+ it('should allow context to be set', function() {
+ var map = new Map([['foo', 'bar'], [65, 42]]);
+ var sums = [];
+ map.forEach(function(value, key, mapRef) {
+ if (typeof this[key] == 'function') {
+ sums.push(value + key);
+ }
+ }, {'foo': function(){}});
+ expect(sums.length).toBe(1);
+ expect(sums[0]).toEqual('barfoo');
+ });
+
+ it('should process new items added', function() {
+ var map = new Map([['foo', 'bar'], [65, 42]]);
+ var sums = [];
+ map.forEach(function(value, key, mapRef) {
+ // Add inverse item [value, key]
+ if (!mapRef.has(value)) {
+ mapRef.set(value, key);
+ }
+ sums.push(value + key);
+ }, {'foo': function(){}});
+ expect(sums.length).toBe(4);
+ expect(sums[0]).toEqual('barfoo');
+ expect(sums[1]).toEqual(107);
+ expect(sums[2]).toEqual('foobar');
+ expect(sums[3]).toEqual(107);
+ });
+
+ it('should skip items that are deleted before being visited', function() {
+ var map = new Map([['foo', 'bar'], [65, 42]]);
+ var sums = [];
+ map.forEach(function(value, key, mapRef) {
+ mapRef.delete(65);
+ sums.push(value + key);
+ }, {'foo': function(){}});
+ expect(sums.length).toBe(1);
+ expect(sums[0]).toEqual('barfoo');
});
});
describe('Map::iterator', function() {
- it('should...', function() {
+ it('should return an iterator object that responds to next(), iterator(), and toString()', function() {
+ var map = new Map([['foo', 'bar']]);
+ var iterator = map.iterator();
+ expect(typeof iterator.next).toBe('function');
+ expect(typeof iterator.iterator).toBe('function');
+ expect(iterator.toString()).toBe('[object Map Iterator]');
+ });
+
+ it('should yield items as requested', function() {
+ var map = new Map([['foo', 'bar'], [undefined, 42]]);
+ var iterator = map.iterator();
+ expect(iterator.next()).toEqual(['foo', 'bar']);
+ expect(iterator.next()).toEqual([undefined, 42]);
+ expect(function(){ iterator.next() }).toThrow(new Error('Stop Iteration'));
});
});

0 comments on commit 96b5eb6

Please sign in to comment.
Something went wrong with that request. Please try again.