简体   繁体   中英

Is there a way to filter an array of objects with multiple dynamic conditions

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!

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" } ] } ] })))

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));

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.

You could build functions and filter the data. This approach features nested search conditions.

A small view to filtering with type: 'and' :

The filtering with a condition retuns a function which acts later as callback for filtering. That means it takes one object from options and peforms a check with a given condition and the handed over data, both from the filter as well from the option's object.

Now for and , you need more than one function and of all functions return true , the object should be in the result set.

To check more than one function, Array#every cones in handy by checking all items and return either true , if all conditions are true or false , if one condition returns false . The iteration breaks in this case as well.

Let's have a look to the returned function:

(c => o => c.every(fn => fn(o)))(filters.map(filterBy))

It is a closure over c with the value of all needed filter conditions

(c =>                          )(filters.map(filterBy))

the finally returned function is the inner part

      o => c.every(fn => fn(o))

where every constraint function is taken and called with the object from options .

 const conditions = { 'is': (a, b) => a === b, 'is less than': (a, b) => a < b }, 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" }], filterGroup = { type: 'and', filters: [{ key: 'category', condition: 'is', value: 'veggie', type: 'filter' }, { key: 'price', condition: 'is less than', value: '45', type: 'filter' }] }, filterGroup2 = { key: 'category', condition: 'is', value: 'veggie', type: 'filter' }, filterBy = ({ type, filters, key, condition, value}) => { if (type === 'filter') return o => conditions[condition](o[key], value); if (type === 'and') return (c => o => c.every(fn => fn(o)))(filters.map(filterBy)); }; console.log(options.filter(filterBy(filterGroup))); console.log(options.filter(filterBy(filterGroup2)));
 .as-console-wrapper { max-height: 100%;important: top; 0; }

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM