cat-t
cat-t

Reputation: 1376

Efficient array filtering based on dynamic criteria

I have a bunch of filter criteria stored in an object. The criteria changes from time to time, so I can't have a static filter (ie: price > 5 && price < 19 && ...).

var criteria = {
    price: {
        min: 5,
        max: 19
    },
    age: {
        max: 35
    }
};

I then have a loop setup to filter through an array based on the criteria and return the filtered array:

var filtered = [];
var add = true;

for (var i=0; i < data.length; i++ ){
    add = true;
    var item = data[i];

    for (var main in criteria){
        for (var type in criteria[main] ){
            if ( type === 'min') {
                if ( !(item[main] > criteria[main][type]) ) {
                    add = false;
                    break;
                }
            } else if ( type === 'max') {
                if ( !(item[main] < criteria[main][type]) ) {
                    add = false;
                    break;
                }
            }
        }
    }
    if (add) {
        filtered.push(item);
    }
}

Is there a more efficient way to setup the filter conditionals ahead of time (ie: item.price > 5 && item.price < 19 && item.age < 35) and then filter the array? As opposed to what I'm currently doing and referencing the object during each array loop - which is inefficient with all the conditionals and sub-loops.

See my jsbin - http://jsbin.com/celin/2/edit .

Upvotes: 1

Views: 2640

Answers (2)

hereandnow78
hereandnow78

Reputation: 14434

i would use Array.prototype.filter:

var filtered = data.filter(function (item) {
  var main, critObj;
  for (main in criteria) {
    critObj = criteria[main];
    if (critObj.min && critObj.min >= item[main]) {
      return false;
    }
    if (critObj.max && critObj.max <= item[main]) {
      return false;
    }
  }
  return true;
});

return falseif it should not be included in your filtered list. inside the for-loop, the function just checks if the criteria has a min, and if if this is bigger than the same property in the array item. if so, it just returns false for this element (the same of course for the max-property).

if both fit, the function returns true, and i will be included in your filtered list!

edit: now with fixed bin

Upvotes: 4

Scott Sauyet
Scott Sauyet

Reputation: 50797

I've been working on the Ramda library, and using it to do this is fairly straightforward:

var test = R.allPredicates(R.reduce(function(tests, key) {
    var field = criteria[key];
    if ('min' in field) {tests.push(R.pipe(R.prop(key), R.gt(R.__, field.min)));}
    if ('max' in field) {tests.push(R.pipe(R.prop(key), R.lt(R.__, field.max)));}
    return tests;
}, [], R.keys(criteria)));

console.log( 'filtered array is: ', data.filter(test) );

(also available in this JSBin.)

To do this without a library, I converted the code above to into a library-less version, and it's a bit more complicated, but still readable:

var test = (function(criteria) {
    var tests = Object.keys(criteria).reduce(function(tests, key) {
        var field = criteria[key];
        if ('min' in field) {tests.push(function(item) {
            return item[key] > field.min;
        });}
        if ('max' in field) {tests.push(function(item) {
            return item[key] < field.max;
        });}
        return tests;
    }, []);
    return function(item) {
        return tests.every(function(test) {return test(item);});
    };
}(criteria));

console.log( 'filtered array is: ', data.filter(test) );

(JSBin)

In either version, the criteria is parsed once to create a set of predicate functions. Those functions are combined into a single predicate which is passed as a filter.

Upvotes: 1

Related Questions