I have an array of objects options
similar to:
const options = [
{
"apiName": "tomato",
"category": "veggie",
"color": "red",
"price": "90"
},
{
"apiName": "banana",
"category": "fruit",
"color": "yellow",
"price": "45"
},
{
"apiName": "brinjal",
"category": "veggie",
"color": "violet",
"price": "35"
},
]
I would like to filter this array using a filtering conditions object (generated dynamically) similar to
Example filterGroup 1
let filterGroup = {
type: 'and',
filters: [
{
key: 'category',
condition: 'is',
value: 'veggie'
type: 'filter'
},
{
key: 'price',
condition: 'is less than',
value: '45',
type: 'filter'
}
]
}
Example filterGroup 2
let filterGroup = {
key: 'category',
condition: 'is',
value: 'veggie'
type: 'filter'
}
In the above filterGroup
object each element in the filters array acts as individual filters that each option
in options
should satisfy.
Possible values of condition
are is
, is not
, is less than
and is greater than
.
How can I filter the options
array using the conditions
object in the most efficient way using JavaScript?
What I have tried (REPL Link - https://replit.com/@pcajanand/DarkseagreenEnlightenedTests#index.js),
Made some filter function creators
const eq = (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] === compareValue)
const ne = (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] === compareValue)
const lt = (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] < compareValue)
const gt = (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] > compareValue)
Made a function to create filter function with an individual filter (type = filter)
const makeFilterFunction = ({condition, value, key}) => {
if (condition === 'is') {
return (eq(key, value))
} else if (condition === 'is greater than') {
return (gt(key, value))
} else if (condition === 'is less than') {
return (lt(key, value))
} else if (condition === 'is not') {
return (ne(key, value))
}
}
Created filter functions and pushed them into an array,
let fnArray = []
if (filters.type === 'and') {
filters.filters.forEach((filter) => {
fnArray.push(makeFilterFunction(filter))
})
} else if (filters.type === 'filter') {
fnArray.push(makeFilterFunction(filters))
}
Loop through every option, check every filter condition against it, then pushed items passing all conditions to an array as filtered result.
const res = opts.reduce((acc, next) => {
let fnIndex = 0
let fnArrayLength = fnArray.length
let itemPassed = true
while(fnIndex < fnArrayLength) {
const fnPassed = fnArray[fnIndex](next)
if (!fnPassed) {
itemPassed = false
break
}
fnIndex = 1
}
if (itemPassed) {
return acc.concat(next)
} else {
return acc
}
}, [])
While this works (I think?), I want to know if there is some other more efficient way to do this. Or if I'm completely missing something and overcomplicating things.
TLDR - Want to filter an array of objects with multiple chained conditions.
Non-native English speaker here, sorry if the question is ambiguous. Thanks for reading!
CodePudding user response:
You are essentially implementing a domain specific language where you need to convert language expressions into runnable programs. For this particular language, we wish to convert expressions from plain JavaScript objects into a JavaScript function -
function evaluate(expr) {
switch (expr?.type) {
case "filter":
return v => evaluateFilter(v, expr)
case "and":
return v => expr.filters.every(e => evaluate(e)(v))
case "or":
return v => expr.filters.some(e => evaluate(e)(v))
//case ...:
// implement any other filters you wish to support
default:
throw Error(`unsupported filter expression: ${JSON.stringify(expr)}`)
}
}
Then we take the resulting function and plug it directly into Array.prototype.filter
. The basic usage will look like this -
myinput.filter(evaluate({ /* your domain-specific expression here */ })
Next, evaluateFilter
is the low-level function that you have already written. Here it is implemented as a single function, but you could separate it more if you desire -
function evaluateFilter(t, {key, condition, value}) {
switch (condition) {
case "is":
return t?.[key] == value
case "is greater than":
return t?.[key] > value
case "is less than":
return t?.[key] < value
case "is not":
return t?.[key] != value
//case ...:
// implement other supported conditions here
default:
throw Error(`unsupported filter condition: ${condition}`)
}
}
Given some input
such as -
const input = [
{ type: "fruit", name: "apple", count: 3 },
{ type: "veggie", name: "carrot", count: 5 },
{ type: "fruit", name: "pear", count: 2 },
{ type: "fruit", name: "orange", count: 7 },
{ type: "veggie", name: "potato", count: 3 },
{ type: "veggie", name: "artichoke", count: 8 }
]
We can now write simple expressions with a single filter -
input.filter(evaluate({
type: "filter",
condition: "is",
key: "type", value: "fruit"
}))
[
{
"type": "fruit",
"name": "apple",
"count": 3
},
{
"type": "fruit",
"name": "pear",
"count": 2
},
{
"type": "fruit",
"name": "orange",
"count": 7
}
]
Or rich expressions that combine multiple filters using and
and/or or
-
input.filter(evaluate({
type: "and",
filters: [
{
type: "filter",
condition: "is not",
key: "type",
value: "fruit"
},
{
type: "filter",
condition: "is greater than",
key: "count",
value: "3"
}
]
}))
[
{
"type": "veggie",
"name": "carrot",
"count": 5
},
{
"type": "veggie",
"name": "artichoke",
"count": 8
}
]
The evaluator is recursive so you can combine and
and/or or
in any imaginable way -
input.filter(evaluate({
type: "or",
filters: [
{
type: "filter",
condition: "is less than",
key: "count",
value: 3
},
{
type: "and",
filters: [
{
type: "filter",
condition: "is not",
key: "type",
value: "fruit"
},
{
type: "filter",
condition: "is greater than",
key: "count",
value: "3"
}
]
}
]
}))
[
{
"type": "veggie",
"name": "carrot",
"count": 5
},
{
"type": "fruit",
"name": "pear",
"count": 2
},
{
"type": "veggie",
"name": "artichoke",
"count": 8
}
]
Expand the snippet to verify the result in your own browser -
function evaluate(expr) {
switch (expr?.type) {
case "filter":
return v => evaluateFilter(v, expr)
case "and":
return v => expr.filters.every(e => evaluate(e)(v))
case "or":
return v => expr.filters.some(e => evaluate(e)(v))
default:
throw Error(`unsupported filter expression: ${JSON.stringify(expr)}`)
}
}
function evaluateFilter(t, {key, condition, value}) {
switch (condition) {
case "is":
return t?.[key] == value
case "is greater than":
return t?.[key] > value
case "is less than":
return t?.[key] < value
case "is not":
return t?.[key] != value
default:
throw Error(`unsupported filter condition: ${condition}`)
}
}
const input = [
{ type: "fruit", name: "apple", count: 3 },
{ type: "veggie", name: "carrot", count: 5 },
{ type: "fruit", name: "pear", count: 2 },
{ type: "fruit", name: "orange", count: 7 },
{ type: "veggie", name: "potato", count: 3 },
{ type: "veggie", name: "artichoke", count: 8 }
]
console.log(input.filter(evaluate({
type: "filter",
condition: "is",
key: "type", value: "fruit"
})))
console.log(input.filter(evaluate({
type: "and",
filters: [
{
type: "filter",
condition: "is not",
key: "type",
value: "fruit"
},
{
type: "filter",
condition: "is greater than",
key: "count",
value: "3"
}
]
})))
console.log(input.filter(evaluate({
type: "or",
filters: [
{
type: "filter",
condition: "is less than",
key: "count",
value: 3
},
{
type: "and",
filters: [
{
type: "filter",
condition: "is not",
key: "type",
value: "fruit"
},
{
type: "filter",
condition: "is greater than",
key: "count",
value: "3"
}
]
}
]
})))
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
CodePudding user response:
You can simplify this a little, here is an example:
const options = [{
"apiName": "tomato",
"category": "veggie",
"color": "red",
"price": "90"
},
{
"apiName": "banana",
"category": "fruit",
"color": "yellow",
"price": "45"
},
{
"apiName": "brinjal",
"category": "veggie",
"color": "violet",
"price": "35"
},
];
const filterGroup1 = {
type: 'and',
filters: [{
key: 'category',
condition: 'is',
value: 'veggie',
type: 'filter'
},
{
key: 'price',
condition: 'is less than',
value: '45',
type: 'filter'
}
]
}
const filterGroup2 = {
key: 'category',
condition: 'is',
value: 'veggie',
type: 'filter'
}
const filterFunConstructor = {
"is": (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] === compareValue),
"is not": (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] !== compareValue),
"is less than": (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] < compareValue),
"is greater than": (propertyAccessKey, compareValue) => (item) => (item[propertyAccessKey] > compareValue)
}
const process = (options, filterGroup) => {
let filterFun;
if (filterGroup.type === 'and') {
filterFun = filterGroup.filters.reduce((a, c) => (a.push(filterFunConstructor[c.condition](c.key, c.value)), a),[]);
} else {
filterFun = [filterFunConstructor[filterGroup.condition](filterGroup.key, filterGroup.value)]
}
return options.filter((v) => filterFun.every((fn) => fn(v)));
}
console.log(process(options, filterGroup1));
console.log(process(options, filterGroup2));
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>
What this does is to use the filterGroup
to create an array of functions and then filter the options
array to see if the items in there will return true
when run through all those functions.