Como agrupar uma matriz de objetos por chave

153

Alguém sabe de uma maneira (se possível também) de agrupar uma matriz de objetos por uma chave de objeto e criar uma nova matriz de objetos com base no agrupamento? Por exemplo, eu tenho uma matriz de objetos de carro:

var cars = [
    {
        'make': 'audi',
        'model': 'r8',
        'year': '2012'
    }, {
        'make': 'audi',
        'model': 'rs5',
        'year': '2013'
    }, {
        'make': 'ford',
        'model': 'mustang',
        'year': '2012'
    }, {
        'make': 'ford',
        'model': 'fusion',
        'year': '2015'
    }, {
        'make': 'kia',
        'model': 'optima',
        'year': '2012'
    },
];

Quero criar uma nova matriz de objetos de carro agrupados por make:

var cars = {
    'audi': [
        {
            'model': 'r8',
            'year': '2012'
        }, {
            'model': 'rs5',
            'year': '2013'
        },
    ],

    'ford': [
        {
            'model': 'mustang',
            'year': '2012'
        }, {
            'model': 'fusion',
            'year': '2015'
        }
    ],

    'kia': [
        {
            'model': 'optima',
            'year': '2012'
        }
    ]
}
Trung Tran
fonte
1
Você olhou groupBy?
SLaks
2
seu resultado não é válido.
Nina Scholz
Existe uma abordagem semelhante para obter um mapa em vez de um objeto?
Andrea Bergonzo

Respostas:

104

A resposta de Timo é como eu faria isso. Simples _.groupBye permita algumas duplicações nos objetos na estrutura agrupada.

No entanto, o OP também solicitou a makeremoção das chaves duplicadas . Se você quisesse ir até o fim:

var grouped = _.mapValues(_.groupBy(cars, 'make'),
                          clist => clist.map(car => _.omit(car, 'make')));

console.log(grouped);

Rendimentos:

{ audi:
   [ { model: 'r8', year: '2012' },
     { model: 'rs5', year: '2013' } ],
  ford:
   [ { model: 'mustang', year: '2012' },
     { model: 'fusion', year: '2015' } ],
  kia: [ { model: 'optima', year: '2012' } ] }

Se você quiser fazer isso usando Underscore.js, observe que sua versão _.mapValuesé chamada _.mapObject.

Jonathan Eunice
fonte
278

Em Javascript simples, você pode usar Array#reducecom um objeto

var cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }],
    result = cars.reduce(function (r, a) {
        r[a.make] = r[a.make] || [];
        r[a.make].push(a);
        return r;
    }, Object.create(null));

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

Nina Scholz
fonte
1
como posso iterar resultresultados?
Mounir Elfassi
1
você pode levar as entradas com Object.entriese percorrer os pares chave / valor.
Nina Scholz
Existe uma maneira de remover o make conjunto de dados depois de agrupado? Está ocupando espaço extra.
Mercurial
sim, por Rest in Destructuring Object .
Nina Scholz
O que r e um representam? Seria correto assumir que r é o acumulador e o valor atual?
Omar
68

Você está procurando _.groupBy().

A remoção da propriedade que você está agrupando dos objetos deve ser trivial, se necessário:

var cars = [{'make':'audi','model':'r8','year':'2012'},{'make':'audi','model':'rs5','year':'2013'},{'make':'ford','model':'mustang','year':'2012'},{'make':'ford','model':'fusion','year':'2015'},{'make':'kia','model':'optima','year':'2012'},];

var grouped = _.groupBy(cars, function(car) {
  return car.make;
});

console.log(grouped);
<script src='https://cdn.jsdelivr.net/lodash/4.17.2/lodash.min.js'></script>


Como bônus, você obtém uma sintaxe ainda melhor com as funções de seta do ES6:

const grouped = _.groupBy(cars, car => car.make);
Timo
fonte
18
E se você desejar ainda mais curto, não var grouped = _.groupBy(cars, 'make');há necessidade de uma função, se o acessador for um nome de propriedade simples.
Jonathan Eunice
1
'_' Significa?
Adrian Grzywaczewski
@AdrianGrzywaczewski, era a convenção padrão para espaçamento entre nomes 'lodash' ou 'sublinhado'. Agora que os librairies são modulares, não é mais necessário, ou seja. npmjs.com/package/lodash.groupby
vilsbole
5
E como posso interagir no resultado?
Luis Antonio Pestana
36

A versão curta para agrupar uma matriz de objetos por uma certa chave no es6:

result = array.reduce((h, obj) => Object.assign(h, { [obj.key]:( h[obj.key] || [] ).concat(obj) }), {})

A versão mais longa:

result = array.reduce(function(h, obj) {
  h[obj.key] = (h[obj.key] || []).concat(obj);
  return h; 
}, {})

Parece que a pergunta original pergunta como agrupar carros por marca, mas omita a marca em cada grupo. Portanto, a resposta seria assim:

result = cars.reduce((h, {model,year,make}) => {
  return Object.assign(h, { [make]:( h[make] || [] ).concat({model,year})})
}, {})
metakungfu
fonte
este não é definitivamente ES5
Shinigami
É apenas funciona! Alguém pode elaborar essa função de redução?
Jeevan
Gostei das duas respostas, mas vejo que elas fornecem o campo "make" como membro de cada matriz "make". Forneci uma resposta com base na sua, onde a saída entregue corresponde à saída esperada. Obrigado!
Daniel Vukasovich
15

Aqui está sua própria groupByfunção, que é uma generalização do código em: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore

function groupBy(xs, f) {
  return xs.reduce((r, v, i, a, k = f(v)) => ((r[k] || (r[k] = [])).push(v), r), {});
}

const cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }];

const result = groupBy(cars, (c) => c.make);
console.log(result);

cdiggins
fonte
15

var cars = [{
  make: 'audi',
  model: 'r8',
  year: '2012'
}, {
  make: 'audi',
  model: 'rs5',
  year: '2013'
}, {
  make: 'ford',
  model: 'mustang',
  year: '2012'
}, {
  make: 'ford',
  model: 'fusion',
  year: '2015'
}, {
  make: 'kia',
  model: 'optima',
  year: '2012'
}].reduce((r, car) => {

  const {
    model,
    year,
    make
  } = car;

  r[make] = [...r[make] || [], {
    model,
    year
  }];

  return r;
}, {});

console.log(cars);

G.aziz
fonte
8

Gostaria de deixar REAL GROUP BYpara o exemplo JS Arrays exatamente o mesmo desta tarefa aqui

const inputArray = [ 
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
    { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 1", Value: "15" },
    { Phase: "Phase 1", Step: "Step 2", Task: "Task 2", Value: "20" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 1", Value: "25" },
    { Phase: "Phase 2", Step: "Step 1", Task: "Task 2", Value: "30" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 1", Value: "35" },
    { Phase: "Phase 2", Step: "Step 2", Task: "Task 2", Value: "40" }
];

var outObject = inputArray.reduce(function(a, e) {
  // GROUP BY estimated key (estKey), well, may be a just plain key
  // a -- Accumulator result object
  // e -- sequentally checked Element, the Element that is tested just at this itaration

  // new grouping name may be calculated, but must be based on real value of real field
  let estKey = (e['Phase']); 

  (a[estKey] ? a[estKey] : (a[estKey] = null || [])).push(e);
  return a;
}, {});

console.log(outObject);

SynCap
fonte
7

Você pode tentar modificar o objeto dentro da função chamada por iteração por _.groupBy func. Observe que a matriz de origem altera seus elementos!

var res = _.groupBy(cars,(car)=>{
    const makeValue=car.make;
    delete car.make;
    return makeValue;
})
console.log(res);
console.log(cars);
nickxbs
fonte
1
Embora esse código possa resolver a questão, incluir uma explicação de como e por que isso resolve o problema realmente ajudaria a melhorar a qualidade da sua postagem. Lembre-se de que você está respondendo à pergunta dos leitores no futuro, não apenas à pessoa que está perguntando agora! Edite sua resposta para adicionar uma explicação e forneça uma indicação de quais limitações e suposições se aplicam.
Makyen
Parece que a melhor resposta para mim é que você percorre a matriz apenas uma vez para obter o resultado desejado. Não há necessidade de usar outra função para remover a makepropriedade, e é mais legível também.
Carrm
7

Também é possível com um forloop simples :

 const result = {};

 for(const {make, model, year} of cars) {
   if(!result[make]) result[make] = [];
   result[make].push({ model, year });
 }
Jonas Wilms
fonte
E provavelmente mais rápido também e mais simples. Expandi seu snippet para ficar um pouco mais dinâmico, pois tinha uma longa lista de campos de uma tabela db que não queria digitar. Observe também que você precisará substituir const por let. for ( let { TABLE_NAME, ...fields } of source) { result[TABLE_NAME] = result[TABLE_NAME] || []; result[TABLE_NAME].push({ ...fields }); }
Adrien
Até, obrigado! medium.com/@mautayro/…
adrien
5

Nos casos em que a chave pode ser nula e queremos agrupá-los como outros

var cars = [{'make':'audi','model':'r8','year':'2012'},{'make':'audi','model':'rs5','year':'2013'},{'make':'ford','model':'mustang','year':'2012'},{'make':'ford','model':'fusion','year':'2015'},{'make':'kia','model':'optima','year':'2012'},
            {'make':'kia','model':'optima','year':'2033'},
            {'make':null,'model':'zen','year':'2012'},
            {'make':null,'model':'blue','year':'2017'},

           ];


 result = cars.reduce(function (r, a) {
        key = a.make || 'others';
        r[key] = r[key] || [];
        r[key].push(a);
        return r;
    }, Object.create(null));
exexziano
fonte
4

Crie um método que possa ser reutilizado

Array.prototype.groupBy = function(prop) {
      return this.reduce(function(groups, item) {
        const val = item[prop]
        groups[val] = groups[val] || []
        groups[val].push(item)
        return groups
      }, {})
    };

Abaixo, você pode agrupar por qualquer critério

const groupByMake = cars.groupBy('make');
        console.log(groupByMake);

var cars = [
    {
        'make': 'audi',
        'model': 'r8',
        'year': '2012'
    }, {
        'make': 'audi',
        'model': 'rs5',
        'year': '2013'
    }, {
        'make': 'ford',
        'model': 'mustang',
        'year': '2012'
    }, {
        'make': 'ford',
        'model': 'fusion',
        'year': '2015'
    }, {
        'make': 'kia',
        'model': 'optima',
        'year': '2012'
    },
];
  //re-usable method
Array.prototype.groupBy = function(prop) {
	  return this.reduce(function(groups, item) {
		const val = item[prop]
		groups[val] = groups[val] || []
		groups[val].push(item)
		return groups
	  }, {})
	};
  
 // initiate your groupBy. Notice the recordset Cars and the field Make....
  const groupByMake = cars.groupBy('make');
		console.log(groupByMake);
    
    //At this point we have objects. You can use Object.keys to return an array

Wahinya Brian
fonte
3

Versão do protótipo usando o ES6 também. Basicamente, isso usa a função de redução para passar um acumulador e um item atual, que depois usa isso para criar suas matrizes "agrupadas" com base na chave passada. a parte interna da redução pode parecer complicada, mas essencialmente ele está testando para ver se a chave do objeto passado existe e se não criar uma matriz vazia e anexar o item atual à nova matriz criada usando a propagação O operador passa todos os objetos da matriz de chaves atual e acrescenta o item atual. Espero que isso ajude alguém!

Array.prototype.groupBy = function(k) {
  return this.reduce((acc, item) => ((acc[item[k]] = [...(acc[item[k]] || []), item]), acc),{});
};

const projs = [
  {
    project: "A",
    timeTake: 2,
    desc: "this is a description"
  },
  {
    project: "B",
    timeTake: 4,
    desc: "this is a description"
  },
  {
    project: "A",
    timeTake: 12,
    desc: "this is a description"
  },
  {
    project: "B",
    timeTake: 45,
    desc: "this is a description"
  }
];

console.log(projs.groupBy("project"));
Azayda
fonte
1

Você também pode usar um array#forEach()método como este:

const cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }];

let newcars = {}

cars.forEach(car => {
  newcars[car.make] ? // check if that array exists or not in newcars object
    newcars[car.make].push({model: car.model, year: car.year})  // just push
   : (newcars[car.make] = [], newcars[car.make].push({model: car.model, year: car.year})) // create a new array and push
})

console.log(newcars);

Barba Negra
fonte
1
function groupBy(data, property) {
  return data.reduce((acc, obj) => {
    const key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}
groupBy(people, 'age');
sama vamsi
fonte
1

Apenas tente este que funciona bem para mim.

let grouped = _.groupBy(cars, 'make');

agravat.in
fonte
2
ReferenceError não capturado: _ não está definido - você deve ter certeza de que sua solução exige a instalação de uma biblioteca de terceiros apenas para resolver isso.
metakungfu
1
desculpe, acho que todo mundo sabe. _ stands e usado principalmente para lodash lib. então você precisa usar o lodash. por favor leia a pergunta para saber que ele / ela está pedindo lodash. bem obrigado. Vou lembrar disso. e nunca se esqueça de escrever lib.
precisa saber é o seguinte
1

Fiz uma referência para testar o desempenho de cada solução que não usa bibliotecas externas.

JSBen.ch

A reduce()opção, postada por @Nina Scholz, parece ser a melhor.

leonardofmed
fonte
0

Eu gostei da resposta @metakunfu, mas ela não fornece exatamente a saída esperada. Aqui está uma atualização que se livra de "make" na carga final JSON.

var cars = [
    {
        'make': 'audi',
        'model': 'r8',
        'year': '2012'
    }, {
        'make': 'audi',
        'model': 'rs5',
        'year': '2013'
    }, {
        'make': 'ford',
        'model': 'mustang',
        'year': '2012'
    }, {
        'make': 'ford',
        'model': 'fusion',
        'year': '2015'
    }, {
        'make': 'kia',
        'model': 'optima',
        'year': '2012'
    },
];

result = cars.reduce((h, car) => Object.assign(h, { [car.make]:( h[car.make] || [] ).concat({model: car.model, year: car.year}) }), {})

console.log(JSON.stringify(result));

Resultado:

{  
   "audi":[  
      {  
         "model":"r8",
         "year":"2012"
      },
      {  
         "model":"rs5",
         "year":"2013"
      }
   ],
   "ford":[  
      {  
         "model":"mustang",
         "year":"2012"
      },
      {  
         "model":"fusion",
         "year":"2015"
      }
   ],
   "kia":[  
      {  
         "model":"optima",
         "year":"2012"
      }
   ]
}
Daniel Vukasovich
fonte
0

Com o lodash / fp, você pode criar uma função com _.flow()esse primeiro grupo por uma tecla, mapear cada grupo e omitir uma tecla de cada item:

const { flow, groupBy, mapValues, map, omit } = _;

const groupAndOmitBy = key => flow(
  groupBy(key),
  mapValues(map(omit(key)))
);

const cars = [{ make: 'audi', model: 'r8', year: '2012' }, { make: 'audi', model: 'rs5', year: '2013' }, { make: 'ford', model: 'mustang', year: '2012' }, { make: 'ford', model: 'fusion', year: '2015' }, { make: 'kia', model: 'optima', year: '2012' }];

const groupAndOmitMake = groupAndOmitBy('make');

const result = groupAndOmitMake(cars);

console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src='https://cdn.jsdelivr.net/g/lodash@4(lodash.min.js+lodash.fp.min.js)'></script>

Ori Drori
fonte
0

Com base na resposta de @Jonas_Wilms, se você não quiser digitar todos os seus campos:

    var result = {};

    for ( let { first_field, ...fields } of your_data ) 
    { 
       result[first_field] = result[first_field] || [];
       result[first_field].push({ ...fields }); 
    }

Não fiz nenhum benchmark, mas acredito que usar um loop for seria mais eficiente do que qualquer coisa sugerida nesta resposta também.

Adrien
fonte
0
const reGroup = (list, key) => {
    const newGroup = {};
    list.forEach(item => {
        const newItem = Object.assign({}, item);
        delete newItem[key];
        newGroup[item[key]] = newGroup[item[key]] || [];
        newGroup[item[key]].push(newItem);
    });
    return newGroup;
};
const animals = [
  {
    type: 'dog',
    breed: 'puddle'
  },
  {
    type: 'dog',
    breed: 'labradoodle'
  },
  {
    type: 'cat',
    breed: 'siamese'
  },
  {
    type: 'dog',
    breed: 'french bulldog'
  },
  {
    type: 'cat',
    breed: 'mud'
  }
];
console.log(reGroup(animals, 'type'));
const cars = [
  {
      'make': 'audi',
      'model': 'r8',
      'year': '2012'
  }, {
      'make': 'audi',
      'model': 'rs5',
      'year': '2013'
  }, {
      'make': 'ford',
      'model': 'mustang',
      'year': '2012'
  }, {
      'make': 'ford',
      'model': 'fusion',
      'year': '2015'
  }, {
      'make': 'kia',
      'model': 'optima',
      'year': '2012'
  },
];

console.log(reGroup(cars, 'make'));
AKelley
fonte
0

Matriz de objeto agrupada em texto datilografado com isso:

groupBy (list: any[], key: string): Map<string, Array<any>> {
    let map = new Map();
    list.map(val=> {
        if(!map.has(val[key])){
            map.set(val[key],list.filter(data => data[key] == val[key]));
        }
    });
    return map;
});
Oluwafisayo Owolo
fonte
Isso parece ineficiente quando você faz uma pesquisa para cada chave. A pesquisa provavelmente tem uma complexidade de O (n).
Leukipp
0

Adoro escrever sem dependência / complexidade, apenas com js simples e puros.

const mp = {}
const cars = [
  {
    model: 'Imaginary space craft SpaceX model',
    year: '2025'
  },
  {
    make: 'audi',
    model: 'r8',
    year: '2012'
  },
  {
    make: 'audi',
    model: 'rs5',
    year: '2013'
  },
  {
    make: 'ford',
    model: 'mustang',
    year: '2012'
  },
  {
    make: 'ford',
    model: 'fusion',
    year: '2015'
  },
  {
    make: 'kia',
    model: 'optima',
    year: '2012'
  }
]

cars.forEach(c => {
  if (!c.make) return // exit (maybe add them to a "no_make" category)

  if (!mp[c.make]) mp[c.make] = [{ model: c.model, year: c.year }]
  else mp[c.make].push({ model: c.model, year: c.year })
})

console.log(mp)

Mohamed Abu Galala
fonte
-1

Aqui está outra solução para isso. Como pedido.

Quero criar uma nova matriz de objetos de carros agrupados por make:

function groupBy() {
  const key = 'make';
  return cars.reduce((acc, x) => ({
    ...acc,
    [x[key]]: (!acc[x[key]]) ? [{
      model: x.model,
      year: x.year
    }] : [...acc[x[key]], {
      model: x.model,
      year: x.year
    }]
  }), {})
}

Resultado:

console.log('Grouped by make key:',groupBy())
Eugen Sunic
fonte
-1

Aqui está uma solução inspirada em Collectors.groupingBy () em Java:

function groupingBy(list, keyMapper) {
  return list.reduce((accummalatorMap, currentValue) => {
    const key = keyMapper(currentValue);
    if(!accummalatorMap.has(key)) {
      accummalatorMap.set(key, [currentValue]);
    } else {
      accummalatorMap.set(key, accummalatorMap.get(key).push(currentValue));
    }
    return accummalatorMap;
  }, new Map());
}

Isso dará um objeto de mapa.

// Usage

const carMakers = groupingBy(cars, car => car.make);

Rahul Sethi
fonte