How to flatten a nested object that may contain repeated Javascript keys?

Asked

Viewed 74 times

-2

I am trying to transform an array of objects in which each element is like this:

{
  id: 1,
  maxiTempo: 30,
  todos: {
    contar: 2,
    porcentagem: 1
  },
  perdeu: {
    contar: 4,
    porcentagem: 1.3
  },
  ganhou: {
    contar: 26,
    porcentagem: 0.9
  }
}

For something like:

{
  id: 1,
  maxiTempo: 30,
  todosContar: 2,
  todosPorcentagem: 1,
  perdeuContar: 4,
  perdeuPorcentagem: 1.3,
  ganhouContar: 26,
  ganhouPorcentagem: 0.9
}

I am using this recursive function to leave objects on the same level:

let flatten = (obj, final = {}) => {
  for (let key in obj) {
    if (typeof obj[key] === 'object' && obj[key] != null) {
      flatten(obj[key], final)
    } else {
      final[key] = obj[key]
    }
  }
  return final
}

Just that I think, like inside ganhou and perdeu have the same names contar and porcentagem, he ignores and I get this:

{
  contar: 26,
  id: 1,
  maxiTempo: 30,
  porcentagem: 0.19696969696969696
}

1 answer

4


The hole is a little lower. Note that you want to "flatten" an object that can contain, in the various nesting levels, repeated keys. And as I explained here, in Javascript, an object nay can own two properties with the same key.

I mean, that’s exactly what you assumed:

Just that I think, like inside ganhou and perdeu have the same names contar and porcentagem, he ignores [...]

Your code performs "flattening" correctly. The problem is that it does not create distinct names for each nested property. That is, the same key can be used several times (as it is actually happening). And, in this case, there is a loss data, since the properties are silently overwritten, so only the latter will prevail.

An option to resolve this is to pass the name of the property current as the function’s third argument flatten:

function flatten(obj, final = {}, baseKey = '') {
  for (const key of Object.keys(obj)) {
    const givenKey = !baseKey ? key : baseKey + ucfirst(key);

    if (typeof obj[key] === 'object' && obj[key] !== null) {
      flatten(obj[key], final, givenKey);
    } else {
      final[givenKey] = obj[key];
    }
  }
  return final;
}

function ucfirst(str) {
  return str[0].toUpperCase() + str.slice(1);
}

const original = {
  id: 1,
  maxiTempo: 30,
  todos: {
    contar: 2,
    porcentagem: 1
  },
  perdeu: {
    contar: 4,
    porcentagem: 1.3
  },
  ganhou: {
    contar: 26,
    porcentagem: 0.9,
    testNest: {
      ok: true
    }
  }
};

console.log(flatten(original));

Note that I created a function ucfirst to capitalize the names correctly. Note also that I preferred not to use for..in (in favor of Object.keys - one of several alternatives) to avoid the potentially undesirable situation of reading an enumerable property resulting from careless definition in the prototypical chain.


Out of personal curiosity, I ended up doing another alternative eliminating recursion, which can be problematic because most Javascript implementations do not optimize tail recursion and stack the calls until an eventual catastrophic burst.

function flatten(obj) {
  const final = {};
  const queue = [['', obj]];

  while (queue.length) {
    const [baseKey, currentObj] = queue.pop();

    for (const [key, val] of Object.entries(currentObj)) {
      const givenKey = !baseKey ? key : baseKey + ucfirst(key);

      if (isObject(val)) {
        queue.push([givenKey, val]);
      } else {
        final[givenKey] = val;
      }
    }
  }

  return final;
}

function ucfirst(str) {
  return str[0].toUpperCase() + str.slice(1);
}

function isObject(val) {
  return typeof val === 'object' && val !== null;
}

const original = {
  id: 1,
  maxiTempo: 30,
  todos: {
    contar: 2,
    porcentagem: 1
  },
  perdeu: {
    contar: 4,
    porcentagem: 1.3
  },
  ganhou: {
    contar: 26,
    porcentagem: 0.9,
    testNest: {
      ok: true
    }
  }
};

console.log(flatten(original));


And to transform each element of the array, just perform a mapping. You can use one for calling the function flatten or use its own Array.prototype.map.

It is also worth noting that the two codes above can’t stand it cyclic structures of objects.

  • 1

    Damn, I understood what you explained. Excellent answer, thank you!!! I just wanted to ask one more question if you allow me. If I pass my object with only one item in its function, it will "flatten" correctly, as you showed but what if it is an array of objects? it will flatten everything into a single item? I will edit my example to explain better.

  • @Isa, you’re welcome! I made an observation about that at the end of the answer. One way to do this is to iterate over each element of the array and do the operation individually. Perhaps the simplest way to do that is to use the map, thus: const myNewArray = myArray.map((obj) => flatten(obj));.

  • really helped me a lot!!!

  • 1

    Excellent response!!!!!

Browser other questions tagged

You are not signed in. Login or sign up in order to post.