最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

recursively filter complex object in javascript - Stack Overflow

programmeradmin1浏览0评论

I've been racking my brain over this one for awhile and thought maybe it's time to post here.

I have a plex object structure that can have a nested item propery any level deep. Here is an example:

 {
    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: []
      }
    ]
  }

I want to end up with an object that looks like this:

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

That is I want to filter out item objects that have an empty answer array and don't have a nested object that has a non empty answer array.

This is what I have but it's not working:

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

I created a REPL Here. We're using ramda in our project so if the solution uses ramda thats fine too. Thanks for the time.

I've been racking my brain over this one for awhile and thought maybe it's time to post here.

I have a plex object structure that can have a nested item propery any level deep. Here is an example:

 {
    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: []
      }
    ]
  }

I want to end up with an object that looks like this:

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

That is I want to filter out item objects that have an empty answer array and don't have a nested object that has a non empty answer array.

This is what I have but it's not working:

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

I created a REPL Here. We're using ramda in our project so if the solution uses ramda thats fine too. Thanks for the time.

Share Improve this question edited Oct 17, 2018 at 21:58 cobolstinks asked Oct 17, 2018 at 21:45 cobolstinkscobolstinks 7,15118 gold badges73 silver badges104 bronze badges 2
  • Is the empty answerGroup array property in the Unicorn Group supposed to be there, and if so, is it supposed to be removed from the result? I supposed equivalently, can you have both item and answer on a node? – Scott Sauyet Commented Oct 17, 2018 at 23:28
  • Hi no I don't want the empty answer array included in the unicornGroup. But I do want the unicornGroup added to the result (just like second json object is) – cobolstinks Commented Oct 18, 2018 at 1:16
Add a ment  | 

4 Answers 4

Reset to default 2

I think filter() is actually the wrong tool for this because it can't handle the situation easily where you want to recursively filter the item array. To do that you need to set the items property to a new filtered array and you end up mutating your original. Maybe a better direction is to just build up a new array by adding the the items you want rather than filtering. The case is simple in the items that are not groups with a child items array — you can just add those if they have answers. The items, however, have to be handled differently. Maybe something like this will help:

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)

To keep the code simple I am pretending (maybe) that the answers property on the pound groups is always empty. It's not clear from the example if these items might have answers and empty item array or both item and answer. Either way it's just a matter of testing and adding it to the object before pushing.

Here's one possibility:

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

While Ramda (disclaimer: I'm a Ramda author) might help at the edges (filter(either(path(['answer', 'length']), path(['item', 'length']))) for instance), this sort of problem would not easily be made point-free, I believe, and I don't think Ramda would add a lot.

I think I have it via a recursive solution. The idea is to go down to the deepest level and then ... note this filter:

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

The first condition is straightforward, but we need to consider the second in case after pruning the lower items we have an item array that is non empty for an object (even if its own answer array is empty).

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

For an example of utilising a number of Ramda functions to solve this:

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

The main points being:

  • R.evolve can be used to map over specific keys of an object, in this case to update the items array.
  • 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) or an empty array respectively.
  • For non-empty item properties, we recursively call the function and wrap it in an array
  • For non-empty answer properties, we include the item by wrapping it in an array
  • For everything else, we return an empty array to exclude it
  • Finally, we remove the answer property for any items that had nested items along with an empty answer value.

See your full example below.

const nonEmptyProp = R.propSatisfies(R.plement(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./ajax/libs/ramda/0.25.0/ramda.min.js"></script>

发布评论

评论列表(0)

  1. 暂无评论