Simplify filtering conditions

Example refactoring a block of code containing AND condition

Imagine you have an array and you would like to select positive odd numbers. Using EcmaScript5 array iterator you write:

1
2
3
4
5
6
var numbers = [1, 5, -3, 2, 0, null, 0, 7];
// select odd positive numbers
var selected = numbers.filter(function (n) {
return (n > 0) && (n % 2);
});
console.log(selected); // [1, 5, 7]

Works correctly, but looks ugly and requires a comment line. Let's simplify this.

step 1: underscore-contrib

Let's bring underscore-contrib library with lots of predicates:

npm install --save underscore-contrib

The filtering function now does not need an explanation line

1
2
3
4
5
var _ = require('underscore-contrib');
selected = numbers.filter(function filterNumbers(n) {
return _.isPositive(n) && _.isOdd(n);
});
console.log(selected); // [1, 5, 7]

I still do not like the boiler plate function filterNumbers

step 2: create AND predicate

It is easy to write a small utility function and to connect multiple conditions into single one

1
2
3
4
5
6
7
8
9
function and() {
var predicates = Array.prototype.slice.call(arguments);
return function evaluateAnd() {
var args = Array.prototype.slice.call(arguments);
return predicates.every(function (fn) {
return fn.apply(null, args);
});
};
}

and creates a new function out of arguments, the returned function evaluateAnd calls every argument function and returns true only if all predicates return truthy value.

1
selected = numbers.filter(and(_.isPositive, _.isOdd));

This is much cleaner filtering code.

step 3: clean up AND and create OR

Lets make sure that and function is robust by checking all arguments to be functions

1
2
3
4
5
6
7
8
9
10
function and() {
var predicates = Array.prototype.slice.call(arguments);
console.assert(predicates.every(_.isFunction), 'expected every predicate to be a function');
return function evaluateAnd() {
var args = Array.prototype.slice.call(arguments);
return predicates.every(function (fn) {
return fn.apply(null, args);
});
};
}

For completeness sake, lets also create OR function that returns true if some predicate is truthy:

1
2
3
4
5
6
7
8
9
10
function or() {
var predicates = Array.prototype.slice.call(arguments);
console.assert(predicates.every(_.isFunction), 'expected every predicate to be a function');
return function evaluateOr() {
var args = Array.prototype.slice.call(arguments);
return predicates.some(function (fn) {
return fn.apply(null, args);
});
};
}

Update 1 - short AND conditions using Ramda library

One can run multiple predicates using the Ramda library. It has a convenient R.allPass method. For example, to execute a function if the argument is even and the user is authorized

1
2
3
4
5
6
7
8
function isEven(x) {
return x % 2 === 0;
}
function foo(x) {
if (R.allPass([isAuthorized, isEven]))(x)) {
// do something
}
}

We can refactor the above R.allPass call for clarity. First, let us move list of conditions outside.

1
2
3
4
5
6
7
function foo(x) {
var conditions = [isAuthorized, isEven];
var predicate = R.allPass(conditions);
if (predicate(x)) {
// do something
}
}

Notice that the argument x is passed to each individual condition function. What if the different conditions expect different arguments? We can partially apply each function first.

1
2
3
4
5
6
7
8
9
10
11
function foo(x) {
// imagine isAuthorized needs a user object
var conditions = [
R.partial(isAuthorized, user),
R.partial(isEvent, x)
];
var predicate = R.allPass(conditions);
if (predicate()) {
// do something
}
}

Imagine we need to simply check if the passed value x is truthy. Unfortunately, R.allPass requires each condition to be a function and not a primitive value. Thus we cannot write

1
2
3
4
5
var conditions = [
x, // not a function - primitive value
R.partial(isAuthorized, user),
R.partial(isEvent, x)
];

We can use a helper function in this case that would return x

1
2
3
4
5
var conditions = [
R.always(x),
R.partial(isAuthorized, user),
R.partial(isEvent, x)
];

One last trick: if you do not want to create an array of conditions when calling R.allPass, you can unroll it to accept a separate list of arguments using R.unapply.

1
2
3
4
5
6
7
8
9
var arePassing = R.unapply(R.allPass);
function foo(x) {
var predicate = arePassing(
R.always(x), R.partial(isAuthorized, user), R.partial(isEvent, x)
);
if (predicate()) {
// do something
}
}

related: refactor OR