简体   繁体   中英

Build tree array from flat array in Typescript / JavaScript

I have a complex json file that I have to handle with TypeScript / Javascript to make it hierarchical, in order to later build a questionnaire. Every entry of the json has a Id (unique), ParentId (0 If root), Text, Description.

My Typescript Interface

export interface Question {
    Id: number;
    Text: string;
    Desc: string;
    ParentId: number;
    ChildAnswers?: Answer[];
}

export interface Answer {
    Id: number;
    Text: string;
    Desc: string;
    ParentId: number;
    ChildQuestion?: Question;
}

I can guarantee that when the object is an answer it will only have one child which we can assume to be a question.

Flat Data Example:

[
{
    Id: 1,
    Text: 'What kind of apple is it?',
    Desc: '',
    ParentId: 0
},
{
    Id: 2,
    Text: 'Green Apple',
    Desc: '',
    ParentId: 1
},
{
    Id: 3,
    Text: 'Red Apple',
    Desc: '',
    ParentId: 1
},
{
    Id: 4,
    Text: 'Purple GMO Apple',
    Desc: '',
    ParentId: 1
},
{
    Id: 5,
    Text: 'What is the issue with the apple?',
    Desc: '',
    ParentId: 2
},
{
    Id: 6,
    Text: 'Spoiled.',
    Desc: '',
    ParentId: 5
},
{
    Id: 7,
    Text: 'Taste Bad.',
    Desc: '',
    ParentId: 5
},
{
    Id: 8,
    Text: 'Too Ripe.',
    Desc: '',
    ParentId: 5
},
{
    Id: 9,
    Text: 'Is not an apple.',
    Desc: '',
    ParentId: 5
},
{
    Id: 10,
    Text: 'The apple was not green.',
    Desc: '',
    ParentId: 5
},
... So on ...
]

My Goal

{
    Id: 1,
    Text: 'What kind of apple is it?',
    Desc: '',
    ParentId: 0,
    ChildAnswers: [
        {
            Id: 2,
            Text: 'Green Apple',
            Desc: '',
            ParentId: 1,
            ChildQuestion: {
                Id: 5,
                Text: 'What is the issue with the apple?',
                Desc: '',
                ParentId: 2,
                ChildAnswers: [
                    {
                        Id: 6,
                        Text: 'Spoiled.',
                        Desc: '',
                        ParentId: 5,
                        ... So on ...
                    },
                    {
                        Id: 7,
                        Text: 'Taste Bad.',
                        Desc: '',
                        ParentId: 5,
                        ... So on ...
                    },
                    {
                        Id: 8,
                        Text: 'Too Ripe.',
                        Desc: '',
                        ParentId: 5,
                        ... So on ...
                    },
                    {
                        Id: 9,
                        Text: 'Is not an apple.',
                        Desc: '',
                        ParentId: 5,
                        ... So on ...
                    },
                    {
                        Id: 10,
                        Text: 'The apple was not green.',
                        Desc: '',
                        ParentId: 5,
                        ... So on ...
                    },
                    ... So on ...
                ]
            }
        },
        {
            Id: 3,
            Text: 'Red Apple',
            Desc: '',
            ParentId: 1,
            ... So on ...
        },
        {
            Id: 4,
            Text: 'Red Apple',
            Desc: '',
            ParentId: 1,
            ... So on ...
        }
        ... So on ...
    ]
}

I'm currently using this list_to_tree function I found here on stackoverflow, I just don't know if how to tell a question and answer apart. Should I just check to see if the length is one for a question or at odd intervals mark it?:

function list_to_tree(list) {
    var map = {}, node, roots = [], i;
    for (i = 0; i < list.length; i += 1) {
        map[list[i].Id] = i; // initialize the map
        list[i].Children = []; // initialize the children
    }
    for (i = 0; i < list.length; i += 1) {
        node = list[i];
        if (node.ParentId !== 0) {
            // if you have dangling branches check that map[node.ParentId] exists
            list[map[node.ParentId]].Children.push(node);
        } else {
            roots.push(node);
        }
    }
    return roots;
}

Here is a brute force solution to the problem:

var flat = [
{
    Id: 1,
    Text: 'What kind of apple is it?',
    Desc: '',
    ParentId: 0
},
{
    Id: 2,
    Text: 'Green Apple',
    Desc: '',
    ParentId: 1
},
{
    Id: 3,
    Text: 'Red Apple',
    Desc: '',
    ParentId: 1
},
{
    Id: 4,
    Text: 'Purple GMO Apple',
    Desc: '',
    ParentId: 1
},
{
    Id: 5,
    Text: 'What is the issue with the apple?',
    Desc: '',
    ParentId: 2
},
{
    Id: 6,
    Text: 'Spoiled.',
    Desc: '',
    ParentId: 5
},
{
    Id: 7,
    Text: 'Taste Bad.',
    Desc: '',
    ParentId: 5
},
{
    Id: 8,
    Text: 'Too Ripe.',
    Desc: '',
    ParentId: 5
},
{
    Id: 9,
    Text: 'Is not an apple.',
    Desc: '',
    ParentId: 5
},
{
    Id: 10,
    Text: 'The apple was not green.',
    Desc: '',
    ParentId: 5
},
]

// first get the roots
const tree = flat.filter((question) => question.ParentId === 0);

// Next we are going to call alternating methods recursively.
function populateQuestionChildren(node) {
  const { Id } = node;
  flat.forEach((answer) => {
    if (answer.ParentId === Id) {
      if (!node.ChildAnswers) {
        node.ChildAnswers = [];
      }
      node.ChildAnswers.push(answer);
      populateAnswerChildren(answer);
    }
  });
}

function populateAnswerChildren(node) {
  const { Id } = node;
  flat.forEach((question) => {
    if (question.ParentId === Id) {
      if (!node.ChildQuestions) {
        node.ChildQuestions = [];
      }
      node.ChildQuestions.push(question);
      populateQuestionChildren(question);
    }
  });
}

// Kick off the build for each question tree. 
tree.forEach((question) => {
  populateQuestionChildren(question);
});

It is likely that there are more elegant solutions - but given that this will be only few dozen or a few hundred question/answers - this should get you what you need.

[EDIT]

I used your interfaces and discovered a problem with my code. There is only one "ChildQuestion" on an Answer object. So here is my change to TypeScript to make it work properly. I hope it helps:


interface Question {
  Id: number;
  Text: string;
  Desc: string;
  ParentId: number;
  ChildAnswers ? : Answer[];
}

interface Answer {
  Id: number;
  Text: string;
  Desc: string;
  ParentId: number;
  ChildQuestion ? : Question;
}

// first get the roots
const tree = flat.filter((question) => question.ParentId === 0);

function populateQuestionChildren(node: Question) {
  const { Id } = node;
  flat.forEach((answer) => {
    if (answer.ParentId === Id) {
      if (!node.ChildAnswers) {
        node.ChildAnswers = [];
      }
      node.ChildAnswers.push(answer);
      populateAnswerChild(answer);
    }
  });
}

function populateAnswerChild(answer: Answer) {
  const { Id } = answer;
  // switch to every so we can break early once a question is found.
  flat.every((node) => {
    if (node.ParentId === Id) {
      answer.ChildQuestion = node;
      populateQuestionChildren(node);
      return false;
    }
    return true;
  });
}

tree.forEach((question) => {
  populateQuestionChildren(question);
});

I've created an answer based on @nephiw's answer. Since the key will always be Questions or Answers, odd number will always be Answers and even number will be Questions. You can simplify into one function instead of two.

const items = [
  {
    Id: 1,
    Text: "What kind of apple is it?",
    Desc: "",
    ParentId: 0
  },
  {
    Id: 2,
    Text: "Green Apple",
    Desc: "",
    ParentId: 1
  },
  {
    Id: 3,
    Text: "Red Apple",
    Desc: "",
    ParentId: 1
  },
  {
    Id: 4,
    Text: "Purple GMO Apple",
    Desc: "",
    ParentId: 1
  },
  {
    Id: 5,
    Text: "What is the issue with the apple?",
    Desc: "",
    ParentId: 2
  },
  {
    Id: 6,
    Text: "Spoiled.",
    Desc: "",
    ParentId: 5
  },
  {
    Id: 7,
    Text: "Taste Bad.",
    Desc: "",
    ParentId: 5
  },
  {
    Id: 8,
    Text: "Too Ripe.",
    Desc: "",
    ParentId: 5
  },
  {
    Id: 9,
    Text: "Is not an apple.",
    Desc: "",
    ParentId: 5
  },
  {
    Id: 10,
    Text: "The apple was not green.",
    Desc: "",
    ParentId: 5
  }
];

const root = items.filter(item => item.ParentId === 0);
const populateChildren = (curentItem, nested) => {
  const { Id } = curentItem;
  const key = nested % 2 === 1 ? 'ChildAnswers' : 'ChildQuestions';
  items.forEach((item) => {
    if (item.ParentId === Id) {
      if (!curentItem[key]) {
        curentItem[key] = [];
      }
      curentItem[key].push(item);
      populateChildren(item, nested + 1);
    }
  });
}
root.forEach((item) => { 
  populateChildren(item, 1);
});
console.log(root);

You could take an approach where you collect the parts independently of the order of the given data and build a tree and map the children by toggeling the question/answer scheme.

 var data = [{ Id: 1, Text: 'What kind of apple is it?', Desc: '', ParentId: 0 }, { Id: 2, Text: 'Green Apple', Desc: '', ParentId: 1 }, { Id: 3, Text: 'Red Apple', Desc: '', ParentId: 1 }, { Id: 4, Text: 'Purple GMO Apple', Desc: '', ParentId: 1 }, { Id: 5, Text: 'What is the issue with the apple?', Desc: '', ParentId: 2 }, { Id: 6, Text: 'Spoiled.', Desc: '', ParentId: 5 }, { Id: 7, Text: 'Taste Bad.', Desc: '', ParentId: 5 }, { Id: 8, Text: 'Too Ripe.', Desc: '', ParentId: 5 }, { Id: 9, Text: 'Is not an apple.', Desc: '', ParentId: 5 }, { Id: 10, Text: 'The apple was not green.', Desc: '', ParentId: 5 }], tree = function (data, root) { const next = { ChildAnswers: 'ChildQuestion', ChildQuestion: 'ChildAnswers' }, toggle = type => ({ children, ...o }) => Object.assign(o, children && { [type]: children.map(toggle(next[type])) }), t = {}; data.forEach(o => { Object.assign(t[o.Id] = t[o.Id] || {}, o); t[o.ParentId] = t[o.ParentId] || {}; t[o.ParentId].children = t[o.ParentId].children || []; t[o.ParentId].children.push(t[o.Id]); }); return t[root].children.map(toggle('ChildAnswers')); }(data, 0); console.log(tree);
 .as-console-wrapper { max-height: 100%;important: top; 0; }

my code:

  makeTree(nodes: any[], parentId: any): any {
return nodes
  .filter((node) => node.parentId === parentId)
  .reduce(
    (tree, node) => [
      ...tree,
      {
        ...node,
        children: this.makeTree(nodes, node.id),
      },
    ],
    []
  ); }

Generate Node from Array

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM