How to go through an array of objects, add specific properties and unify those objects where it has a specific property equal to another?

Asked

Viewed 287 times

7

I have an array of objects that have this structure below:

listaProdutos = 
[
      {nome:'Teclado',preco:28.00},
      {nome:'Mouse',preco:36.00},
      {nome:'Monitor',preco:500.00},
      {nome:'CPU',preco:800.00},
      {nome:'Teclado',preco:28.00},
      {nome:'Monitor',preco:500.00},
];

My intention would be to generate an array of objects with the result below:

novoLista = 
[
    {nome:'Teclado',preco:56.00},
    {nome:'Mouse',preco:36.00},
    {nome:'Monitor',preco:1000.00},
    {nome:'CPU',preco:800.00} 
];

You could help me with the best approach to this situation ?

  • 1

    See what the hash table structure/ scattering table would be / hash table in the JS (set of key/value pairs in which the key does not repeat)? Go through the array and see if the names already exist as hash table keys, if they already exist you update the value of the array (add what was already with the object), or add the new pair.

  • 1

    Table hash is an essential and ubiquitous data structure in computing, present in languages under several names (dictionaries, maps, hashmaps, etc.), if you do not know it, I recommend that you study and understand its working principle well, because it applies to a diversity of situations. Here at Sopt itself there is a good explanation: https://answall.com/q/27827/357

  • 2

    I’m keeping a benchmark with every answer to this question. In order not to be unfair with any response, I suggest that the imminent user of any of the codes below change the default values of the number of elements of the benchmark to mirror the needs of the project. The performance of each solution varies significantly according to the number of unique items (NAMES_COUNT) and of total elements (ITEMS_COUNT). Here is the link.

  • @Luizfelipe made simulations with 3 test scenarios, with 1000 items, 100 and 10 and the results are completely different, it would be interesting to assemble these 3 scenarios to have a good report at the end

  • @Ricardopunctual as it is, and also varies with orders of magnitude greater than 1e3. Try with 1e5 or 1e6, for example. P That’s why I suggested that the user test several according to the approximate amount that will work. But let’s face it, JS really isn’t the language for this kind of operation with many items, so this idea of benchmark is kind of pointless first of all...

  • Guys, thank you so much for being helpful. I’m still a layman when it comes to testing benchmark performance. This question is based on a course that I am doing and I was left with doubts. As I did not want to do strange things (which was how I knew how to do) I preferred to consult here, to add knowledge. But honestly, in your opinion. Which of the answers would perform better with "few data" and "lots of data"? Would the difference be noticeable with "too much data"? From what I saw there, with a layman’s vision, Ricardo Pontual’s was the one that seemed to have the best result.

  • 1

    @Gatodeschrödinger, with few data the difference is minimal and almost imperceptible, since (although percentually the difference is visible) everything will be very fast. In larger quantities, it will make a difference. In your case, if the amount of data is small, choose the code you like best. No doubt all the solutions posted here are very elegant. :-)

  • 3

    @Gatodeschrödinger depends on from which number Voce considers "too much data" or "too little data". The nice thing about these answers is that they are distinctive solutions, with different benchmark results depending on the situation. In Bench. that Luiz Felipe showed, his solution is excellent for cases of many objects in the array (+1000), but for the simplest case, Ricardo Pontual’s solution is the one that best adapts. I even think we should study a little more about the Map. For me this was the question with the most interesting answers of the year :D .

  • Yes, @Cmtecardeal, But the idea is really this. For me it may be "few data", but in the future I may need the solution for "lots of data". Or other users who enter after a solution, can vary between wanting the answer to "too much data" and "too little data". The idea is that the answer really fits me. But the general spirit is that I can help future users too, with needs equal to mine, but with amounts of data that may vary from one to another.

  • i confess that I really liked all approaches, and the test makes clear the strengths and weaknesses of each implementation in different scenarios, it makes a lot of difference and leaves the question much richer with beyond answers, analysis comparing each of them :)

  • Please avoid long discussions in the comments; your talk was moved to the chat

Show 6 more comments

5 answers

5


One can use the method reduce() to iterate on each element of the array, remove duplicates and add. For example:

listaProdutos = 
[
      {nome:'Teclado',preco:28.00},
      {nome:'Mouse',preco:36.00},
      {nome:'Monitor',preco:500.00},
      {nome:'CPU',preco:800.00},
      {nome:'Teclado',preco:28.00},
      {nome:'Monitor',preco:500.00},
];

const novoLista = listaProdutos.reduce((soma, cur) => {
  // guarda o nome atual e verifica se existe repetido
  let nome = cur.nome;
  let repetido = soma.find(elem => elem.nome === nome)
  // se for repetido soma, caso contrário adiciona o elemento ao novo array
  if (repetido) repetido.preco += cur.preco;
  else soma.push(cur);
  // retorna o elemento agrupado e somado
  return soma;
}, []);

console.log(novoLista);

  • Cool! : ) +1 I only think it is valid to note that as reduce and find have complexity O(n), this code has quadratic complexity, which can be bad in cases of large amount of items.

  • Guys, I’ll check tomorrow afternoon. Today I’m super exhausted. Thank you so much.

  • @Luizfelipe, out of curiosity I created a benchmark between the two solutions and the result is almost the same, being the reduce slightly more performatic: https://jsben.ch/oQc4s

  • 1

    The difference is noticeable with a slightly larger data sample, see. The greater the number of elements, the greater the difference. In small samples, the velocity difference is tiny and makes little difference.

  • That reduce with the find in was quite interesting.

  • All the answers were great, served me and were properly "upvoted" by me. But this was the one that I found simpler and the one that I could understand better. Although I don’t know if it was the most performative. Thank you all!

  • 2

    thanks @Gatodeschrödinger, the tests, which incidentally Luiz Felipe did a great job of putting together and maintaining show that this algorithm I used is the fastest with few elements, I did a test with up to 30 he is the fastest, from there to 100 is already average, and with more than 100 begins to fall performace, being the slowest with 1000 elements up, I leave commented here for those who see this great question and want to try some of the algorithms :)

  • Nothing. I thank you :)

  • This is being very useful to me. If I could, I would give +2. hahaha'

Show 4 more comments

5

You can accumulate each nome single and go adding the preco of each object by nome corresponding. Once the map has been completed, the new array is generated. One way to keep objects from a single key is to use the data structure Map javascript.

Something like that:

function sumByPropName(nameProp, sumProp, arr) {
  const map = new Map();

  for (const obj of arr) {
    // Chave que utilizaremos para o mapa. A `key` corresponde a cada "nome" único.
    const key = obj[nameProp];

    // Caso nenhum objeto já tiver sido registrado para a chave atual, devemos o
    // inserir pela primeira vez.
    if (!map.has(key)) {
      map.set(key, obj);

      // Podemos pular para a próxima iteração, já que o valor correspondente
      // já está incluído no `obj`.
      continue;
    }

    // Caos já tenha sido registrado, é só somar ao valor já armazenado.
    map.get(key)[sumProp] += obj[sumProp];
  }

  // Converter o mapa em array:
  return [...map.values()];
}

const original = [
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Mouse', preco: 36.0 },
  { nome: 'Monitor', preco: 500.0 },
  { nome: 'CPU', preco: 800.0 },
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Monitor', preco: 500.0 }
];

// Queremos identificar elementos únicos pela propriedade `nome` (1º arg.)
// O somatório corresponde à propriedade `preco` (2º arg.)
console.log(sumByPropName('nome', 'preco', original));

The complexity of the above function would be something like O(n + m), being n the number of elements of the original array and m the number of nomes only.

  • The n comes from for (which is indispensable in this type of situation).
  • The m (which is often tiny and can be disregarded) comes from the need to convert the map to array at the end of the function.

If Map cannot be used (was introduced in Ecmascript 2015), can be used Object.create(null) as a substitute.

I already tried to explain directly by the code, any doubt leave in the comments I try to edit to clarify.

  • Guys, I’ll check tomorrow afternoon. Today I’m super exhausted. Thank you so much.

5

I will share my solution, this being, in my view, a decent alternative to the other answers.

Code:

const listaProdutos = [
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Mouse', preco: 36.0 },
  { nome: 'Monitor', preco: 500.0 },
  { nome: 'CPU', preco: 800.0 },
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Monitor', preco: 500.0 },
];

const resObject = {};

listaProdutos.forEach((elem) => {
  const valorAnterior = resObject[elem.nome] || 0;

  resObject[elem.nome] = valorAnterior + elem.preco;
});

const novoLista = Object.keys(resObject).map((key) => {
  return { nome: key, preco: resObject[key] };
});

console.log(novoLista);

The explanations:

  • const resObject = {}; is a temporary object to save the value of nome as a key of this object, in addition to maintaining an accumulated value of each preco from the list of products.

  • forEach to iterate on the list elements.

  • valorAnterior receives the value already saved on resObject according to the key (Teclado, Mouse, etc...). If the key does not exist, we adopt the value 0.

  • resObject[elem.nome] = valorAnterior + elem.preco; basically add a key to the object resObject with an initial value according to elem.preco. If the key already exists, it already has a value, then we will make a sum.

At this point, if you give a console.log, will see the object resObject with the keys with the names of the listed product names and their values already summed.

const listaProdutos = [
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Mouse', preco: 36.0 },
  { nome: 'Monitor lcd', preco: 500.0 },
  { nome: 'CPU', preco: 800.0 },
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Monitor', preco: 500.0 },
];

const resObject = {};

listaProdutos.forEach((elem) => {
  const valorAnterior = resObject[elem.nome] || 0;

  resObject[elem.nome] = valorAnterior + elem.preco;
});

console.log(resObject)

From now on, what we do is associate the key to the resObject for an object with the property nome:

 return { nome: key, ...}

and the preco will be the value of this key using resObject[key]:

return { nome: key, preco: resObject[key] };

I added a toFixed just to stay as you wish to novoLista:

const listaProdutos = [
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Mouse', preco: 36.0 },
  { nome: 'Monitor lcd', preco: 500.0 },
  { nome: 'CPU', preco: 800.0 },
  { nome: 'Teclado', preco: 28.0 },
  { nome: 'Monitor', preco: 500.0 },
];

const resObject = {};

listaProdutos.forEach((elem) => {
  const valorAnterior = resObject[elem.nome] || 0;

  resObject[elem.nome] = valorAnterior + elem.preco;
});

const novoLista = Object.keys(resObject).map((key) => {
  return { nome: key, preco: parseFloat(resObject[key]).toFixed(2) };
});


console.log(novoLista);

  • 1

    Cool! I put in benchmark not to be left out. :-)

  • 1

    good, I think will be very good this question, with various solutions and benchmark :)

  • Thank you, I’ll check them all.

4

Still on the subject Map another approach may be used.

Make it an instance of Map an index for the elements of Array novoLista which will initially be populated with a copy of the first object in the list listaProdutos whose property nome has not yet been indexed. If an index is already owned nome just add up preço at the preço of the already indexed element.

let listaProdutos = 
[
      {nome:'Teclado',preco:28.00},
      {nome:'Mouse',preco:36.00},
      {nome:'Monitor',preco:500.00},
      {nome:'CPU',preco:800.00},
      {nome:'Teclado',preco:28.00},
      {nome:'Monitor',preco:500.00},
];

let novoLista = [];

let m = new Map();

//Para todos os elementos de listaProdutos...
for(let prod of listaProdutos){
  //Verifica se índice contiver a chave prod.nome...
  if (m.has(prod.nome)){
    //No item indexado por prod.nome no array novoLista incrementa o preço registrado com o novo preço.
    novoLista[m.get(prod.nome)].preco += prod.preco;                          
  } else {
    //Cria um índice nomeado pelo valor de prod.nome apontando para o mais novo elemento do array novoLista
    m.set(prod.nome, novoLista.push({nome:prod.nome,preco:prod.preco}) - 1);  
  }
}

console.log(novoLista);

Thank you to Luiz Felipe which put the above code in the form of a function:

let listaProdutos = 
[
  {nome:'Teclado',preco:28.00},
  {nome:'Mouse',preco:36.00},
  {nome:'Monitor',preco:500.00},
  {nome:'CPU',preco:800.00},
  {nome:'Teclado',preco:28.00},
  {nome:'Monitor',preco:500.00},
];

function somar(arr) {
  let novoLista = [];
  let m = new Map();
  for (let prod of arr) {
    if (m.has(prod.nome)) {
      novoLista[m.get(prod.nome)].preco += prod.preco;
    } else {
      m.set(prod.nome, novoLista.push({
        nome: prod.nome,
        preco: prod.preco
      }) - 1);
    }
  }
  return novoLista;
}

console.log(somar(listaProdutos))

  • 1

    I also put in the benchmark.

  • 1

    Thank you, I’ll check them all.

  • 1

    @Luizfelipe I picked up your benchmark code. I hope there are no problems. When I wrote this answer I was very tired. I’d been up all night fighting with codepoints and that’s when I found a answer here on the site I brought to solve my problem.

4

const listaProdutos = [{
    nome: 'Teclado',
    preco: 28.00
  },
  {
    nome: 'Mouse',
    preco: 36.00
  },
  {
    nome: 'Monitor',
    preco: 500.00
  },
  {
    nome: 'CPU',
    preco: 800.00
  },
  {
    nome: 'Teclado',
    preco: 28.00
  },
  {
    nome: 'Monitor',
    preco: 500.00
  },
];

// Agrupe
const groups = listaProdutos.reduce((acc, current) => {
  if (!acc[current.nome]) {
    acc[current.nome] = { ...current
    }

    return acc
  }

  acc[current.nome].preco = acc[current.nome].preco + current.preco

  return acc
}, {})

// Extraia a lista 
const newList = Object.values(groups)
console.log(newList)

  • I also put in the benchmark.

  • From what I could see, that was the fastest, right ?

  • it would be nice to add comments or in question explain how you did to group

  • I’m going to pick the answer tomorrow. Man, the job is sucking me in an absurd way. By a wonderful chance, all the answers have served me. I can’t even choose one. If I could I would choose all of them. Thank you very much.

Browser other questions tagged

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