-1

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!

  • The `filterGroup` examples are not the same, the first one has a top level `type` property. What is that ? – Titus Nov 24 '21 at 19:30
  • do you have only one nested filter, or could it be unlimited? – Nina Scholz Nov 24 '21 at 19:31
  • @Titus basically filterGroup can come in either form. If it only has a single filter it will have type = 'filter'. Otherwise it will have type = 'and' (meaning all the filters are combined using and). – Anand Krishnan Nov 24 '21 at 19:34
  • @NinaScholz No, for our use case only one level of nesting. So there wont be anything like (Filter1 & Filter 2) || Filter 3. But If I can cover something like that, it would be a bonus. – Anand Krishnan Nov 24 '21 at 19:36

3 Answers3

-1

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.

Titus
  • 22,031
  • 1
  • 23
  • 33
-1

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; }
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • Thanks for the quick answer. Although, I didn't quite understand what is happening in the 'and' case, would you be kind enough to walk me through? – Anand Krishnan Nov 24 '21 at 20:10
-1

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"
        }
      ]
    }
  ]
})))
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Amazing!, This is exactly what I was hoping to achieve. Just to make things clear, you are recursively calling the evaluator function in until the expression is just a single filter right? Edit: my bad, I only read the code at first you made it clear it was recurrsive – Anand Krishnan Nov 24 '21 at 20:21
  • happy to help! yes that is correct. for **compound** expressions like `and` and `or`, we use `evaluate` recursively to evaluate each of the combined sub-expressions. `{type: "filter", ...}` is a simple expression with no sub-expressions to evaluate and so the recursive `evaluate` eventually stops :D – Mulan Nov 24 '21 at 20:25
  • in this [related Q&A](https://stackoverflow.com/a/69168750/633183) we go thru an exercise of implementing a basic programming language for computing pocket calculator arithmetic. in another exercise, [this Q&A](https://stackoverflow.com/a/57739457/633183) explores extending javascript's capabilities using javascript itself. if you have any questions, i'm happy to assist ^^ – Mulan Nov 24 '21 at 20:37
  • 1
    Thanks a bunch. I will be sure to check those out. – Anand Krishnan Nov 24 '21 at 21:04