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'
        }
    ]
}

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.


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; }


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


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);

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})})
}, {})

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);


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);


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);


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);

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 });
 }

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


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));

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


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"));

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);


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');

1

Apenas tente este que funciona bem para mim.

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


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.


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"
      }
   ]
}

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>


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.


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'));

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;
});

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)


-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())

-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);

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.