Repository: rtfeldman/seamless-immutable
Pull request #93 (2016-01-14 10:54:42)

Implement .without(predicate) - see #87
Exclude all keys/values that satisfy the predicate

Simplify the test by testing primarily 
without(predicate).

PS: I recommend to also run `git co e09e798 test/ImmutableObject/test-without.js; grunt`
to verify that the old tests before my refactoring still pass (they do for me but you should
not trust me :-)) 


Patch:
diff --git a/README.md b/README.md
index 2498872..38ec319 100644
--- a/README.md
+++ b/README.md
@@ -210,9 +210,14 @@ Immutable({the: "forests", will: "echo", with: "laughter"}).without(["will", "wi
 
 Immutable({the: "forests", will: "echo", with: "laughter"}).without("will", "with")
 // returns Immutable({the: "forests"})
+
+
+Immutable({the: "forests", will: "echo", with: "laughter"}).without((value, key) => key === "the" || value === "echo")
+// returns Immutable({with: "laughter"})
 ```
 
-Returns an Immutable Object excluding the given keys from the existing object.
+Returns an Immutable Object excluding the given keys or keys/values satisfying
+the given predicate from the existing object.
 
 Multiple keys can be provided, either in an Array or as extra arguments.
 
@@ -319,6 +324,10 @@ Adds `#asMutable`
 
 Initial stable release
 
+## Development
+
+Run `npm install -g grunt-cli`, `npm install` and then `grunt` to build and test it.
+
 [1]: https://secure.travis-ci.org/rtfeldman/seamless-immutable.svg
 [2]: https://travis-ci.org/rtfeldman/seamless-immutable
 [3]: https://badge.fury.io/js/seamless-immutable.svg
diff --git a/seamless-immutable.development.js b/seamless-immutable.development.js
index cd6a480..5c1698b 100644
--- a/seamless-immutable.development.js
+++ b/seamless-immutable.development.js
@@ -209,15 +209,21 @@
       return this;
     }
 
-    // If we weren't given an array, use the arguments list.
-    if (!(keysToRemove instanceof Array)) {
-      keysToRemove = Array.prototype.slice.call(arguments);
+    var remove;
+    if (typeof keysToRemove === "function") {
+      // The argument is a predicate.
+      remove = keysToRemove;
+    } else {
+      // If we weren't given an array, use the arguments list.
+      var keysToRemoveArray = (keysToRemove instanceof Array) ?
+         keysToRemove : Array.prototype.slice.call(arguments);
+      remove = function(val, key) { return keysToRemoveArray.indexOf(key) >= 0; }
     }
 
     var result = this.instantiateEmptyObject();
 
     for (var key in this) {
-      if (this.hasOwnProperty(key) && (keysToRemove.indexOf(key) === -1)) {
+      if (this.hasOwnProperty(key) && ! remove(this[key], key)) {
         result[key] = this[key];
       }
     }
diff --git a/seamless-immutable.production.min.js b/seamless-immutable.production.min.js
index a7688ac..210a946 100644
--- a/seamless-immutable.production.min.js
+++ b/seamless-immutable.production.min.js
@@ -1,1 +1,1 @@
-!function(){"use strict";function a(a,b,c){Object.defineProperty(a,b,{enumerable:!1,configurable:!1,writable:!1,value:c})}function b(b){a(b,z,!0)}function c(a){return"object"==typeof a?null===a||a.hasOwnProperty(z):!0}function d(a){return!(null===a||"object"!=typeof a||a instanceof Array||a instanceof Date)}function e(a){var b=new Error(a);return b.__proto__=e,b}function f(a,c){b(a);return a}function g(b,c){var d=b[c];a(b,c,function(){return y(d.apply(b,arguments))})}function h(a,b){if(a in this&&this[a]===b)return this;var c=o.call(this);return c[a]=y(b),j(c)}function i(a,b){var c=a[0];if(1===a.length)return h.call(this,c,b);var d,e=a.slice(1),f=this[c];if(d="object"==typeof f&&null!==f&&"function"==typeof f.setIn?f.setIn(e,b):i.call(F,e,b),c in this&&f===d)return this;var g=o.call(this);return g[c]=d,j(g)}function j(b){for(var c in D)if(D.hasOwnProperty(c)){var d=D[c];g(b,d)}a(b,"flatMap",m),a(b,"asObject",p),a(b,"asMutable",o),a(b,"set",h),a(b,"setIn",i);for(var e=0,j=b.length;j>e;e++)b[e]=y(b[e]);return f(b,C)}function k(b){return a(b,"asMutable",l),f(b,E)}function l(){return new Date(this.getTime())}function m(a){if(0===arguments.length)return this;var b,c=[],d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this);e instanceof Array?c.push.apply(c,e):c.push(e)}return j(c)}function n(a){if(0===arguments.length)return this;a instanceof Array||(a=Array.prototype.slice.call(arguments));var b=this.instantiateEmptyObject();for(var c in this)this.hasOwnProperty(c)&&-1===a.indexOf(c)&&(b[c]=this[c]);return x(b,{instantiateEmptyObject:this.instantiateEmptyObject})}function o(a){var b,c,d=[];if(a&&a.deep)for(b=0,c=this.length;c>b;b++)d.push(q(this[b]));else for(b=0,c=this.length;c>b;b++)d.push(this[b]);return d}function p(a){"function"!=typeof a&&(a=function(a){return a});var b,c={},d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this),f=e[0],g=e[1];c[f]=g}return x(c)}function q(a){return!a||!a.hasOwnProperty(z)||a instanceof Date?a:a.asMutable({deep:!0})}function r(a,b){for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function s(a,b){function c(a,c,f){var g=y(c[f]),j=i&&i(a[f],g,b),k=a[f];if(void 0!==e||void 0!==j||!a.hasOwnProperty(f)||g!==k&&g===g){var l;l=j?j:h&&d(k)&&d(g)?k.merge(g,b):g,(k!==l&&l===l||!a.hasOwnProperty(f))&&(void 0===e&&(e=r(a,a.instantiateEmptyObject())),e[f]=l)}}if(0===arguments.length)return this;if(null===a||"object"!=typeof a)throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not "+JSON.stringify(a));var e,f,g=a instanceof Array,h=b&&b.deep,i=b&&b.merger;if(g)for(var j=0;j<a.length;j++){var k=a[j];for(f in k)k.hasOwnProperty(f)&&c(this,k,f)}else for(f in a)a.hasOwnProperty(f)&&c(this,a,f);return void 0===e?this:x(e,{instantiateEmptyObject:this.instantiateEmptyObject})}function t(a,b){var c=a[0];if(1===a.length)return u.call(this,c,b);var d,e=a.slice(1),f=this[c];if(d=this.hasOwnProperty(c)&&"object"==typeof f&&null!==f&&"function"==typeof f.setIn?f.setIn(e,b):t.call(G,e,b),this.hasOwnProperty(c)&&f===d)return this;var g=r(this,this.instantiateEmptyObject());return g[c]=d,x(g,this)}function u(a,b){if(this.hasOwnProperty(a)&&this[a]===b)return this;var c=r(this,this.instantiateEmptyObject());return c[a]=y(b),x(c,this)}function v(a){var b,c=this.instantiateEmptyObject();if(a&&a.deep)for(b in this)this.hasOwnProperty(b)&&(c[b]=q(this[b]));else for(b in this)this.hasOwnProperty(b)&&(c[b]=this[b]);return c}function w(){return{}}function x(b,c){var d=c&&c.instantiateEmptyObject?c.instantiateEmptyObject:w;return a(b,"merge",s),a(b,"without",n),a(b,"asMutable",v),a(b,"instantiateEmptyObject",d),a(b,"set",u),a(b,"setIn",t),f(b,A)}function y(a,b){if(c(a))return a;if(a instanceof Array)return j(a.slice());if(a instanceof Date)return k(new Date(a.getTime()));var d=b&&b.prototype,e=d&&d!==Object.prototype?function(){return Object.create(d)}:w,f=e();for(var g in a)a.hasOwnProperty(g)&&(f[g]=y(a[g]));return x(f,{instantiateEmptyObject:e})}var z="__immutable_invariants_hold",A=["setPrototypeOf"],B=["keys"],C=A.concat(["push","pop","sort","splice","shift","unshift","reverse"]),D=B.concat(["map","filter","slice","concat","reduce","reduceRight"]),E=A.concat(["setDate","setFullYear","setHours","setMilliseconds","setMinutes","setMonth","setSeconds","setTime","setUTCDate","setUTCFullYear","setUTCHours","setUTCMilliseconds","setUTCMinutes","setUTCMonth","setUTCSeconds","setYear"]);e.prototype=Error.prototype;var F=y([]),G=y({});y.isImmutable=c,y.ImmutableError=e,Object.freeze(y),"object"==typeof module?module.exports=y:"object"==typeof exports?exports.Immutable=y:"object"==typeof window?window.Immutable=y:"object"==typeof global&&(global.Immutable=y)}();
\ No newline at end of file
+!function(){"use strict";function a(a,b,c){Object.defineProperty(a,b,{enumerable:!1,configurable:!1,writable:!1,value:c})}function b(b){a(b,z,!0)}function c(a){return"object"==typeof a?null===a||a.hasOwnProperty(z):!0}function d(a){return!(null===a||"object"!=typeof a||a instanceof Array||a instanceof Date)}function e(a){var b=new Error(a);return b.__proto__=e,b}function f(a,c){b(a);return a}function g(b,c){var d=b[c];a(b,c,function(){return y(d.apply(b,arguments))})}function h(a,b){if(a in this&&this[a]===b)return this;var c=o.call(this);return c[a]=y(b),j(c)}function i(a,b){var c=a[0];if(1===a.length)return h.call(this,c,b);var d,e=a.slice(1),f=this[c];if(d="object"==typeof f&&null!==f&&"function"==typeof f.setIn?f.setIn(e,b):i.call(F,e,b),c in this&&f===d)return this;var g=o.call(this);return g[c]=d,j(g)}function j(b){for(var c in D)if(D.hasOwnProperty(c)){var d=D[c];g(b,d)}a(b,"flatMap",m),a(b,"asObject",p),a(b,"asMutable",o),a(b,"set",h),a(b,"setIn",i);for(var e=0,j=b.length;j>e;e++)b[e]=y(b[e]);return f(b,C)}function k(b){return a(b,"asMutable",l),f(b,E)}function l(){return new Date(this.getTime())}function m(a){if(0===arguments.length)return this;var b,c=[],d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this);e instanceof Array?c.push.apply(c,e):c.push(e)}return j(c)}function n(a){if(0===arguments.length)return this;var b;if("function"==typeof a)b=a;else{var c=a instanceof Array?a:Array.prototype.slice.call(arguments);b=function(a,b){return c.indexOf(b)>=0}}var d=this.instantiateEmptyObject();for(var e in this)this.hasOwnProperty(e)&&!b(this[e],e)&&(d[e]=this[e]);return x(d,{instantiateEmptyObject:this.instantiateEmptyObject})}function o(a){var b,c,d=[];if(a&&a.deep)for(b=0,c=this.length;c>b;b++)d.push(q(this[b]));else for(b=0,c=this.length;c>b;b++)d.push(this[b]);return d}function p(a){"function"!=typeof a&&(a=function(a){return a});var b,c={},d=this.length;for(b=0;d>b;b++){var e=a(this[b],b,this),f=e[0],g=e[1];c[f]=g}return x(c)}function q(a){return!a||!a.hasOwnProperty(z)||a instanceof Date?a:a.asMutable({deep:!0})}function r(a,b){for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function s(a,b){function c(a,c,f){var g=y(c[f]),j=i&&i(a[f],g,b),k=a[f];if(void 0!==e||void 0!==j||!a.hasOwnProperty(f)||g!==k&&g===g){var l;l=j?j:h&&d(k)&&d(g)?k.merge(g,b):g,(k!==l&&l===l||!a.hasOwnProperty(f))&&(void 0===e&&(e=r(a,a.instantiateEmptyObject())),e[f]=l)}}if(0===arguments.length)return this;if(null===a||"object"!=typeof a)throw new TypeError("Immutable#merge can only be invoked with objects or arrays, not "+JSON.stringify(a));var e,f,g=a instanceof Array,h=b&&b.deep,i=b&&b.merger;if(g)for(var j=0;j<a.length;j++){var k=a[j];for(f in k)k.hasOwnProperty(f)&&c(this,k,f)}else for(f in a)a.hasOwnProperty(f)&&c(this,a,f);return void 0===e?this:x(e,{instantiateEmptyObject:this.instantiateEmptyObject})}function t(a,b){var c=a[0];if(1===a.length)return u.call(this,c,b);var d,e=a.slice(1),f=this[c];if(d=this.hasOwnProperty(c)&&"object"==typeof f&&null!==f&&"function"==typeof f.setIn?f.setIn(e,b):t.call(G,e,b),this.hasOwnProperty(c)&&f===d)return this;var g=r(this,this.instantiateEmptyObject());return g[c]=d,x(g,this)}function u(a,b){if(this.hasOwnProperty(a)&&this[a]===b)return this;var c=r(this,this.instantiateEmptyObject());return c[a]=y(b),x(c,this)}function v(a){var b,c=this.instantiateEmptyObject();if(a&&a.deep)for(b in this)this.hasOwnProperty(b)&&(c[b]=q(this[b]));else for(b in this)this.hasOwnProperty(b)&&(c[b]=this[b]);return c}function w(){return{}}function x(b,c){var d=c&&c.instantiateEmptyObject?c.instantiateEmptyObject:w;return a(b,"merge",s),a(b,"without",n),a(b,"asMutable",v),a(b,"instantiateEmptyObject",d),a(b,"set",u),a(b,"setIn",t),f(b,A)}function y(a,b){if(c(a))return a;if(a instanceof Array)return j(a.slice());if(a instanceof Date)return k(new Date(a.getTime()));var d=b&&b.prototype,e=d&&d!==Object.prototype?function(){return Object.create(d)}:w,f=e();for(var g in a)a.hasOwnProperty(g)&&(f[g]=y(a[g]));return x(f,{instantiateEmptyObject:e})}var z="__immutable_invariants_hold",A=["setPrototypeOf"],B=["keys"],C=A.concat(["push","pop","sort","splice","shift","unshift","reverse"]),D=B.concat(["map","filter","slice","concat","reduce","reduceRight"]),E=A.concat(["setDate","setFullYear","setHours","setMilliseconds","setMinutes","setMonth","setSeconds","setTime","setUTCDate","setUTCFullYear","setUTCHours","setUTCMilliseconds","setUTCMinutes","setUTCMonth","setUTCSeconds","setYear"]);e.prototype=Error.prototype;var F=y([]),G=y({});y.isImmutable=c,y.ImmutableError=e,Object.freeze(y),"object"==typeof module?module.exports=y:"object"==typeof exports?exports.Immutable=y:"object"==typeof window?window.Immutable=y:"object"==typeof global&&(global.Immutable=y)}();
\ No newline at end of file
diff --git a/src/seamless-immutable.js b/src/seamless-immutable.js
index 6431cda..c05e1d6 100644
--- a/src/seamless-immutable.js
+++ b/src/seamless-immutable.js
@@ -209,15 +209,21 @@
       return this;
     }
 
-    // If we weren't given an array, use the arguments list.
-    if (!(keysToRemove instanceof Array)) {
-      keysToRemove = Array.prototype.slice.call(arguments);
+    var remove;
+    if (typeof keysToRemove === "function") {
+      // The argument is a predicate.
+      remove = keysToRemove;
+    } else {
+      // If we weren't given an array, use the arguments list.
+      var keysToRemoveArray = (keysToRemove instanceof Array) ?
+         keysToRemove : Array.prototype.slice.call(arguments);
+      remove = function(val, key) { return keysToRemoveArray.indexOf(key) >= 0; }
     }
 
     var result = this.instantiateEmptyObject();
 
     for (var key in this) {
-      if (this.hasOwnProperty(key) && (keysToRemove.indexOf(key) === -1)) {
+      if (this.hasOwnProperty(key) && ! remove(this[key], key)) {
         result[key] = this[key];
       }
     }


Test patch:
diff --git a/test/ImmutableObject/test-without.js b/test/ImmutableObject/test-without.js
index 0bde238..cdbe46e 100644
--- a/test/ImmutableObject/test-without.js
+++ b/test/ImmutableObject/test-without.js
@@ -7,65 +7,75 @@ function generateArrayOfStrings() {
   return JSC.array()().map(function() { return JSC.string()(); });
 }
 
+function argumentsToArray(theArguments) {
+  return Array.prototype.slice.call(theArguments);
+}
+
+function dropKeysPredicate(keys) {
+  return function (value, key) {
+    return _.includes(keys, key);
+  }
+}
+
 module.exports = function(config) {
   var Immutable = config.implementation;
   var TestUtils = getTestUtils(Immutable);
   var check     = TestUtils.check;
 
   describe("#without", function() {
-    function generateWithoutTestsFor(specifiers) {
+
+    function checkImmutableWithKeys(keysSpecifier, callback/*function(immutable, keys)*/) {
       var runs = 100;
 
       function checkMultiple(callback) {
-        check(runs, specifiers, function(list) {
-          var useVarArgs = !(list instanceof Array);
+        // keysSpecifier = array of generators
+        check(runs, keysSpecifier, function(singleArrayOrFirstKey) {
+
+          var useVarArgs = !(singleArrayOrFirstKey instanceof Array);
 
+          var keys;
           if (arguments.length > 1) {
-            list = Array.prototype.slice.call(arguments);
-          } else if (useVarArgs) {
-            list = [list]
+            keys = Array.prototype.slice.call(arguments);
+          } else if (singleArrayOrFirstKey instanceof Array) {
+            keys = singleArrayOrFirstKey;
+          } else {
+            keys = [singleArrayOrFirstKey];
           }
 
-          assert.notStrictEqual(list.length, 0, "Can't usefully check without() with no objects");
+          assert.notStrictEqual(keys.length, 0, "Can't usefully check without() with no objects");
 
           // Make an object that at LEAST contains the specified keys.
-          var immutable = Immutable(list).asObject(function(key) {
+          var immutable = Immutable(keys).asObject(function(key) {
             return [key, JSC.any()()];
           }).merge(TestUtils.ComplexObjectSpecifier()());
 
-          function runWithout(keys) {
-            keys = keys || list;
-
-            return useVarArgs ?
-              immutable.without.apply(immutable, keys) :
-              immutable.without(keys);
-          }
-
-          callback(immutable, list, runWithout);
+          callback(immutable, keys, useVarArgs);
         })
       }
 
-      it("drops the listed keys", function() {
-        checkMultiple(function(immutable, keys, runWithout) {
-          var expectedKeys = _.difference(_.keys(immutable), keys);
-          var result = runWithout(keys);
+      checkMultiple(callback);
+    }
 
-          TestUtils.assertJsonEqual(_.keys(result), expectedKeys);
+    function generateWithoutTestsFor(firstSpecifier) {
 
-          // Make sure the remaining keys still point to the same values
-          _.each(_.keys(result), function(key) {
-            TestUtils.assertJsonEqual(immutable[key], result[key]);;
-          });
-        });
-      });
+      var keysSpecifier = (firstSpecifier === generateArrayOfStrings) ?
+        [firstSpecifier] : argumentsToArray(arguments);
 
-      it("returns an Immutable Object", function() {
-        checkMultiple(function(immutable, mutables, runWithout) {
-          var result = runWithout();
+      checkImmutableWithKeys(keysSpecifier, function(immutable, keys, useVarArgs) {
 
-          assert.instanceOf(result, Object);
+        it("returns the same result as a corresponding without(predicate)", function() {
+          var expected = immutable.without(dropKeysPredicate(keys));
+          var actual   = useVarArgs ?
+            immutable.without.apply(immutable, keys) :
+            immutable.without(keys);
+          TestUtils.assertJsonEqual(actual, expected);
+        });
 
-          TestUtils.assertIsDeeplyImmutable(result);
+        it("drops the keys", function() {
+          var expectedKeys = _.difference(_.keys(immutable), keys);
+          var result = immutable.without(keys);
+
+          TestUtils.assertJsonEqual(_.keys(result), expectedKeys);
         });
       });
     }
@@ -99,15 +109,56 @@ module.exports = function(config) {
     });
 
     describe("when passed a single key", function() {
-      generateWithoutTestsFor([JSC.string()]);
+      generateWithoutTestsFor(JSC.string());
     });
 
     describe("when passed multiple keys", function() {
-      generateWithoutTestsFor([JSC.string(), JSC.string(), JSC.string()]);
+      generateWithoutTestsFor(JSC.string(), JSC.string(), JSC.string());
     });
 
     describe("when passed an array of keys", function() {
-      generateWithoutTestsFor([generateArrayOfStrings]);
+      generateWithoutTestsFor(generateArrayOfStrings);
+    });
+
+    describe("when passed a predicate", function() {
+      checkImmutableWithKeys([generateArrayOfStrings], function(immutable, keys) {
+
+        it("drops the keys satisfying the predicate", function() {
+          var expectedKeys = _.difference(_.keys(immutable), keys);
+          var result = immutable.without(dropKeysPredicate(keys));
+
+          assert.deepEqual(_.keys(result), expectedKeys);
+
+          // Make sure the remaining keys still point to the same values
+          _.each(_.keys(result), function(key) {
+            TestUtils.assertJsonEqual(immutable[key], result[key]);;
+          });
+        });
+
+        it("returns an Immutable Object", function() {
+          var result = immutable.without(dropKeysPredicate(keys));
+          assert.instanceOf(result, Object);
+          TestUtils.assertIsDeeplyImmutable(result);
+        });
+
+        it("drops keys whose values satisfy the predicate", function() {
+          var valuesToDrop = _.chain(immutable).pick(keys).values();
+          // We might have dropped more keys as more keys might have the same value
+          var expectedKeys = _.chain(immutable)
+            .omit(function (value) {
+              return _.includes(valuesToDrop, value);
+            })
+            .keys()
+            .value();
+          var result = immutable.without(function (value, key) {
+            return _.includes(valuesToDrop, value);
+          });
+
+          assert.deepEqual(_.keys(result), expectedKeys);
+        });
+
+      });
+
     });
   });
 };