рекурсивно фильтровать сложный объект в JavaScript - PullRequest
0 голосов
/ 18 октября 2018

Я долго ломал голову над этим и подумал, может быть, пришло время опубликовать здесь.

У меня сложная структура объектов, которая может иметь вложенный элемент любого уровня.Вот пример:

 {
    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 Здесь .Мы используем ramda в нашем проекте, так что если решение использует ramda, то это тоже хорошо.Спасибо за время.

Ответы [ 4 ]

0 голосов
/ 18 октября 2018

Для примера использования ряда функций Рамды для решения этой проблемы:

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 canиспользоваться для отображения определенных ключей объекта, в этом случае для обновления массива items.
  • R.chain может использоваться как для отображения списка, так и для удаления элементов,возвращая элемент, заключенный в массив из одного элемента (здесь используется R.of) или пустой массив соответственно.
  • Для непустых свойств item мы рекурсивно вызываем функцию иобернуть его в массив
  • Для непустых answer свойств мы включаем элемент, заключая его в массив
  • Для всего остального мы возвращаем пустой массив, чтобы исключить его
  • Наконец, мы удаляем свойство 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>
0 голосов
/ 18 октября 2018

Я думаю, что 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 или оба item и answer.В любом случае это всего лишь вопрос тестирования и добавления его к объекту перед нажатием.

0 голосов
/ 18 октября 2018

Вот одна из возможностей:

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

Хотя Рамда (отказ от ответственности: я автор Рамды) мог бы помочь по краям (например, filter(either(path(['answer', 'length']), path(['item', 'length'])))), проблема такого рода не может быть легко решена-Я уверен, и я не думаю, что Рамда добавил бы много.

0 голосов
/ 18 октября 2018

Я думаю, что у меня есть рекурсивное решение.Идея состоит в том, чтобы спуститься до самого глубокого уровня, а затем ... обратите внимание на этот фильтр:

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