递归过滤 javascript 中的复杂对象

recursively filter complex object in javascript

我已经为这个问题绞尽脑汁了一段时间,觉得也许是时候 post 了。

我有一个复杂的对象结构,可以有一个嵌套项 属性 任意深度。这是一个例子:

 {
    resourceType: 'QuestionnaireResponse',
    item: [
      {
        linkId: 'Floors',
        answer: []
      },
      {
        linkId: 'KID',
        answer: [
          {
            valueBoolean: false
          }
        ]
      },
      {
        linkId: 'Age',
        answer: [
          {
            valueString: '≥30 Years'
          }
        ]
      },
      {
        linkId: 'UnicornGroup',
        item: [
          {
            linkId: 'DoYouLikeUnicorns',
            answer: [{valueBoolean: true}]
          },
          {
            linkId: 'DoYouLikeFHIR'
          }
        ],
        answer: []
      }
    ]
  }

我想得到一个如下所示的对象:

{
    resourceType: 'QuestionnaireResponse',
    item: [
      {
        linkId: 'KID',
        answer: [
          {
            valueBoolean: false
          }
        ]
      },
      {
        linkId: 'Age',
        answer: [
          {
            valueString: '≥30 Years'
          }
        ]
      },
      {
        linkId: 'UnicornGroup',
        item: [
          {
            linkId: 'DoYouLikeUnicorns',
            answer: [{valueBoolean: true}]
          }
        ]
      }
    ]
  }

也就是说,我想过滤掉答案数组为空且不包含答案数组为非空的嵌套对象的项目对象。

这是我拥有的,但它不起作用:

var res = fItems.filter(function f(o) {
  if (o.answer && o.answer.length > 0) { 
    return true 
  } else {
    if(o.item){
      return f(o.item); 
    }
  }
});

我创建了一个 REPL Here。我们在我们的项目中使用 ramda,所以如果解决方案使用 ramda 那也很好。谢谢你的时间。

我想我通过递归解决方案得到了它。这个想法是深入到最深层次,然后......注意这个过滤器:

items.filter(i => i.answer || (i.item && i.item.length > 0));

第一个条件很简单,但我们需要考虑第二个条件,以防在修剪较低的项目后,我们的项目数组对于某个对象来说是非空的(即使它自己的答案数组是空的)。

let allItems = {
  resourceType: 'QuestionnaireResponse',
  item: [{
      linkId: 'Floors',
      answer: []
    },
    {
      linkId: 'KID',
      answer: [{
        valueBoolean: false
      }]
    },
    {
      linkId: 'Age',
      answer: [{
        valueString: '≥30 Years'
      }]
    },
    {
      linkId: 'UnicornGroup',
      item: [{
          linkId: 'DoYouLikeUnicorns',
          answer: [{
            valueBoolean: true
          }]
        },
        {
          linkId: 'DoYouLikeFHIR'
        }
      ],
      answer: []
    },
    {
      linkId: 'DBZGroup', // this object should complete go away too because all of its children will be removed
      item: [{
          linkId: 'DoYouLikeUnicorns',
          answer: []
        },
        {
          linkId: 'DoYouLikeFHIR'
        }
      ],
      answer: []
    }
  ]
}

function filter(items) {
  items.forEach(i => {
    if (i.item && i.item.length > 0) i.item = filter(i.item);
    if (i.answer && i.answer.length === 0) delete i.answer;
  });
  return items.filter(i => i.answer || (i.item && i.item.length > 0));
}

// make a deep copy if you don't want to mutate the original
allItems.item = filter(allItems.item);
console.log(allItems);

我认为 filter() 实际上是错误的工具,因为它无法轻松处理您想递归过滤 item 数组的情况。为此,您需要将 items 属性 设置为一个新的过滤数组,并最终改变您的原始数组。也许更好的方向是通过添加您想要的项目而不是过滤来构建一个新数组。对于不是具有子项数组的组的项,情况很简单——如果他们有答案,您可以添加它们。但是,必须以不同方式处理这些项目。也许这样的事情会有所帮助:

let obj = {resourceType: 'QuestionnaireResponse',item: [{linkId: 'Floors',answer: []},{linkId: 'KID',answer: [{valueBoolean: false}]},{linkId: 'Age',answer: [{valueString: '≥30 Years'}]},{linkId: 'UnicornGroup',item: [{linkId: 'DoYouLikeUnicorns',answer: [{valueBoolean: true}]},{linkId: 'DoYouLikeFHIR'}],answer: []}]}

function filterAnswers(item_arr){
    return item_arr.reduce((arr, current) => {
        // deal with groups
        if (current.item && current.item.length){
            let item = filterAnswers(current.item)
            if (item.length) {
                let ret_obj = {linkId: current.linkId, item:item}
                arr.push(ret_obj)
            }
        }
        // deal with the simple case
        else if(current.answer && current.answer.length)
            arr.push(current)
        return arr
    }, [])
}
let filtered_items = filterAnswers(obj.item)
console.log(filtered_items)

为了保持代码简单,我假装(也许)复合组上的 answers 属性 总是空的。从示例中不清楚这些项目是否可能具有 answers 和空 item 数组或同时具有 itemanswer。无论哪种方式,它都只是在推送之前测试并将其添加到对象的问题。

这是一种可能性:

const filterAnswers = ({item = [], ...rest}) => {
  const items = item.map(filterAnswers).filter(
    node => (node.answer && node.answer.length)
            || (node.item && node.item.length)
  )
  return Object.assign({...rest}, items.length ? {item: items} : {})
}

const allItems = {"item": [{"answer": [], "linkId": "Floors"}, {"answer": [{"valueBoolean": false}], "linkId": "KID"}, {"answer": [{"valueString": "≥30 Years"}], "linkId": "Age"}, {"answer": [], "item": [{"answer": [{"valueBoolean": true}], "linkId": "DoYouLikeUnicorns"}, {"linkId": "DoYouLikeFHIR"}], "linkId": "UnicornGroup"}], "resourceType": "QuestionnaireResponse"}

console.log(filterAnswers(allItems))

虽然 Ramda(免责声明:我是 Ramda 的作者)可能会在边缘有所帮助(例如 filter(either(path(['answer', 'length']), path(['item', 'length'])))),但我相信,这类问题不会轻易变得毫无意义,而且我不要认为 Ramda 会增加很多。

有关利用多个 Ramda 函数解决此问题的示例:

const fn = R.pipe(
  R.evolve({
    item: R.chain(R.cond([
      [nonEmptyProp('item'), R.o(R.of, x => fn(x))],
      [nonEmptyProp('answer'), R.of],
      [R.T, R.always([])]
    ]))
  }),
  R.when(R.propEq('answer', []), R.dissoc('answer'))
)

要点是:

  • R.evolve 可用于映射对象的特定键,在这种情况下更新 items 数组。
  • R.chain can be used to both map over a list and remove elements, by return the item wrapped as a single element array (here using R.of) 或空数组。
  • 对于非空item属性,我们递归调用函数并将其包装在数组中
  • 对于非空 answer 属性,我们通过将其包装在数组中来包含该项目
  • 对于其他一切,我们return一个空数组来排除它
  • 最后,我们删除了任何具有嵌套项目以及空 answer 值的项目的 answer 属性。

请参阅下面的完整示例。

const nonEmptyProp = R.propSatisfies(R.complement(R.either(R.isNil, R.isEmpty)))

const fn = R.pipe(
  R.evolve({
    item: R.chain(R.cond([
      [nonEmptyProp('item'), R.o(R.of, x => fn(x))],
      [nonEmptyProp('answer'), R.of],
      [R.T, R.always([])]
    ]))
  }),
  R.when(R.propEq('answer', []), R.dissoc('answer'))
)

////

const data = {
    resourceType: 'QuestionnaireResponse',
    item: [
      {
        linkId: 'Floors',
        answer: []
      },
      {
        linkId: 'KID',
        answer: [
          {
            valueBoolean: false
          }
        ]
      },
      {
        linkId: 'Age',
        answer: [
          {
            valueString: '≥30 Years'
          }
        ]
      },
      {
        linkId: 'UnicornGroup',
        item: [
          {
            linkId: 'DoYouLikeUnicorns',
            answer: [{valueBoolean: true}]
          },
          {
            linkId: 'DoYouLikeFHIR'
          }
        ],
        answer: []
      }
    ]
  }

const expected = {
    resourceType: 'QuestionnaireResponse',
    item: [
      {
        linkId: 'KID',
        answer: [
          {
            valueBoolean: false
          }
        ]
      },
      {
        linkId: 'Age',
        answer: [
          {
            valueString: '≥30 Years'
          }
        ]
      },
      {
        linkId: 'UnicornGroup',
        item: [
          {
            linkId: 'DoYouLikeUnicorns',
            answer: [{valueBoolean: true}]
          }
        ]
      }
    ]
  }

console.log(
  R.equals(expected, fn(data))
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>