How to entirely level (map / reduce / recursion) a nested object of unknown depth in the most efficient manner?

637 Views Asked by At

I would like to do something like the following but in a more large scale and efficient way. Assume I have an array of objects where each object needs to be leveled/flattened.

Convert something like this ...

[{
  name: 'John Doe',
  address: {
    apartment: 1550,
    streetno: 167,
    streetname: 'Victoria',
  },
}, {
  name: 'Joe Smith',
  address: {
    apartment: 2,
    streetno: 111,
    streetname: 'Jones',
  },
}]

... to that ...

[{
  name: 'John Doe',
  apartment: 1550,
  streetno: 167,
  streetname: 'Victoria',
}, {
  name: 'Joe Smith',
  apartment: 2,
  streetno: 111,
  streetname: 'Jones',
}]

As is shown above, address as well is an object which needs to be leveled/flattened.

But most importantly, one does not know the object/data-structure in advance. Thus one neither knows property-names nor the depth of the nested levels.

5

There are 5 best solutions below

0
Peter Seliger On BEST ANSWER

"So before receiving the object you do not know much about its structure."

The OP's main task actually is to level any given nested object-based data-structure into an object of just a single entries-level. And because one does not know anything about a data-structure in advance, one has to come up with a recursive approach.

Once implemented, such a function of cause can be used as callback for an array's mapping process.

The recursive implementation itself is based on type-detection (distinguish in between Array- and Object-types and primitive values) and on reduceing the entries (key-value pairs) of an object according to the currently processed value's type.

function recursivelyLevelObjectEntriesOnly(type) {
  let result = type;
  if (Array.isArray(type)) {

    result = type
      .map(recursivelyLevelObjectEntriesOnly);

  } else if (type && 'object' === typeof type) {

    result = Object
      .entries(type)
      .reduce((merger, [key, value]) => {

        if (value && 'object' === typeof value && !Array.isArray(value)) {

          Object.assign(merger, recursivelyLevelObjectEntriesOnly(value));
        } else {
          merger[key] = recursivelyLevelObjectEntriesOnly(value);
        }
        return merger;

      }, {});    
  }
  return result;
}

const sampleData = [{
  name: 'John Doe',
  address: { apartment: 1550, streetno: 167, streetname: 'Victoria' },
}, {
  name: 'Joe Smith',
  address: { apartment: 2, streetno: 111, streetname: 'Jones' },
}, {
  foo: {
    bar: "bar",
    baz: "baz",
    biz: {
      buzz: "buzz",
      bizz: [{
        name: 'John Doe',
        address: { apartment: 1550, streetno: 167, streetname: 'Victoria' },
      }, {
        name: 'Joe Smith',
        address: { apartment: 2, streetno: 111, streetname: 'Jones' },
      }, {
        foo: {
          bar: "bar",
          baz: "baz",
          biz: {
            buzz: "buzz",
            booz: {
              foo: "foo",
            },
          },
        },
      }],
      booz: {
        foo: "foo",
      },
    },
  },
}];

const leveledObjectData = sampleData.map(recursivelyLevelObjectEntriesOnly);
console.log({ leveledObjectData });

// no mutation at `sampleData`.
console.log({ sampleData });
.as-console-wrapper { min-height: 100%!important; top: 0; }

0
Dogunbound hounds On

That isn't an array.

If you want to flatten a dictionary, do it this way: https://stackoverflow.com/a/22047469/5676613

This has O(n^k) and Omega(n) time complexity where n is the size of the dictionary, and k is the depth of the dictionary (or how many nests are in the dictionary).

2
James On

Assuming you have an array of these objects, you can easily combine each object with its "address" property using destructuring:

const myInput = [
  {
    name: 'john doe',
    address: { apartment: 1550, streetno: 167, streetname: 'Victoria'}
  },
  {
    name: 'Joe Smith',
    address: { apartment: 2, streetno: 111, streetname: 'Jones'}
  }
];

const myOutput = myInput.map(({address, ...rest}) => ({...rest, ...address}));
console.log(myOutput);

2
Andy On

map over the array and return a new object that has had its address property merged into it, the address property deleted, and the new object returned.

const arr=[{name:"john doe",address:{apartment:1550,streetno:167,streetname:"Victoria",a:"a"},b:"b"}];

const out = arr.map(obj => {
  const newObj = { ...obj, ...obj.address };
  delete newObj.address;
  return newObj;
});

console.log(out);

0
Scott Sauyet On

Here's a fairly simply approach based on a recursive function that first converts an element to the following form and then calls Object .fromEntries on the result:

[
  ["name", "John Doe"],
  ["apartment", 1550],
  ["streetno", 167],
  ["streetname", "Victoria"]
]

It looks like this:

const deepEntries = (o) =>
  Object .entries (o) .flatMap (([k, v]) => v.constructor === Object ? deepEntries (v) : [[k, v]])

const deepFlat = (o) =>
  Object .fromEntries (deepEntries (o))

const deepFlatAll = (xs) =>
  xs .map (deepFlat)

const input = [{name: 'John Doe', address: {apartment: 1550, streetno: 167, streetname: 'Victoria'}, }, {name: 'Joe Smith', address: {apartment: 2, streetno: 111, streetname: 'Jones'}}]

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

But you do have a possible significant concern here. If you're flattening multiple levels, it's quite possible that the different levels have nodes with the same name; most of those will be clobbered when you put the object back together.

One solution to such a problem would be to flatten to a different format. I've seen something like this used quite successfully:

{
  "name": "John Doe",
  "address.apartment": 1550,
  "address.streetno": 167,
  "address.streetname": "Victoria"
}

If you look around StackOverflow, you can certainly find answers for how to do that.