2

I'm trying to build an array of objects from another two arrays, can someone help me out.

const parent =[
  { Id: 1, Cate: 'Accommodation', p_id: null },
  { Id: 4, Cate: 'National Travel', p_id: null }
]

const child =[
  { Id: 2, Cate: 'Hotel Accommodation', p_id: 1 },
  { Id: 3, Cate: 'Own arrangement', p_id: 1 },
  { Id: 5, Cate: 'Air', p_id: 4 },
  { Id: 6, Cate: 'Bus', p_id: 4 },
  { Id: 7, Cate: 'AC Volvo', p_id: 6 },
  { Id: 8, Cate: 'Luxury Bus', p_id: 6 },
  { Id: 9, Cate: 'Bus', p_id: 6 }
]

const data = [
  {
    id: 1,
    tittle: 'Accommodation',
    subItem: [
      { id: 2, tittle: 'Hotel Accommodation', subItems: [] },
      { id: 3, tittle: 'Own arrangement', subItems: [] },
    ],
  },
  {
    id: 4,
    tittle: 'National Travel',
    subItem: [
      { id: 5, tittle: 'Air', subItems: [] },
      {
        id: 6,
        tittle: 'Bus',
        subItem: [
          { id: 7, tittle: 'AC Volvo' },
          { id: 5, tittle: 'Air' },
          { id: 8, tittle: 'Luxury Bus' },
          { id: 9, tittle: 'Bus' },
        ],
      },
    ],
  },
];

parent and child are the data I get from DB, now I need to change that into something looking like data. The thing is it has to be dynamic, as subItems can go long as it needs to be, how can I make something like that?

Nick Parsons
  • 45,728
  • 6
  • 46
  • 64
Thorin
  • 131
  • 1
  • 2
  • 9

4 Answers4

4

You can maintain two objects, a parent object, and a seen object. The seen object is responsible for storing all object ids already seen. That means, for each object in your array, you add it to the seen object.

// Seen object
{
  id_1: {id: id_1, ...}, /* id: 1 */
  id_2: {id: id_2, ...}, /* id: 2 */
  id_3: {id: id_3, ...}, /* id: 3 */
  id_4: {id: id_3, ...}  /* id: 4 */
}

As your parent objects come before they are used by your child objects, you can say that, when the p_id does not appear in the seen object, then the current object is a parent, and so, you can add its id to the parents object (in the above example, assume id_1 is a parent). Keep in mind that this object also gets added to seen, and so, we can create a link between the parent object in seen and the one in parents by using its reference:

// Parents object
{
  id_1: /* ref: 1 */ <-- this refers to the seen[id_1] object
}

This link then allows us to change the object in seen, which would then result in the object in parents changing.

When an object is not a parent object, then it must be a child of another object we have already seen. In this example, assume that the object with id_2 is a child/subItem of id_1. We can first find id_2's parent object (ie: the object with id_1) by looking it up in the seen object using seen[p_id]. Once we have the parent object for id_2, we can add a subItem array to it if needed, and add a reference to our id_2 object to this array:

// Seen object
{
  id_1: {id: id_1, ..., subItem: [/* ref: 2 */]}, /* id: 1 */
  id_2: {id: id_2, ...}, /* id: 2 */
}

we add id_2 as a reference by pushing in the object we added to seen by using .push(seen[Id]). We're adding a reference, as this will mean that if we change the object stored at id_2 in our seen object, then its changes will be reflected in id_1's subItem array.

Once we have iterated over all objects in our array, we can grab the values of our parents object, which contains references to the objects in seen, and those objects themselves also (may) contain references to other objects in seen. This gives us our final result:

const parent =[ { Id: 1, Cate: 'Accommodation', p_id: null }, { Id: 4, Cate: 'National Travel', p_id: null } ];
const child = [ { Id: 2, Cate: 'Hotel Accommodation', p_id: 1 }, { Id: 3, Cate: 'Own arrangement', p_id: 1 }, { Id: 5, Cate: 'Air', p_id: 4 }, { Id: 6, Cate: 'Bus', p_id: 4 }, { Id: 7, Cate: 'AC Volvo', p_id: 6 }, { Id: 8, Cate: 'Luxury Bus', p_id: 6 }, { Id: 9, Cate: 'Bus', p_id: 6 } ];

const buildChildren = arr => {
  const parents = {}, seen = {};
  for(const {Id, Cate, p_id} of arr) {
    seen[Id] = {id: Id, title: Cate};
    if(p_id in seen) {
      const myParent = seen[p_id];
      myParent.subItem = myParent.subItem || []; // add subItem property if it doesn't exist yet
      myParent.subItem.push(seen[Id]); 
    } else { // Adding a new parent
      parents[Id] = seen[Id];
    }
    
  }
  return Object.values(parents);
}

console.log(buildChildren([...parent, ...child]));
Nick Parsons
  • 45,728
  • 6
  • 46
  • 64
  • I've figured out 3 issues. [here](https://stackoverflow.com/questions/66718770/how-to-build-a-dynamic-object-on-js-from-two-arrays/66728564#comment117956384_66718770), [here](https://stackoverflow.com/questions/66718770/how-to-build-a-dynamic-object-on-js-from-two-arrays/66728564#comment117956414_66718770), and [here](https://stackoverflow.com/questions/66718770/how-to-build-a-dynamic-object-on-js-from-two-arrays/66728564#comment117956497_66718770) – Nguyễn Văn Phong Mar 21 '21 at 04:15
  • 1
    Besides, [I've already given an answer](https://stackoverflow.com/questions/66718770/how-to-build-a-dynamic-object-on-js-from-two-arrays/66728564#66728564), you can give me your feedback, Thanks. +1 respect – Nguyễn Văn Phong Mar 21 '21 at 04:17
  • 1
    Thanks, yes, I agree, that those are some issues that are worth looking into. To me it looks as though the `subItem` array is only required if it has values (I think OP made a mistake in their expected result). I also based my answer off of the original data, where parent objects come first, so I didn't bother to sort by the parent ids, but for a more generalized solution this would be a good thing to do. Your solution looks good, you could make it even more concise if you did something like `(parentItemInStore.subItem ??= []).push(store[Id]);`, but that probably isn't as readable – Nick Parsons Mar 21 '21 at 04:28
3

The more concise and another detailed explanation (Based on @Nick Parsons's idea)

Besides, the problem is that item.Id make sure that it should be sorted ascending.


The main ideas are:

  1. Push the "parent item" into result.

    //If the first meet item with `parent ID` doesn't exist in store yet, then it should be the parent item
    if(!parentItemInStore) 
       result.push(store[Id]); // Adding a new parent
    
  2. Add subItem into the "parent item". By using the Reference Javascript, we can modify the item as well as reflect the reference item.

    else { // add subItem
       parentItemInStore.subItem ??= []; // Using `logical nullish assignment` to initialize subItem
       parentItemInStore.subItem.push(store[Id]); 
     }
    

const parent =[ { Id: 1, Cate: 'Accommodation', p_id: null }, { Id: 4, Cate: 'National Travel', p_id: null } ];
const child = [ { Id: 2, Cate: 'Hotel Accommodation', p_id: 1 }, { Id: 3, Cate: 'Own arrangement', p_id: 1 }, { Id: 5, Cate: 'Air', p_id: 4 }, { Id: 7, Cate: 'AC Volvo', p_id: 6 }, { Id: 6, Cate: 'Bus', p_id: 4 }, { Id: 8, Cate: 'Luxury Bus', p_id: 6 }, { Id: 9, Cate: 'Bus', p_id: 6 } ];

const buildHierarchyCollection = flatItems => {
  flatItems.sort((a, b) => a.Id - b.Id); // this line is extremely important
  const result = [], store = {};
  for(const {Id, Cate, p_id} of flatItems) { 
    store[Id] = {Id, Cate}; // Store each item to keep the reference
    const parentItemInStore = store[p_id];
    if(!parentItemInStore) // If the first meet item with `parent ID` doesn't exist in store yet, then it should be the parent item
      result.push(store[Id]); // Adding a new parent
    else { // add subItem
      parentItemInStore.subItem ??= []; // Using `logical nullish assignment` to initialize subItem
      parentItemInStore.subItem.push(store[Id]); 
    }
  }
  
  return result;
}

console.log(buildHierarchyCollection([...parent, ...child]));

Solution 2: No need to sort

const parent =[ { Id: 1, Cate: 'Accommodation', p_id: null }, { Id: 4, Cate: 'National Travel', p_id: null } ];
const child = [ { Id: 2, Cate: 'Hotel Accommodation', p_id: 1 }, { Id: 3, Cate: 'Own arrangement', p_id: 1 }, { Id: 5, Cate: 'Air', p_id: 4 }, { Id: 7, Cate: 'AC Volvo', p_id: 6 }, { Id: 6, Cate: 'Bus', p_id: 4 }, { Id: 8, Cate: 'Luxury Bus', p_id: 6 }, { Id: 9, Cate: 'Bus', p_id: 6 } ];

const resolveParrentMissing = (store, currentItem) => {
  for(let [key, value] of Object.entries(store))
    if(value.p_id == currentItem.Id) {
      currentItem.subItem ??= [];
      currentItem.subItem.push(value); 
    }
}

const buildHierarchyCollection = flatItems => {
  const result = [], store = {};
  for(const {Id, Cate, p_id} of flatItems) {
    store[Id] = {Id, Cate, p_id}; // Store each item to keep the reference
    const parentItemInStore = store[p_id];
    if(!p_id) // If the first meet item with `parent ID` doesn't exist in store yet, then it should be the parent item
      result.push(store[Id]); // Adding a new parent
    else if(parentItemInStore) { // add subItem
      parentItemInStore.subItem ??= []; // Using `logical nullish assignment` to initialize subItem
      parentItemInStore.subItem.push(store[Id]); 
    }
    
    resolveParrentMissing(store, store[Id]);
  }
  
  return result;
}

const result = buildHierarchyCollection([...parent, ...child]);
console.log(result.map(({p_id, ...rest}) => rest));
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
  • This is making the perhaps unwarranted assumption that parents have lower-numbered ids than their children. If that can be guaranteed, then this should work fine. But if not, a technique like mine might be cleaner. – Scott Sauyet Apr 02 '21 at 12:54
  • Yes, I do think so. @Scott Sauyet . actually, I have an loop called `resolveParrentMissing` be responsible for pushing all children items into the last parent. As a result, I don't need sort the array but still make sure the correct result. – Nguyễn Văn Phong Apr 02 '21 at 12:56
  • 1
    Without testing it, I believe this would be fine. For an elegant implementation of this technique, see [an answer](https://stackoverflow.com/a/62138087/1243641) from @Thankyou. – Scott Sauyet Apr 02 '21 at 13:30
  • Yes, Thanks @ScottSauyet – Nguyễn Văn Phong Apr 02 '21 at 13:30
  • And, while I'm pretty certain that this will be more efficient than my technique, mine would likely do fine for many datasets, and I find it much more elegant. – Scott Sauyet Apr 02 '21 at 13:32
  • I agree with you. But in the case of the deep structure, we have to `recursive` multiple times leading throw `StackOverflowException` @ScottSauyet? – Nguyễn Văn Phong Apr 02 '21 at 13:43
  • If you mean that this won't build a tree more than 10,000 levels deep, then yes, that's correct. But if you have a structure like that, this is probably the least of your worries! – Scott Sauyet Apr 02 '21 at 13:48
  • Yup. I got you. Thanks @ScottSauyet – Nguyễn Văn Phong Apr 02 '21 at 13:49
2

I think this approach is simpler than many others presented here.

const makeForest = (xs, id) => 
  xs .filter ((x) => x.p_id == id) 
     .map (({Id, Cate, p_id}, _, __, subItems = makeForest (xs, Id)) => ({
       id: Id, tittle: Cate, ... (subItems.length ? {subItems} : {})
     }))

const parent = [{Id: 1, Cate: 'Accommodation', p_id: null}, {Id: 4, Cate: 'National Travel', p_id: null}]
const child = [{Id: 2, Cate: 'Hotel Accommodation', p_id: 1}, {Id: 3, Cate: 'Own arrangement', p_id: 1}, {Id: 5, Cate: 'Air', p_id: 4}, {Id: 6, Cate: 'Bus', p_id: 4}, {Id: 7, Cate: 'AC Volvo', p_id: 6}, {Id: 8, Cate: 'Luxury Bus', p_id: 6}, {Id: 9, Cate: 'Bus', p_id: 6}]

console .log (
  makeForest ([...parent, ...child])
)
.as-console-wrapper {max-height: 100% !important; top: 0}

We start by combining the two arrays outside our main call. This sort of code is easier to work with on a single list.

We filter out those items whose p_ids match an input parameter (and if we don't pass one, then the ones with null values will also match. Then for each one of those, we build a new object, with subItems being created with a recursive all, using the same list and the current id.

If there are other fields we want to copy over directly, we can just destructure out a ...rest parameter and include it in our mapping:

const makeForest = (xs, id) => 
  xs .filter ((x) => x.p_id == id) 
     .map (({Id, Cate, p_id, ...rest}, _, __, subItems = makeForest (xs, Id)) => ({
       id: Id, tittle: Cate, ... rest, ... (subItems.length ? {subItems} : {})
     }))

Update

Nguyễn Văn Phong pointed out this simplification:

const makeForest = (xs, id) => 
  xs .filter ((x) => x.p_id == id) 
     .map (({Id, Cate, p_id, subItems = makeForest (xs, Id)}) => ({
       id: Id, tittle: Cate, ... (subItems.length ? {subItems} : {})
     }))

which would work equally well with the ...rest case. It's very nice not to need those placeholder variables for the map call.

Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • See also a generalization of this in [another answer](https://stackoverflow.com/a/63855623/1243641). – Scott Sauyet Mar 29 '21 at 16:36
  • That's exactly what I was looking for `- Recursive`. btw, Could you pls tell me the order params (I'm quite confused about the 4th parameter named `subItems`). According to [map's specs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map),we just have 3 params. +1 respect. – Nguyễn Văn Phong Apr 02 '21 at 03:32
  • I mean you can simply this like `({Id, Cate, p_id, subItems = makeForest (xs, Id)}` – Nguyễn Văn Phong Apr 02 '21 at 03:50
  • Also, what happens if we have a deep structure. As a result, It's stack overflow, right? What's your opinion in this case? Which is better between your answer and [mine](https://stackoverflow.com/a/66728564/9071943) – Nguyễn Văn Phong Apr 02 '21 at 03:53
  • 1
    @Nguyễn: That's an interesting simplification. When I start with almost any array method and want to default some parameters, I just assume I need placeholders for the index and array parameters. But this shows I don't need them when I'm already destructuring an object. – Scott Sauyet Apr 02 '21 at 12:45
  • 1
    @Nguyễn: That technique involves adding a default parameter at call-time so that I don't have to declare a local variable. It helps me keep to write functions with only expressions, no statements. But your simplification of it is nicer in this case. – Scott Sauyet Apr 02 '21 at 12:51
  • @Nguyễn: "Also, what happens if we have a deep structure?" I'm not sure what you're suggesting. All this code does is to convert a flat array to a nested one (not a tree, but a forest) based on the relationship between `id`s of one object and `p_id`s of another. – Scott Sauyet Apr 02 '21 at 13:04
1

const parent =[ { Id: 1, Cate: 'Accommodation', p_id: null }, { Id: 4, Cate: 'National Travel', p_id: null } ];
const child = [ { Id: 2, Cate: 'Hotel Accommodation', p_id: 1 }, { Id: 3, Cate: 'Own arrangement', p_id: 1 }, { Id: 5, Cate: 'Air', p_id: 4 }, { Id: 6, Cate: 'Bus', p_id: 4 }, { Id: 7, Cate: 'AC Volvo', p_id: 6 }, { Id: 8, Cate: 'Luxury Bus', p_id: 6 }, { Id: 9, Cate: 'Bus', p_id: 6 } ];

function prepardata(parent, child) {
    // map to hold all the values of parent and child arrays
    const map = {};
    // map to hold final result
    let item = {};
    // add parent elements to map
    for (let i = 0; i < parent.length; i++) {
        map[parent[i].Id] = parent[i];
    }
    // add child elements to map
    for (let i = 0; i < child.length; i++) {
        // adding empty subItems array to every child category
        child[i]["subItems"] = [];
        map[child[i].Id] = child[i];
    }
    /**
     * At this point we have map ready with key as Id and value as
     * the object.
     * Using for-in to iterate over the map where key is Id
     */
    for (let key in map) {
    // if map has a key with value of the current entry p_id (map[key].p_id))
    if (map.hasOwnProperty(map[key].p_id)) {
        let parentCat = map[map[key].p_id];     // parent category element is the entry in map with value equal to p_id
        let childCat = map[key];    // child category element is the entry in map with value equal to key
            // Elements with p_id as null should be at the top level
            if (!parentCat.p_id) {
                // If final item map does not have an entry, then add it
                if (!item[parentCat.Id]) {
                    item[parentCat.Id] = {
                        id: parentCat.Id,
                        title: parentCat.Cate,
                        subItems: [childCat]
                    }
                } else {
                    // else just push the element in the existing parent categories subitems
                    item[parentCat.Id]["subItems"].push(childCat);
                }
            } else {
                // Else it's a sub category, the item needs to be pushed under its subItems array
                if (item[parentCat.p_id]) {
                    item[parentCat.p_id]["subItems"].filter(s => s.Id === childCat.p_id)[0]["subItems"].push(childCat);
                }
            }
        }
    }
    // finally return the values from item map.
    return Object.values(item);
}

const data = prepardata(parent, child);
console.log(data);

You can use javascript map and filter to achieve this result. Iterate over the parent array to get the id and title and filter the child array to get only items with matching p_id.

const data = [];
parent.map(p => {
    const item = {
        id: p.Id,
        title: p.Cate,
        subItem: child.filter(c => c.p_id === p.Id)
    }
    data.push(item);
});

Edit I've added snippet with the updated solution which supports multiple levels as my initial answer only supported 2 levels. Accepted answer by @Nick Parsons is correct and also shorter version but I'm still updating this answer to complete it.

  • i tried it, but it doesn't get the 3rd level of subitems. like, there'll be sub-items to sub-items, it can go on, this way I can only get two levels of subitems. – Thorin Mar 20 '21 at 06:58