Skip to content

Instantly share code, notes, and snippets.

@poetix
Created December 27, 2011 22:28
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save poetix/1525363 to your computer and use it in GitHub Desktop.
Save poetix/1525363 to your computer and use it in GitHub Desktop.
Lazy map, filter and reduce in Javascript
Iterators = (function() {
var each, map, filter, reduce, toArray, toObject;
function _map(iter, f) {
return {
hasNext: iter.hasNext,
next: function() { return f(iter.next()); }
};
}
function _each(iter, f) {
while (iter.hasNext()) {
f(iter.next());
}
}
function _toArray(iter) {
var array = [];
_each(iter, function(item) { array.push(item); });
return array;
}
function _toObject(iter) {
var object = {};
_each(iter, function(item) {
object[item.key] = item.value;
});
return object;
}
function peekable(iter) {
var buffer = undefined;
if (iter.peek) { return iter; }
return {
hasNext: function() {
if (buffer === undefined) {
return iter.hasNext();
}
return true;
},
next: function() {
var result;
if (buffer === undefined) {
result = iter.next();
} else {
result = buffer;
}
buffer = undefined;
return result;
},
peek: function() {
if (buffer === undefined) {
buffer = iter.next();
}
return buffer;
}
};
}
function _filter(iter, p) {
var peekableIter = peekable(iter);
function hasNext() {
while(peekableIter.hasNext()) {
if (p(peekableIter.peek())) {
return true;
}
peekableIter.next();
}
}
return {
hasNext: hasNext,
next: function() {
if (hasNext()) {
return peekableIter.next();
}
}
};
}
function _reduce(iter, f, seed) {
var result = seed === undefined ? iter.next() : seed;
while (iter.hasNext()) {
result = f(result, iter.next());
}
return result;
}
function arrayIterator(array) {
var i = 0;
return {
hasNext: function() { return i < array.length; },
next: function() { return array[i++]; }
};
}
function keyIterator(object) {
var key = null, keys = [];
for (key in object) {
if (object.hasOwnProperty(key)) { keys.push(key); }
}
return arrayIterator(keys);
}
function valueIterator(object) {
return _map(keyIterator(object), function(key) { return object[key]; });
}
function objectIterator(object) {
return _map(keyIterator(object), function(key) {
return { key: key, value: object[key] };
});
}
function toIterator(arg) {
if (arg.hasNext && arg.next) { return arg; }
if (arg instanceof Array) {
return arrayIterator(arg);
}
if (typeof arg === 'object') {
return objectIterator(arg);
}
throw "Cannot convert argument to iterator: " + arg;
}
function convertArgToIterator(f) {
return function() {
var args = arguments;
args[0] = toIterator(args[0]);
return f.apply(f, args);
};
}
each = convertArgToIterator(_each);
map = convertArgToIterator(_map);
filter = convertArgToIterator(_filter);
reduce = convertArgToIterator(_reduce);
toArray = convertArgToIterator(_toArray);
toObject = convertArgToIterator(_toObject);
return {
each: each,
map: map,
filter: filter,
reduce: reduce,
keyIterator: keyIterator,
valueIterator: valueIterator,
toIterator: toIterator,
toArray: toArray,
toObject: toObject,
project: function(value, propertyName) {
return map(value, function(item) { return item[propertyName]; });
}
};
}());
var _ = Iterators;
describe("Iterators.keyIterator", function() {
it("should return an iterator over an object's keys", function() {
var input = { foo: "bar", baz: "xyzzy" },
iter = _.keyIterator(input);
expect(iter.next()).toEqual("foo");
expect(iter.next()).toEqual("baz");
});
});
describe("Iterators.valueIterator", function() {
it("should return an iterator over an object's values", function() {
var input = { foo: "bar", baz: "xyzzy" },
iter = _.valueIterator(input);
expect(iter.next()).toEqual("bar");
expect(iter.next()).toEqual("xyzzy");
});
});
describe("Iterators.toIterator", function() {
it("should return an iterator verbatim", function() {
var iter = _.toIterator([1, 2, 3]);
expect(_.toIterator(iter)).toEqual(iter);
});
it("should convert an array to an array iterator", function() {
var input = [1, 2, 3],
iter = _.toIterator(input);
expect(iter.next()).toEqual(1);
expect(iter.next()).toEqual(2);
expect(iter.next()).toEqual(3);
});
it("should convert an object to an entry iterator", function() {
var input = { foo: "bar", baz: "xyzzy" },
iter = _.toIterator(input);
expect(iter.next()).toEqual({key: "foo", value: "bar"});
expect(iter.next()).toEqual({key: "baz", value: "xyzzy"});
});
});
describe("Iterators.toArray", function() {
it("should convert an iterator into an array", function() {
var input = [1, 2, 3],
iter = _.toIterator(input);
expect(_.toArray(iter)).toEqual(input);
});
});
describe("Iterators.map", function() {
it("should return a lazy iterator", function() {
var input = [1, 2, 3],
f = jasmine.createSpy('map function'),
iter = _.map(input, f);
expect(f).wasNotCalled();
iter.next();
expect(f).toHaveBeenCalledWith(1);
});
it("should apply the supplied function to each element in the iterator", function() {
var input = [1, 2, 3];
expect(_.toArray(_.map(input, function(n) { return n * 2; }))).toEqual([2, 4, 6]);
});
});
describe("Iterators.project", function() {
it("should perform a map projecting the supplied property", function() {
var input = [{x: 1, y: 10},
{x: 2, y: 20}];
expect(_.toArray(_.project(input, "x"))).toEqual([1, 2]);
expect(_.toArray(_.project(input, "y"))).toEqual([10, 20]);
});
});
describe("Iterators.filter", function() {
it("should return a lazy iterator", function() {
var input = [1, 2, 3],
p = jasmine.createSpy('predicate'),
iter = _.filter(input, p);
expect(p).wasNotCalled();
iter.hasNext();
expect(p).toHaveBeenCalledWith(1);
});
it("should return an iterator over the elements matched by the supplied predicate", function() {
var input = [1, 2, 3, 4, 5, 6],
isEven = function(number) { return ((number >> 1) << 1) === number; };
expect(_.toArray(_.filter(input, isEven))).toEqual([2, 4, 6]);
});
});
describe("Iterators.toObject", function() {
it("should convert an entry iterator into an object", function() {
var input = { foo: "bar", baz: "xyzzy" },
iter = _.toIterator(input);
expect(_.toObject(iter)).toEqual(input);
});
});
describe("Iterators.reduce", function() {
it("should apply the reduction using the first element of the iterator as seed value " +
"if none is supplied", function() {
var input = [1, 2, 3],
sum = function(a, b) { return a + b; };
expect(_.reduce(input, sum)).toEqual(6);
});
it("should use the given seed value if it is supplied", function() {
var input = [1, 2, 3],
sum = function(a, b) { return a + b; };
expect(_.reduce(input, sum, 4)).toEqual(10);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment