1

I'm trying to group a big nested object with multiple properties such as this one :

[
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 70,
      "name": "Name70"
    }
  },
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 61,
      "name": "Name61"
    }
  },
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 4,
      "name": "Name4",
      "sub": {
        "id": 5,
        "name": "Name5",
        "sub": {
          "id": 29,
          "name": "Name29"
        }
      }
    }
  },
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 4,
      "name": "Name4",
      "sub": {
        "id": 5,
        "name": "Name5",
        "sub": {
          "id": 8,
          "name": "Name8",
          "sub": {
            "id": 163,
            "name": "Name163"
          }
        }
      }
    }
  },
  {
    "id": 10,
    "name": "Name10",
    "sub": {
      "id": 4,
      "name": "Name4"
    }
  }
]

As you can see, the "sub" are not arrays as of now, but they would be in the expected output even if there's only one object in it.
I'd like to group the array by object's id recursively to get this kind of output :

[
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": [
      {
        "id": 70,
        "name": "Name70"
      },
      {
        "id": 61,
        "name": "Name61"
      },
      {
        "id": 4,
        "name": "Name4",
        "sub": [
          {
            "id": 5,
            "name": "Name5",
            "sub": [
              {
                "id": 29,
                "name": "Name29"
              },
              {
                "id": 8,
                "name": "Name8",
                "sub": [
                  {
                    "id": 163,
                    "name": "Name163"
                  }
                ]
              }
            ]
          }
        ]
      }
    ]
  },
  {
    "id": 10,
    "name": "Name10",
    "sub": [
      {
        "id": 4,
        "name": "Name4"
      }
    ]
  }
]

So far, I tried some shenanigans with lodash and d3.nest() but I just can't seem to group it.
Have you guys ever face something similar? And if so, how did you manage to code this?

Thanks a lot

3 Answers 3

2

You could take a recursive approach with a function which merges an object into an array by looking for same id.

const
    merge = (target, { sub, ...o }) => {
        let temp = target.find(({ id }) => id === o.id);
        if (sub) sub = merge(temp?.sub || [], sub)
        if (!temp) target.push(temp = { ...o, sub });
        return target;
    };

var data = [{ id: 14, name: "Name14", theme: true, sub: { id: 70, name: "Name70" } }, { id: 14, name: "Name14", theme: true, sub: { id: 61, name: "Name61" } }, { id: 14, name: "Name14", theme: true, sub: { id: 4, name: "Name4", sub: { id: 5, name: "Name5", sub: { id: 29, name: "Name29" } } } }, { id: 14, name: "Name14", theme: true, sub: { id: 4, name: "Name4", sub: { id: 5, name: "Name5", sub: { id: 8, name: "Name8", sub: { id: 163, name: "Name163" } } } } }, { id: 10, name: "Name10", sub: { id: 4, name: "Name4" } }],
    result = data.reduce(merge, []);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Sign up to request clarification or add additional context in comments.

2 Comments

nice! same approach as mine (recursive), but much smarter :)
Thanks a lot Nina! It is exactly what I wanted and your code looks amazing!
0

You could create a Map for each sub property -- keyed by id -- and merge the objects into those maps. Then finally convert those sub-maps back to sub-arrays:

function mergeArray(arr) {
    function mergeObject(a, b) {
        while (b = b.sub) {
            if (!a.sub) a.sub = new Map;
            let child = a.sub.get(b.id);
            if (child) a = child;
            else a.sub.set(b.id, a = { id: b.id, name: b.name });
        }
    }

    function convertMap(map) {
        return Array.from(map.values(), obj => {
            if (obj.sub) obj.sub = convertMap(obj.sub);
            return obj;
        });
    }

    let map = new Map(arr.map(({id, name}) => [id, {id, name}]));
    for (let item of arr) mergeObject(map.get(item.id), item);
    return convertMap(map);
}

// Demo with input from question
let input = [{"id": 14,"name": "Name14","theme": true,"sub": {"id": 70,"name": "Name70"}},{"id": 14,"name": "Name14","theme": true,"sub": {"id": 61,"name": "Name61"}},{"id": 14,"name": "Name14","theme": true,"sub": {"id": 4,"name": "Name4","sub": {"id": 5,"name": "Name5","sub": {"id": 29,"name": "Name29"}}}},{"id": 14,"name": "Name14","theme": true,"sub": {"id": 4,"name": "Name4","sub": {"id": 5,"name": "Name5","sub": {"id": 8,"name": "Name8","sub": {"id": 163,"name": "Name163"}}}}},{"id": 10,"name": "Name10","sub": {"id": 4,"name": "Name4"}}];

console.log(mergeArray(input));

3 Comments

Thanks a lot for the fast answer trincot. I will try it out tomorrow as I'm not at work anymore and get back to you!
Your solution works wonders! However, I am going to accept @Nina Scholz's one as it is more concise. Thanks a lot again!
True, Nina's solution is more concise. Just be aware that it has a worse time complexity, because of the nested call to find. I have added the use of a Map, which adds extra code, but it serves a purpose: the time complexity reduces, which may become noticable on larger data sets. On small data sets it will run slower because of the Map creation overhead.
0

I would do it with recursive functions, because I think those are more versatile:

const data = [{
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 70,
      "name": "Name70"
    }
  },
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 61,
      "name": "Name61"
    }
  },
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 4,
      "name": "Name4",
      "sub": {
        "id": 5,
        "name": "Name5",
        "sub": {
          "id": 29,
          "name": "Name29"
        }
      }
    }
  },
  {
    "id": 14,
    "name": "Name14",
    "theme": true,
    "sub": {
      "id": 4,
      "name": "Name4",
      "sub": {
        "id": 5,
        "name": "Name5",
        "sub": {
          "id": 8,
          "name": "Name8",
          "sub": {
            "id": 163,
            "name": "Name163"
          }
        }
      }
    }
  },
  {
    "id": 10,
    "name": "Name10",
    "sub": {
      "id": 4,
      "name": "Name4"
    }
  }
]

// setting up arrays
const recursiveModify = (node) => {
  if (typeof node.sub === "undefined") {
    return node
  } else {
    node.sub = [recursiveModify(node.sub)]
    return node
  }
}

const recursiveReduce = (nodes) => {
  return nodes.reduce((a, c) => {
    const item = a.find(e => e.id === c.id)
    if (!item) {
      a.push(c)
    } else {
      item.sub = recursiveReduce([...item.sub, ...c.sub])
    }
    return a
  }, [])
}

const dataWithArray = data.map(node => {
  return recursiveModify(node)
})

const result = recursiveReduce(dataWithArray)
console.log(result)

Unfortunately I could only do it with two passes - one for creating the sub as arrays, then one for actually grouping the data. I'm pretty sure it can be done in one pass - I just have no more time to work it out.

2 Comments

Thank you for your time muka! I will however accept Nina's answer as it is more concise
@JoPe I would do the same :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.