Home > Software design >  Find only questions in nested array (recursive)
Find only questions in nested array (recursive)

Time:11-04

I'm trying to find all the objects that has the type equal to question in this deep nested array:

{
  "uuid": "707a5ffd-68e2-4dbd-b539-128512ba3a0a",
  "type": "page",
  "items": [
    {
      "uuid": "9d823429-cc24-444d-a21c-a81357305851",
      "title": "1",
      "type": "question",
    },
    {
      "type": "section",
      "title": "2",
      "uuid": "346dec94-124c-4932-bd40-af9dc68f1d27",
      "items": [
        {
          "uuid": "bf0a9ab9-99cc-4833-b3d3-84a97072e85f",
          "title": "2.1",
          "type": "question",
        }
      ],
    },
    {
      "type": "section",
      "title": "3",
      "uuid": "4964096d-0de9-4ab1-ace5-e42516d6b866",
      "items": [
        {
          "uuid": "b2170580-1e2e-4fb4-a7b9-a56b79db21b3",
          "title": "3.1",
          "type": "question",
        }
      ],
    }
  ],
  "params": {
    "collapsed": false
  }
}

So far I have the following algorithm to transverse the deep array:

export const findQuestions = (items, item = undefined, questions = []) => {
  const _questions = [...questions];

  if (
    item !== undefined
    && !Object.prototype.hasOwnProperty.call(item, 'items')
  ) {
    return _questions;
  }

  for (const _item of items) {
    if (_item.type === 'question') {
      _questions.push(_item);
    }

    if (Object.prototype.hasOwnProperty.call(_item, 'items')) {
      return findQuestions(_item.items, _item, _questions);
    }
  }

  return _questions;
};

I call findQuestions by passing the object referenced above:

const questions = findQuestions([this.lastPage]);

Unfortunately the algorithm stops at 2.1 and do not transverse up to 3.1. My objective is to have an array with all questions:

[
  {
    "title": "1",
  },
  {
    "title": "2.1",
  },
  {
    "title": "3.1",
  }
]

I'm assuming that the problem is at

if (
    item !== undefined
    && !Object.prototype.hasOwnProperty.call(item, 'items')
  ) {
    return _questions;
  }

which returns sooner than it should. How can I fetch all questions?

CodePudding user response:

Another approach builds this atop reusable functions that you might keep around for other needs. Here we build getQuestionTitles as a simple call to findByType ('question') and an extraction of the titles from the matching nodes. findByType is a call to deepFilter using the predicate (o) => o .type == 'question' supplied by findByType. And deepFilter calls traverse to visit all the nodes and recursive subnodes in an Object (in depth-first pre-order; post-order would be a trivial change.)

function * traverse (o) {
  yield o;
  if (Object (o) === o) {
    for (let c of Object .values (o)) {yield * traverse (c)}
  }
}

const deepFilter = (pred) => (o) => 
  [... traverse (o)] .filter (pred)

const findByType = (target) => 
  deepFilter ((o) => o .type == target)

const getQuestionTitles = (obj) =>
  findByType ('question') (obj) .map (({title}) => ({title}))

const data = {uuid: "707a5ffd-68e2-4dbd-b539-128512ba3a0a", type: "page", items: [{uuid: "9d823429-cc24-444d-a21c-a81357305851", title: "1", type: "question"}, {type: "section", title: "2", uuid: "346dec94-124c-4932-bd40-af9dc68f1d27", items: [{uuid: "bf0a9ab9-99cc-4833-b3d3-84a97072e85f", title: "2.1", type: "question"}]}, {type: "section", title: "3", uuid: "4964096d-0de9-4ab1-ace5-e42516d6b866", items: [{uuid: "b2170580-1e2e-4fb4-a7b9-a56b79db21b3", title: "3.1", type: "question"}]}], params: {collapsed: false}}

console .log (getQuestionTitles (data))
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

While getQuestionTitles is specific to this problem, the others are increasingly more generic. traverse is built as a generator since we could use it for find as well as filter, and we'd want to stop as soon as it was done.

I find this a very useful style of coding: keeping around generic abstractions and writing my specific code on them, and in that way keeping the custom code extremely simple.

CodePudding user response:

Here is a simple recursive function

const data = {
  "uuid": "707a5ffd-68e2-4dbd-b539-128512ba3a0a",
  "type": "page",
  "items": [
    {
      "uuid": "9d823429-cc24-444d-a21c-a81357305851",
      "title": "1",
      "type": "question",
    },
    {
      "type": "section",
      "title": "2",
      "uuid": "346dec94-124c-4932-bd40-af9dc68f1d27",
      "items": [
        {
          "uuid": "bf0a9ab9-99cc-4833-b3d3-84a97072e85f",
          "title": "2.1",
          "type": "question",
        }
      ],
    },
    {
      "type": "section",
      "title": "3",
      "uuid": "4964096d-0de9-4ab1-ace5-e42516d6b866",
      "items": [
        {
          "uuid": "b2170580-1e2e-4fb4-a7b9-a56b79db21b3",
          "title": "3.1",
          "type": "question",
        }
      ],
    }
  ],
  "params": {
    "collapsed": false
  }
}

const getQuestions = (data) => {
  let questions = []
  data.items.map(d => {
    if(d.type == 'question') {
      questions.push({type: d.type, uutd: d.uuid, title: d.title})
     }
     if(d.items) {
        questions = [...questions, ...getQuestions(d)]
     }
  })
  return questions;
}

console.log(getQuestions(data))
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

You can use a recursive function to return questions and recursively iterate over sections:

function getQuestions(items) {
  return items.flatMap(({ type, title, items }) => type === 'question' ? { title } : getQuestions(items));
}

Example:

const data = {
  "uuid": "707a5ffd-68e2-4dbd-b539-128512ba3a0a",
  "type": "page",
  "items": [
    {
      "uuid": "9d823429-cc24-444d-a21c-a81357305851",
      "title": "1",
      "type": "question",
    },
    {
      "type": "section",
      "title": "2",
      "uuid": "346dec94-124c-4932-bd40-af9dc68f1d27",
      "items": [
        {
          "uuid": "bf0a9ab9-99cc-4833-b3d3-84a97072e85f",
          "title": "2.1",
          "type": "question",
        }
      ],
    },
    {
      "type": "section",
      "title": "3",
      "uuid": "4964096d-0de9-4ab1-ace5-e42516d6b866",
      "items": [
        {
          "uuid": "b2170580-1e2e-4fb4-a7b9-a56b79db21b3",
          "title": "3.1",
          "type": "question",
        }
      ],
    }
  ],
  "params": {
    "collapsed": false
  }
};

function getQuestions(items) {
  return items.flatMap(({ type, title, items }) => type === 'question' ? { title } : getQuestions(items));
}

console.log(getQuestions(data.items));
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

Recursive solution:

You can get all object with type as question as:

function getItems(obj) {
  if (obj.type === "question") result.push({ title: obj.title });
  if (obj.items) obj.items.forEach((o) => getItems(o));
}

const obj = {
  uuid: "707a5ffd-68e2-4dbd-b539-128512ba3a0a",
  type: "page",
  items: [
    {
      uuid: "9d823429-cc24-444d-a21c-a81357305851",
      title: "1",
      type: "question",
    },
    {
      type: "section",
      title: "2",
      uuid: "346dec94-124c-4932-bd40-af9dc68f1d27",
      items: [
        {
          uuid: "bf0a9ab9-99cc-4833-b3d3-84a97072e85f",
          title: "2.1",
          type: "question",
        },
      ],
    },
    {
      type: "section",
      title: "3",
      uuid: "4964096d-0de9-4ab1-ace5-e42516d6b866",
      items: [
        {
          uuid: "b2170580-1e2e-4fb4-a7b9-a56b79db21b3",
          title: "3.1",
          type: "question",
        },
      ],
    },
  ],
  params: {
    collapsed: false,
  },
};

const result = [];
function getItems(obj) {
  if (obj.type === "question") result.push({ title: obj.title });
  if (obj.items) obj.items.forEach((o) => getItems(o));
}

getItems(obj);
console.log(result);
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

I suggest to you this solution

function findQuestions(array) {
      const questions = [];
      for (const element of array) {
        if (element.type === "question") {
          questions.push(element)
        }

        if ("items" in element) {
          questions.push(findQuestions(element.items))
        }
      }

      return questions
}

CodePudding user response:

If you have more than one level of nesting, then try to use recursive approach:

let allItems = [];

const traverseObject = (obj) => {
    if (!obj.items || obj.items.length == 0)
        return;

    for (let item of obj.items)
    {
      if (item.type === 'question')
          allItems.push(item);
      
      if (item.items && item.items.length > 0)
          traverseObject(item)
    }
}

An example:

const fooJson = {
  "uuid": "707a5ffd-68e2-4dbd-b539-128512ba3a0a",
  "type": "page",
  "items": [
    {
      "uuid": "9d823429-cc24-444d-a21c-a81357305851",
      "title": "1",
      "type": "question",
    },
    {
      "type": "section",
      "title": "2",
      "uuid": "346dec94-124c-4932-bd40-af9dc68f1d27",
      "items": [
        {
          "uuid": "bf0a9ab9-99cc-4833-b3d3-84a97072e85f",
          "title": "2.1",
          "type": "question",
          "items": [
            {
              "uuid": "bf0a9ab9-99cc-4833-b3d3-84a97072e85f",
              "title": "fooBar",
              "type": "question",
            }
          ],
        }
      ],
    },
    {
      "type": "section",
      "title": "3",
      "uuid": "4964096d-0de9-4ab1-ace5-e42516d6b866",
      "items": [
        {
          "uuid": "b2170580-1e2e-4fb4-a7b9-a56b79db21b3",
          "title": "3.1",
          "type": "question",
        }
      ],
    }
  ],
  "params": {
    "collapsed": false
  }
};



let allItems = [];

const traverseObject = (obj) => {
    if (!obj.items || obj.items.length == 0)
        return;

    for (let item of obj.items)
    {
      if (item.type === 'question')
          allItems.push(item);
      
      if (item.items && item.items.length > 0)
          traverseObject(item)
    }
}

traverseObject(fooJson);
console.log(allItems)
<iframe name="sif5" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePudding user response:

You can use a recursive generator function:

function* get_titles(d){ 
    if (d.type === 'question'){
        yield {title:d.title}
    }
    for (var i of ('items' in d ? d.items : [])){
        yield* get_titles(i)
    }
}
var result = [...get_titles(data)]
  • Related