diff --git a/README.md b/README.md
index b1e4f83..0e34a51 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,21 @@ The Array-like API also includes `every`, `some` and `forEach`. On the other han
The `forEach`, `every` and `some` functions are reducers and take a continuation callback, like `reduce` (see example further down).
+Note: the `filter`, `every` and `some` methods can also be controlled by a mongodb filter condition rather than a function. The following are equivalent:
+
+``` javascript
+// filter expressed as a function
+reader = numberReader(1000).filter(function(_, n) {
+ return n >= 10 && n < 20;
+});
+
+// mongo-style filter
+reader = numberReader(1000).filter({
+ $gte: 10,
+ $lt: 20,
+});
+```
+
## Pipe
@@ -197,6 +212,8 @@ infiniteReader().until(function(_, n) {
}).pipe(_, ez.devices.console.log);
```
+Note: `while` and `until` conditions can also be expressed as mongodb conditions.
+
## Transformations
diff --git a/lib/predicate._js b/lib/predicate._js
new file mode 100644
index 0000000..cdeb671
--- /dev/null
+++ b/lib/predicate._js
@@ -0,0 +1,137 @@
+"use strict";
+
+function pfalse(_, obj) {
+ return false;
+}
+
+function ptrue(_, obj) {
+ return true;
+}
+
+var ops = {
+ $eq: function(val) {
+ return function(_, v) {
+ return v == val;
+ };
+ },
+ $ne: function(val) {
+ return function(_, v) {
+ return v != val;
+ };
+ },
+ $gt: function(val) {
+ return function(_, v) {
+ return v > val;
+ };
+ },
+ $gte: function(val) {
+ return function(_, v) {
+ return v >= val;
+ };
+ },
+ $lt: function(val) {
+ return function(_, v) {
+ return v < val;
+ };
+ },
+ $lte: function(val) {
+ return function(_, v) {
+ return v <= val;
+ };
+ },
+ $in: function(val) {
+ return function(_, v) {
+ return val.indexOf(v) >= 0;
+ }
+ },
+ $nin: function(val) {
+ return function(_, v) {
+ return val.indexOf(v) < 0;
+ }
+ },
+ $and: function(val) {
+ return and(val.map(exports.predicate));
+ },
+ $or: function(val) {
+ return or(val.map(exports.predicate));
+ },
+ $nor: function(val) {
+ return not(or(val.map(exports.predicate)));
+ },
+ $not: function(val) {
+ return not(exports.predicate(val));
+ },
+}
+
+function re_test(re) {
+ return function(_, val) {
+ return re.test(val);
+ }
+}
+
+function not(predicate) {
+ return function(_, obj) {
+ return !predicate(_, obj);
+ }
+}
+
+function or(predicates) {
+ if (predicates.length === 0) return pfalse;
+ if (predicates.length === 1) return predicates[0];
+ return function(_, obj) {
+ return predicates.some_(_, function(_, predicate) {
+ return predicate(_, obj);
+ });
+ }
+}
+
+function and(predicates) {
+ if (predicates.length === 0) return ptrue;
+ if (predicates.length === 1) return predicates[0];
+ return function(_, obj) {
+ return predicates.every_(_, function(_, predicate) {
+ return predicate(_, obj);
+ });
+ }
+}
+
+function compose(f, g) {
+ return function(_, obj) {
+ return f(_, g(_, obj));
+ }
+}
+
+function deref(key) {
+ return function(_, obj) {
+ if (obj == null) return undefined;
+ var v = obj[key];
+ return typeof v === "function" ? v(_) : v;
+ }
+}
+
+function walk(p) {
+ var i = p.indexOf('.');
+ if (i >= 0) {
+ return compose(walk(p.substring(i + 1)), walk(p.substring(0, i)));
+ } else {
+ return deref(p);
+ }
+}
+
+exports.predicate = function(val) {
+ if (val instanceof RegExp) {
+ return re_test(val);
+ } else if (typeof val === "object" && val) {
+ return and(Object.keys(val).map(function(k) {
+ var v = val[k];
+ if (k[0] === '$') {
+ if (!ops[k]) throw new Error("bad operator: " + k);
+ return ops[k](v);
+ } else {
+ return compose(exports.predicate(v), walk(k));
+ }
+ }));
+ } else {
+ return ops.$eq(val);
+ }
+};
\ No newline at end of file
diff --git a/lib/reader._js b/lib/reader._js
index fa86891..62fefb6 100644
--- a/lib/reader._js
+++ b/lib/reader._js
@@ -30,6 +30,8 @@
///
var streams = require('streamline-streams/lib/streams');
var flows = require('streamline/lib/util/flows');
+var predicate = require('./predicate').predicate;
+
var generic;
var Decorated = function Decorated(read) {
@@ -79,6 +81,7 @@ exports.decorate = function(proto) {
/// Stops streaming and returns false as soon as `fn` returns false on an entry.
proto.every = function(_, fn, thisObj) {
thisObj = thisObj !== undefined ? thisObj : this;
+ if (typeof fn !== 'function') fn = predicate(fn);
var self = this;
while (true) {
var val = self.read(_);
@@ -94,6 +97,7 @@ exports.decorate = function(proto) {
/// Stops streaming and returns true as soon as `fn` returns true on an entry.
proto.some = function(_, fn, thisObj) {
thisObj = thisObj !== undefined ? thisObj : this;
+ if (typeof fn !== 'function') fn = predicate(fn);
var self = this;
while (true) {
var val = self.read(_);
@@ -166,6 +170,7 @@ exports.decorate = function(proto) {
/// Returns another stream on which other operations may be chained.
proto.filter = function(fn, thisObj) {
thisObj = thisObj !== undefined ? thisObj : this;
+ if (typeof fn !== 'function') fn = predicate(fn);
return this.transform(function(_, reader, writer) {
for (var i = 0, val;
(val = reader.read(_)) !== undefined; i++) {
@@ -180,6 +185,7 @@ exports.decorate = function(proto) {
/// Returns another stream on which other operations may be chained.
proto.until = function(fn, thisObj) {
thisObj = thisObj !== undefined ? thisObj : this;
+ if (typeof fn !== 'function') fn = predicate(fn);
return this.transform(function(_, reader, writer) {
for (var i = 0, val;
(val = reader.read(_)) !== undefined; i++) {
@@ -197,6 +203,7 @@ exports.decorate = function(proto) {
/// Returns another stream on which other operations may be chained.
proto.
while = function(fn, thisObj) {
+ if (typeof fn !== 'function') fn = predicate(fn);
return this.until(function(_, val, i) {
return !fn.call(thisObj, _, val, i);
}, thisObj);
diff --git a/test/common/predicate-test._js b/test/common/predicate-test._js
new file mode 100644
index 0000000..f72dd76
--- /dev/null
+++ b/test/common/predicate-test._js
@@ -0,0 +1,317 @@
+"use strict";
+QUnit.module(module.id);
+
+var predicate = require("ez-streams/lib/predicate").predicate;
+
+function t(_, pred, obj, result) {
+ equals(predicate(pred)(_, obj), result, JSON.stringify(pred) + " with " + JSON.stringify(obj) + " => " + result);
+}
+
+asyncTest("direct values", 6, function(_) {
+ t(_, 5, 5, true);
+ t(_, 5, 6, false);
+ t(_, 'a', 'a', true);
+ t(_, 'a', 'aa', false);
+ t(_, true, true, true);
+ t(_, true, false, false);
+ start();
+});
+
+asyncTest("gt", 3, function(_) {
+ t(_, {
+ $gt: 4,
+ }, 5, true);
+
+ t(_, {
+ $gt: 5,
+ }, 5, false);
+
+ t(_, {
+ $gt: 6,
+ }, 5, false);
+
+ start();
+});
+
+asyncTest("gte", 3, function(_) {
+ t(_, {
+ $gte: 4,
+ }, 5, true);
+
+ t(_, {
+ $gte: 5,
+ }, 5, true);
+
+ t(_, {
+ $gte: 6,
+ }, 5, false);
+
+ start();
+});
+
+asyncTest("lt", 3, function(_) {
+ t(_, {
+ $lt: 4,
+ }, 5, false);
+
+ t(_, {
+ $lt: 5,
+ }, 5, false);
+
+ t(_, {
+ $lt: 6,
+ }, 5, true);
+
+ start();
+});
+
+asyncTest("lte", 3, function(_) {
+ t(_, {
+ $lte: 4,
+ }, 5, false);
+
+ t(_, {
+ $lte: 5,
+ }, 5, true);
+
+ t(_, {
+ $lte: 6,
+ }, 5, true);
+
+ start();
+});
+
+asyncTest("ne", 3, function(_) {
+ t(_, {
+ $ne: 4,
+ }, 5, true);
+
+ t(_, {
+ $ne: 5,
+ }, 5, false);
+
+ t(_, {
+ $ne: 6,
+ }, 5, true);
+
+
+ start();
+});
+
+asyncTest("range", 3, function(_) {
+ t(_, {
+ $gte: 3,
+ $lte: 7,
+ }, 2, false);
+
+ t(_, {
+ $gte: 3,
+ $lte: 7,
+ }, 5, true);
+
+ t(_, {
+ $gte: 3,
+ $lte: 7,
+ }, 8, false);
+
+ start();
+});
+
+asyncTest("regexp", 2, function(_) {
+ t(_, /^hel/, 'hello', true);
+ t(_, /^hel/, 'world', false);
+
+ start();
+});
+
+asyncTest("and", 2, function(_) {
+ t(_, {
+ $and: [2, 5],
+ }, 5, false);
+
+ t(_, {
+ $and: [5, 5],
+ }, 5, true);
+
+ start();
+});
+
+asyncTest("or", 2, function(_) {
+ t(_, {
+ $or: [2, 5],
+ }, 5, true);
+
+ t(_, {
+ $or: [2, 6],
+ }, 5, false);
+
+ start();
+});
+
+asyncTest("nor", 2, function(_) {
+ t(_, {
+ $nor: [2, 5],
+ }, 5, false);
+
+ t(_, {
+ $nor: [2, 6],
+ }, 5, true);
+
+ start();
+});
+
+asyncTest("not", 2, function(_) {
+ t(_, {
+ $not: {
+ $gt: 2
+ },
+ }, 5, false);
+
+ t(_, {
+ $not: {
+ $lt: 2
+ },
+ }, 5, true);
+
+ start();
+});
+
+asyncTest("in", 3, function(_) {
+ t(_, {
+ $in: [2, 3, 5]
+ }, 3, true);
+
+ t(_, {
+ $in: [2, 3, 5]
+ }, 4, false);
+
+ t(_, {
+ $in: [2, 3, 5]
+ }, 5, true);
+
+ start();
+});
+
+asyncTest("not in", 3, function(_) {
+ t(_, {
+ $nin: [2, 3, 5]
+ }, 3, false);
+
+ t(_, {
+ $nin: [2, 3, 5]
+ }, 4, true);
+
+ t(_, {
+ $nin: [2, 3, 5]
+ }, 5, false);
+
+ start();
+});
+
+asyncTest("empty and", 2, function(_) {
+ t(_, {}, {}, true);
+
+ t(_, {}, {
+ a: 5,
+ }, true);
+
+ start();
+});
+
+asyncTest("empty or", 2, function(_) {
+ t(_, {
+ $or: []
+ }, {}, false);
+
+ t(_, {
+ $or: []
+ }, {
+ a: 5,
+ }, false);
+
+ start();
+});
+
+asyncTest("single property", 2, function(_) {
+ t(_, {
+ a: 5,
+ }, {
+ a: 5,
+ b: 3,
+ }, true);
+
+ t(_, {
+ a: 6,
+ }, {
+ a: 5,
+ b: 3,
+ }, false);
+ start();
+});
+
+asyncTest("implicit and (multiple properties)", 2, function(_) {
+ t(_, {
+ a: 5,
+ b: 3,
+ }, {
+ a: 5,
+ b: 3,
+ }, true);
+
+ t(_, {
+ a: 5,
+ b: 3,
+ }, {
+ a: 5,
+ }, false);
+
+ start();
+});
+
+asyncTest("walk", 5, function(_) {
+ t(_, {
+ 'a.b': /^hel/,
+ }, {
+ a: {
+ b: 'hello',
+ }
+ }, true);
+
+ t(_, {
+ 'a.b': /^hel/,
+ }, {
+ a: {
+ c: 'hello',
+ }
+ }, false);
+
+ t(_, {
+ 'a.c': /^hel/,
+ }, {
+ b: {
+ c: 'hello',
+ }
+ }, false);
+
+ t(_, {
+ 'a.b.c': /^hel/,
+ }, {
+ a: {
+ b: {
+ c: 'hello',
+ }
+ }
+ }, true);
+
+ t(_, {
+ 'a.b.c': /^hel/,
+ }, {
+ a: {
+ b: {
+ c: 'world',
+ }
+ }
+ }, false);
+
+ start();
+});
\ No newline at end of file
diff --git a/test/server/api-test._js b/test/server/api-test._js
index 24b2cd9..edcdfb8 100644
--- a/test/server/api-test._js
+++ b/test/server/api-test._js
@@ -35,7 +35,7 @@ asyncTest("map", 1, function(_) {
start();
});
-asyncTest("every", 3, function(_) {
+asyncTest("every", 6, function(_) {
strictEqual(numbers(5).every(_, function(_, num) {
return num < 5;
}), true);
@@ -45,10 +45,19 @@ asyncTest("every", 3, function(_) {
strictEqual(numbers(5).every(_, function(_, num) {
return num != 2;
}), false);
+ strictEqual(numbers(5).every(_, {
+ $lt: 5,
+ }), true);
+ strictEqual(numbers(5).every(_, {
+ $lt: 4,
+ }), false);
+ strictEqual(numbers(5).every(_, {
+ $ne: 2,
+ }), false);
start();
});
-asyncTest("some", 3, function(_) {
+asyncTest("some", 6, function(_) {
strictEqual(numbers(5).some(_, function(_, num) {
return num >= 5;
}), false);
@@ -58,6 +67,15 @@ asyncTest("some", 3, function(_) {
strictEqual(numbers(5).some(_, function(_, num) {
return num != 2;
}), true);
+ strictEqual(numbers(5).some(_, {
+ $gte: 5,
+ }), false);
+ strictEqual(numbers(5).some(_, {
+ $gte: 4,
+ }), true);
+ strictEqual(numbers(5).some(_, {
+ $ne: 2,
+ }), true);
start();
});
@@ -114,24 +132,34 @@ asyncTest("transform - less reads than writes", 1, function(_) {
start();
});
-asyncTest("filter", 1, function(_) {
+asyncTest("filter", 2, function(_) {
strictEqual(numbers(10).filter(function(_, val) {
return val % 2;
}).pipe(_, arraySink()).toArray().join(','), "1,3,5,7,9");
+ strictEqual(numbers(10).filter({
+ $gt: 2,
+ $lt: 6,
+ }).pipe(_, arraySink()).toArray().join(','), "3,4,5");
start();
});
-asyncTest("while", 1, function(_) {
+asyncTest("while", 2, function(_) {
strictEqual(numbers().while(function(_, val) {
return val < 5;
}).pipe(_, arraySink()).toArray().join(','), "0,1,2,3,4");
+ strictEqual(numbers().while({
+ $lt: 5,
+ }).pipe(_, arraySink()).toArray().join(','), "0,1,2,3,4");
start();
});
-asyncTest("until", 1, function(_) {
+asyncTest("until", 2, function(_) {
strictEqual(numbers().until(function(_, val) {
return val > 5;
}).pipe(_, arraySink()).toArray().join(','), "0,1,2,3,4,5");
+ strictEqual(numbers().until({
+ $gt: 5,
+ }).pipe(_, arraySink()).toArray().join(','), "0,1,2,3,4,5");
start();
});