Reputation: 1273
I have this project which has a search page where I have a basic filter form. I managed to filter the data but only with one filter at a time. I can't figure the logic behind applying multiple filters in order to narrow down the data.
Example:
let data = [{
budget: "220",
categories: ["party", "school"]
},
{
budget: "450",
categories: ["self"]
},
{
budget: "600",
categories: ["dev", "work"]
}
];
const filters = {
budget: ["200","500"],
categories: ["party"]
}
//////Expected behavior:
Outputs only the first object because it's budget it's between 200 and 500 and it has "party" as it's category.
This example simulates basically what I have in my app. As you can see, I have an array of objects. Each object has a budget and multiple categories. For the filters, let's say I apply a budget filter (which is going to be a range filter) and one category filter. How should I chain this filters together in order to filter the data properly?
Upvotes: 2
Views: 2372
Reputation: 23372
This question is a great excuse for a functional programming excercise :D
Array.prototype.filter
takes a predicate function. A predicate takes one argument and returns a boolean. Although javascript won't complain, it makes sense that the types of the elements in the array match the types your predicate can handle.
You already got to the point of filtering with one predicate, but I'll include an example anyway:
// [ number ]
const numbers = [ 1, 2, 3, 4, 5, 6 ];
// number -> bool
const lt5 = x => x < 5;
// [ number ] -> (number -> bool) -> [ number ]
const result = numbers.filter(lt5);
console.log(result); // [ 1, 2, 3, 4 ]
Now, let's say you only want the even numbers that are less than 5... How do we apply multiple filters? The most straightforward way is to filter twice:
// [ number ]
const numbers = [ 1, 2, 3, 4, 5, 6 ];
// number -> bool
const lt5 = x => x < 5;
// number -> bool
const even = x => x % 2 === 0;
const result = numbers
.filter(lt5) // [ 1, 2, 3, 4 ]
.filter(even);
console.log(result); // [ 2, 4 ]
Although some people will complain about efficiency (this loops 10 times), I'd actually recommend this approach whenever you need the elements to pass all of multiple filters.
However, if we want to switch between being able to filter items that either all, or any of our predicates, we need another approach. Luckily, there's some
and every
!
// [ number ]
const numbers = [ 1, 2, 3, 4, 5, 6 ];
// number -> bool
const lt5 = x => x < 5;
// number -> bool
const even = x => x % 2 === 0;
// number -> bool
const lt5_OR_even = x => [lt5, even].some(f => f(x));
// number -> bool
const lt5_AND_even = x => [lt5, even].every(f => f(x));
console.log(
numbers.filter(lt5_OR_even) // [ 1, 2, 3, 4, 6 ]
);
console.log(
numbers.filter(lt5_AND_even) // [ 2, 4 ]
);
Instead of looping over arrays of predicates, we can also take a different approach. We can compose our predicates in to new ones using two small helpers, both
and either
:
// (a -> bool) -> (a -> bool) -> a -> bool
const both = (f, g) => x => f(x) && g(x);
// (a -> bool) -> (a -> bool) -> a -> bool
const either = (f, g) => x => f(x) || g(x);
const numbers = [ 1, 2, 3, 4, 5, 6 ];
const lt5 = x => x < 5;
const even = x => x % 2 === 0;
console.log(
numbers.filter(either(lt5, even)) // [ 1, 2, 3, 4, 6 ]
);
console.log(
numbers.filter(both(lt5, even)) // [ 2, 4 ]
);
With these helpers, we can take any array of predicates and merge them in to one! The only thing we need to add is a "seed" so we can reduce
safely:
// (a -> bool) -> (a -> bool) -> a -> bool
const both = (f, g) => x => f(x) && g(x);
// (a -> bool) -> (a -> bool) -> a -> bool
const either = (f, g) => x => f(x) || g(x);
// any -> bool
const True = _ => true;
const Filter = (predicates, comparer = both) =>
predicates.reduce(comparer, True);
const myPred = Filter([
x => x > 5,
x => x < 10,
x => x % 2 === 0
]);
console.log(
[1,2,3,4,5,6,7,8,9,10,11].filter(myPred) // [ 6, 8 ]
);
Putting it all together, you start to realize this overcomplicates things for simple examples 😅. However, it's still interesting to see how we can reuse and combine single purpose, testable functions in a functional manner.
const both = (f, g) => x => f(x) && g(x);
const either = (f, g) => x => f(x) || g(x);
const True = _ => true;
const gte = min => x => +x >= +min;
const lte = max => x => +x <= +max;
const overlap = (xs, ys) => xs.some(x => ys.includes(x));
const Filter = (predicates, comparer = both) =>
predicates.reduce(comparer, True);
const BudgetFilter = ([min, max]) => ({ budget }) =>
Filter([ gte(min), lte(max) ]) (budget);
const CategoryFilter = allowed => ({ categories }) =>
overlap(allowed, categories);
const EventFilter = (cfg, opts) => Filter(
Object
.entries(opts)
.map(([k, v]) => cfg[k](v))
);
// App:
const filterConfig = {
budget: BudgetFilter,
categories: CategoryFilter
};
const cheapPartyFilter = EventFilter(
filterConfig,
{
budget: ["200", "500"],
categories: ["party"]
}
);
let data = [{ budget: "220", categories: ["party", "school"] }, { budget: "450", categories: ["self"] }, { budget: "600", categories: ["dev", "work", "party"] }];
console.log(data.filter(cheapPartyFilter));
Upvotes: 5
Reputation: 191916
Each filter should have a function that can handle the check. The filterHandlers
is a Map of such handlers.
The array and the object of filters are passed to applyFilters()
. The method gets the keys of the filters (via Object.keys()
), and iterates the items with Array.filters()
. Using Array.every()
the item is checked by all filters (using the relevant handler). If all checks pass, the item will be included in the resulting array.
Whenever you need to add another filter, you add another handler to the Map.
const filterHandlers = new Map([
[
'budget',
(val, [min, max]) => val >= min && val <= max
],
[
'categories',
(current, categories) => current.some( // some - at least one, every - all of them
(c) => categories.includes(c)
)
],
[]
]);
const applyFilters = (arr, filters) => {
const filterKeys = Object.keys(filters);
return arr.filter(o => filterKeys.every((key) => {
const handler = filterHandlers.get(key);
return !handler || handler(o[key], filters[key]);
}));
}
const data = [{"budget":"220","categories":["party","school"]},{"budget":"450","categories":["self"]},{"budget":"600","categories":["dev","work"]}];
const filters = {"budget":["200","500"],"categories":["party"]};
const result = applyFilters(data, filters);
console.log(result);
Upvotes: 5
Reputation: 1079
As someone suggested above its a great functional programming excercise. So this is my take on it. Might be little bit trickier to understand And it is not pure FP per say, but you can modify and add as many filter to each as long as the data is an object is only one level deep.
function datafilter (datas, filters, filterops) {
//the follwing two function convert the filters from [key, [op, operands]] to [key, filterFunction] based on your filters and filterops
var getFilterFunctionFor = ([operation, operands])=> filterops[operation](operands)
var filterFuncs = filters.map(([key, operation])=>[key, getFilterFunctionFor(operation)])
//now filter the data by aplying each filterFunction to mathcing key in data
return datas.filter((data)=>{
return filterFuncs.reduce((prevOpTrue, [key, applyFilterTo])=>(key in data) && prevOpTrue && applyFilterTo(data[key]), true)
})
}
var datas = [{
budget: "220",
categories: ["party", "school"]
},
{
budget: "450",
categories: ["self"]
},
{
budget: "600",
categories: ["dev", "work"]
}
];
var ops = {
rangeBetween : ([min, max])=> (value)=>((value>= min)&&(value<=max)),
contains : (key) => (list) => list.includes(key)
}
var filters = [
['budget', ['rangeBetween', ["200", "500"]]],
['categories',['contains', 'party']]
]
console.log(datafilter(datas,filters, ops))
Upvotes: 0
Reputation: 26844
You can use filter()
to filter the array. Use &&
for multiple conditions. Use .every()
to check if all elements of filters.categories
are on current entry.
let data = [{
budget: "220",
categories: ["party", "school"]
},
{
budget: "450",
categories: ["self"]
},
{
budget: "600",
categories: ["dev", "work"]
}
];
const filters = {
budget: ["200", "500"],
categories: ["party"]
}
let result = data.filter(o => filters.budget[0] <= o.budget && filters.budget >= o.budget[1] && filters.categories.every(e => o.categories.includes(e)))
console.log(result);
Upvotes: 1