【问题标题】:Is there a way to filter an array of objects with multiple dynamic conditions有没有办法过滤具有多个动态条件的对象数组
【发布时间】:2022-01-03 04:18:36
【问题描述】:

我有一个对象数组options,类似于:

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

我想使用类似于

的过滤条件对象(动态生成)过滤这个数组
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'
    }

在上面的filterGroup 对象中,filters 数组中的每个元素都充当单独的过滤器,options 中的每个option 都应该满足这些过滤器。 condition 的可能值为isis notis less thanis greater than

如何使用 JavaScript 以最有效的方式使用 conditions 对象过滤 options 数组?

我尝试过的(REPL 链接 - https://replit.com/@pcajanand/DarkseagreenEnlightenedTests#index.js),

制作了一些过滤器函数创建者

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)

制作了一个函数来创建带有单个过滤器的过滤器函数(类型=过滤器)

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

创建过滤器函数并将它们推送到数组中,

let fnArray = []
if (filters.type === 'and') {
  filters.filters.forEach((filter) => {
    fnArray.push(makeFilterFunction(filter))
  })
} else if (filters.type === 'filter') {
  fnArray.push(makeFilterFunction(filters))
}

遍历每个选项,检查每个过滤条件,然后将传递所有条件的项目作为过滤结果推送到数组。

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
  }
}, [])

虽然这可行(我认为?),但我想知道是否有其他更有效的方法可以做到这一点。或者,如果我完全遗漏了某些东西并且使事情变得过于复杂。

TLDR - 想要过滤具有多个链接条件的对象数组。

此处为非英语母语者,如果问题含糊不清,请见谅。 感谢阅读!

【问题讨论】:

  • filterGroup 的例子不一样,第一个有顶级的type 属性。那是什么?
  • 您只有一个嵌套过滤器,还是可以无限?
  • @Titus 基本上 filterGroup 可以以任何一种形式出现。如果它只有一个过滤器,它将具有 type = 'filter'。否则它将有 type = 'and' (意味着所有过滤器都使用 and 组合)。
  • @NinaScholz 不,对于我们的用例,只有一层嵌套。所以不会有类似 (Filter1 & Filter 2) ||过滤器 3。但如果我能覆盖类似的东西,那将是一个奖励。

标签: javascript arrays reactjs json filtering


【解决方案1】:

你可以稍微简化一下,这里有一个例子:

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

这样做是使用filterGroup 创建一个函数数组,然后过滤options 数组以查看其中的项目在运行所有这些函数时是否会返回true

【讨论】:

  • 这很棒,帮助我意识到我确实把事情复杂化了。感谢您的回答!
【解决方案2】:

您可以构建函数并过滤数据。这种方法具有嵌套搜索条件。

使用type: 'and'进行过滤的小视图:

带有条件的过滤会返回一个函数,该函数稍后用作过滤的回调。这意味着它从options 中获取一个对象,并对给定条件和移交的数据进行检查,既来自过滤器,也来自选项对象。

现在对于and,您需要多个函数,并且在所有函数中返回true,对象应该在结果集中。

要检查多个函数,Array#every 锥体很方便,检查所有项目并返回 true,如果所有条件都是 truefalse,如果一个条件返回 false。在这种情况下,迭代也会中断。

让我们看看返回的函数:

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

它是对c 的闭包,具有所有需要的过滤条件的值

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

最终返回的函数是内部

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

每个约束函数都被从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; }

【讨论】:

  • 感谢您的快速回答。虽然,我不太明白在“和”情况下发生了什么,但您能帮我介绍一下吗?
【解决方案3】:

您实际上是在实现一种特定领域的语言,您需要将语言表达式转换为可运行的程序。对于这种特定的语言,我们希望将纯 JavaScript 对象的表达式转换为 JavaScript 函数 -

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)}`)
  }
}

然后我们将生成的函数直接插入Array.prototype.filter。基本用法如下所示 -

myinput.filter(evaluate({ /* your domain-specific expression here */ })

接下来,evaluateFilter 是您已经编写的低级函数。在这里它被实现为一个单独的函数,但如果你愿意,你可以将它更多地分开 -

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}`)
  }
}

给定一些input,例如 -

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

我们现在可以使用单个过滤器编写简单的表达式 -

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

或使用and 和/或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
  }
]

评估器是递归的,因此您可以以任何可以想象的方式组合 and 和/或 or -

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

展开sn-p,在自己的浏览器中验证结果-

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

【讨论】:

  • 太棒了!这正是我希望实现的。只是为了清楚起见,您递归地调用评估器函数,直到表达式只是一个过滤器,对吗?编辑:我的错,我一开始只阅读了代码,你明确表示它是递归的
  • 乐于助人!对,那是正确的。对于像andor 这样的复合 表达式,我们递归地使用evaluate 来评估每个组合的子表达式。 {type: "filter", ...} 是一个简单的表达式,没有要评估的子表达式,因此递归 evaluate 最终会停止:D
  • 在这个related Q&A 中,我们将通过一个练习来实现一种用于计算袖珍计算器算术的基本编程语言。在另一个练习中,this Q&A 探讨了使用 javascript 本身扩展 javascript 的功能。如果您有任何问题,我很乐意为您提供帮助^^
  • 非常感谢。我一定会去看看。
猜你喜欢
  • 2021-07-05
  • 2021-12-10
  • 2020-11-10
  • 2021-11-09
  • 2019-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-10-25
相关资源
最近更新 更多