Método mais eficiente para agrupar em uma matriz de objetos

507

Qual é a maneira mais eficiente de agrupar objetos em uma matriz?

Por exemplo, dada essa matriz de objetos:

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

Estou exibindo essas informações em uma tabela. Gostaria de agrupar por métodos diferentes, mas quero somar os valores.

Estou usando o Underscore.js para sua função de agrupamento, o que é útil, mas não faz todo o truque, porque não quero que eles sejam "divididos", mas "mesclados", mais como o group bymétodo SQL .

O que eu estou procurando seria capaz de totalizar valores específicos (se solicitado).

Então, se eu fiz groupby Phase, eu gostaria de receber:

[
    { Phase: "Phase 1", Value: 50 },
    { Phase: "Phase 2", Value: 130 }
]

E se eu fiz groupy Phase/ Step, receberia:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 15 },
    { Phase: "Phase 1", Step: "Step 2", Value: 35 },
    { Phase: "Phase 2", Step: "Step 1", Value: 55 },
    { Phase: "Phase 2", Step: "Step 2", Value: 75 }
]

Existe um script útil para isso ou devo usar o Underscore.js e depois percorrer o objeto resultante para fazer os totais sozinho?

Rail24
fonte

Respostas:

755

Se você deseja evitar bibliotecas externas, é possível implementar concisamente uma versão básica do seguinte groupBy()modo:

var groupBy = function(xs, key) {
  return xs.reduce(function(rv, x) {
    (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
  }, {});
};

console.log(groupBy(['one', 'two', 'three'], 'length'));

// => {3: ["one", "two"], 5: ["three"]}

Ceasar Bautista
fonte
18
Eu modificaria desta maneira: `` `return xs.reduce (function (rv, x) {var v = instância da chave da Função? key (x): x [chave]; (rv [v] = rv [v] || []). push (x); return rv;}, {}); `` permitindo que as funções de retorno de chamada retornem um critério de classificação
y_nk 6/07/16
110
Aqui está um que gera uma matriz e não um objeto: groupByArray (xs, chave) {retorna xs.reduce (função (rv, x) {let v = instância da função da função? Key (x): x [key]; let el = rv .find ((r) => r &&r.key === v); if (el) {el.values.push (x);} else {rv.push ({chave: v, valores: [x] });} retornar rv;}, []); }
tomitrescak
24
Ótimo, exatamente o que eu precisava. No caso de alguém precisa dele, aqui está a assinatura do texto dactilografado:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Michael Sandino
4
Quanto vale a pena, a solução de tomitrescak, embora conveniente, é significativamente menos eficiente, pois find () provavelmente é O (n). A solução na resposta é O (n), a partir da redução (a atribuição de objeto é O (1), como é push), enquanto o comentário é O (n) * O (n) ou O (n ^ 2) ou em O (nlgn)
narthur157
21
Se alguém estiver interessado, criei uma versão mais legível e anotada dessa função e a coloquei em uma lista: gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d Achei útil entender o que realmente está acontecendo aqui.
robmathers
228

Usando o objeto ES6 Map:

function groupBy(list, keyGetter) {
    const map = new Map();
    list.forEach((item) => {
         const key = keyGetter(item);
         const collection = map.get(key);
         if (!collection) {
             map.set(key, [item]);
         } else {
             collection.push(item);
         }
    });
    return map;
}

// example usage

const pets = [
    {type:"Dog", name:"Spot"},
    {type:"Cat", name:"Tiger"},
    {type:"Dog", name:"Rover"}, 
    {type:"Cat", name:"Leo"}
];
    
const grouped = groupBy(pets, pet => pet.type);
    
console.log(grouped.get("Dog")); // -> [{type:"Dog", name:"Spot"}, {type:"Dog", name:"Rover"}]
console.log(grouped.get("Cat")); // -> [{type:"Cat", name:"Tiger"}, {type:"Cat", name:"Leo"}]
    
    

Sobre o mapa: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

mortb
fonte
ortb @, como obtê-lo sem chamar o get()método? que é que eu quero que a saída seja exibida sem passar a chave
Fai Zal Dong
@FaiZalDong: Não sei o que seria melhor para o seu caso? Se eu escrever console.log(grouped.entries());no exemplo jsfiddle, ele retornará um iterável que se comporta como uma matriz de chaves + valores. Você pode tentar isso e ver se isso ajuda?
mortb
7
Você também pode tentarconsole.log(Array.from(grouped));
mortb
Eu amo esta resposta, muito flexível
benshabatnoam
para ver o número de elementos em grupos:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
Ahmet Şimşek
105

com ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);
Joseph Nields
fonte
3
Demora um pouco para se acostumar, mas o mesmo acontece com a maioria dos modelos de C ++.
Levi Haskell
2
Eu estraguei meu cérebro e ainda não consegui entender como no mundo funciona a partir de ...result. Agora não consigo dormir por causa disso.
8
Elegante, mas dolorosamente lento em matrizes maiores!
Infinity1975
4
lol o que é isso
Nino Škopac 19/02
1
solução fácil. Obrigado
Ezequiel Tavares
59

Embora a resposta linq seja interessante, também é bastante pesada. Minha abordagem é um pouco diferente:

var DataGrouper = (function() {
    var has = function(obj, target) {
        return _.any(obj, function(value) {
            return _.isEqual(value, target);
        });
    };

    var keys = function(data, names) {
        return _.reduce(data, function(memo, item) {
            var key = _.pick(item, names);
            if (!has(memo, key)) {
                memo.push(key);
            }
            return memo;
        }, []);
    };

    var group = function(data, names) {
        var stems = keys(data, names);
        return _.map(stems, function(stem) {
            return {
                key: stem,
                vals:_.map(_.where(data, stem), function(item) {
                    return _.omit(item, names);
                })
            };
        });
    };

    group.register = function(name, converter) {
        return group[name] = function(data, names) {
            return _.map(group(data, names), converter);
        };
    };

    return group;
}());

DataGrouper.register("sum", function(item) {
    return _.extend({}, item.key, {Value: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.Value);
    }, 0)});
});

Você pode vê-lo em ação no JSBin .

Eu não vi nada no Underscore que faça o que hasfaz, embora eu possa estar perdendo. É quase o mesmo que _.contains, mas usa _.isEqualmais do que ===para comparações. Fora isso, o restante é específico do problema, embora com uma tentativa de ser genérico.

Agora DataGrouper.sum(data, ["Phase"])retorna

[
    {Phase: "Phase 1", Value: 50},
    {Phase: "Phase 2", Value: 130}
]

E DataGrouper.sum(data, ["Phase", "Step"])retorna

[
    {Phase: "Phase 1", Step: "Step 1", Value: 15},
    {Phase: "Phase 1", Step: "Step 2", Value: 35},
    {Phase: "Phase 2", Step: "Step 1", Value: 55},
    {Phase: "Phase 2", Step: "Step 2", Value: 75}
]

Mas sumé apenas uma função potencial aqui. Você pode registrar outras pessoas como desejar:

DataGrouper.register("max", function(item) {
    return _.extend({}, item.key, {Max: _.reduce(item.vals, function(memo, node) {
        return Math.max(memo, Number(node.Value));
    }, Number.NEGATIVE_INFINITY)});
});

e agora DataGrouper.max(data, ["Phase", "Step"])vai voltar

[
    {Phase: "Phase 1", Step: "Step 1", Max: 10},
    {Phase: "Phase 1", Step: "Step 2", Max: 20},
    {Phase: "Phase 2", Step: "Step 1", Max: 30},
    {Phase: "Phase 2", Step: "Step 2", Max: 40}
]

ou se você registrou isso:

DataGrouper.register("tasks", function(item) {
    return _.extend({}, item.key, {Tasks: _.map(item.vals, function(item) {
      return item.Task + " (" + item.Value + ")";
    }).join(", ")});
});

então ligar DataGrouper.tasks(data, ["Phase", "Step"])vai te levar

[
    {Phase: "Phase 1", Step: "Step 1", Tasks: "Task 1 (5), Task 2 (10)"},
    {Phase: "Phase 1", Step: "Step 2", Tasks: "Task 1 (15), Task 2 (20)"},
    {Phase: "Phase 2", Step: "Step 1", Tasks: "Task 1 (25), Task 2 (30)"},
    {Phase: "Phase 2", Step: "Step 2", Tasks: "Task 1 (35), Task 2 (40)"}
]

DataGrouperem si é uma função. Você pode chamá-lo com seus dados e uma lista das propriedades pelas quais deseja agrupar. Ele retorna uma matriz cujos elementos são objetos com duas propriedades: keyé a coleção de propriedades agrupadas, valsé uma matriz de objetos que contém as propriedades restantes que não estão na chave. Por exemplo, DataGrouper(data, ["Phase", "Step"])produzirá:

[
    {
        "key": {Phase: "Phase 1", Step: "Step 1"},
        "vals": [
            {Task: "Task 1", Value: "5"},
            {Task: "Task 2", Value: "10"}
        ]
    },
    {
        "key": {Phase: "Phase 1", Step: "Step 2"},
        "vals": [
            {Task: "Task 1", Value: "15"}, 
            {Task: "Task 2", Value: "20"}
        ]
    },
    {
        "key": {Phase: "Phase 2", Step: "Step 1"},
        "vals": [
            {Task: "Task 1", Value: "25"},
            {Task: "Task 2", Value: "30"}
        ]
    },
    {
        "key": {Phase: "Phase 2", Step: "Step 2"},
        "vals": [
            {Task: "Task 1", Value: "35"}, 
            {Task: "Task 2", Value: "40"}
        ]
    }
]

DataGrouper.registeraceita uma função e cria uma nova função que aceita os dados iniciais e as propriedades para agrupar. Essa nova função pega o formato de saída como acima e executa sua função em cada uma delas, retornando uma nova matriz. A função gerada é armazenada como uma propriedade de DataGrouperacordo com o nome fornecido e também retornada se você quiser apenas uma referência local.

Bem, isso é muita explicação. O código é razoavelmente direto, espero!

Scott Sauyet
fonte
Oi .. Posso ver você agrupar e somar apenas por um valor, mas no caso de eu querer somar por valor1 e valor2 e valor3 ... você tem uma solução?
SAMUEL OSPINA
@SAMUELOSPINA você já encontrou uma maneira de fazer isso?
howMuchCheeseIsTooMuchCheese
50

Eu verificaria o grupo lodash ; parece que ele faz exatamente o que você está procurando. Também é bastante leve e muito simples.

Exemplo de violino: https://jsfiddle.net/r7szvt5k/

Desde que o nome do seu array seja arrgroupBy com lodash, é apenas:

import groupBy from 'lodash/groupBy';
// if you still use require:
// const groupBy = require('lodash/groupBy');

const a = groupBy(arr, function(n) {
  return n.Phase;
});
// a is your array grouped by Phase attribute
jmarceli
fonte
1
Esta resposta não é problemática? Há várias maneiras pelas quais o resultado lodash _.groupBy não está no formato do resultado solicitado pelo OP. (1) O resultado não é uma matriz. (2) O "valor" tornou-se a "chave" no resultado do (s) objeto (s) lodash.
mg1075
44

Provavelmente linq.js, isso é feito com mais facilidade , que se destina a ser uma verdadeira implementação do LINQ em JavaScript ( DEMO ):

var linq = Enumerable.From(data);
var result =
    linq.GroupBy(function(x){ return x.Phase; })
        .Select(function(x){
          return {
            Phase: x.Key(),
            Value: x.Sum(function(y){ return y.Value|0; })
          };
        }).ToArray();

resultado:

[
    { Phase: "Phase 1", Value: 50 },
    { Phase: "Phase 2", Value: 130 }
]

Ou, mais simplesmente, usando os seletores baseados em string ( DEMO ):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();
mellamokb
fonte
podemos usar várias propriedades ao agrupar aqui:GroupBy(function(x){ return x.Phase; })
Amit
38

Você pode criar um ES6 a Mappartir de array.reduce().

const groupedMap = initialArray.reduce(
    (entryMap, e) => entryMap.set(e.id, [...entryMap.get(e.id)||[], e]),
    new Map()
);

Isso tem algumas vantagens sobre as outras soluções:

  • Não requer bibliotecas (ao contrário de _.groupBy() )
  • Você obtém um JavaScript em Mapvez de um objeto (por exemplo, conforme retornado por _.groupBy()). Isso tem muitos benefícios , incluindo:
    • ele lembra a ordem em que os itens foram adicionados pela primeira vez,
    • chaves podem ser de qualquer tipo, e não apenas cadeias.
  • A Mapé um resultado mais útil que uma matriz de matrizes. Mas se você deseja uma matriz de matrizes, pode chamar Array.from(groupedMap.entries())(para uma matriz de [key, group array]pares) ou Array.from(groupedMap.values())(para uma matriz simples de matrizes).
  • É bastante flexível; frequentemente, o que você planeja fazer a seguir com este mapa pode ser feito diretamente como parte da redução.

Como um exemplo do último ponto, imagine que eu tenho uma matriz de objetos nos quais eu quero fazer uma fusão (rasa) por id, assim:

const objsToMerge = [{id: 1, name: "Steve"}, {id: 2, name: "Alice"}, {id: 1, age: 20}];
// The following variable should be created automatically
const mergedArray = [{id: 1, name: "Steve", age: 20}, {id: 2, name: "Alice"}]

Para fazer isso, normalmente começaria agrupando por ID e depois mesclando cada uma das matrizes resultantes. Em vez disso, você pode fazer a mesclagem diretamente no reduce():

const mergedArray = Array.from(
    objsToMerge.reduce(
        (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}),
        new Map()
    ).values()
);
Arthur Tacca
fonte
1
Não sei por que isso não tem mais votos. É conciso, legível (para mim) e aparência eficiente. Ele não voar em IE11 , mas o retrofit não é muito difícil ( a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map()), aproximadamente)
unbob
24
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

De: http://underscorejs.org/#groupBy

Julio Marins
fonte
18

Você pode fazer isso com a biblioteca JavaScript do Alasql :

var data = [ { Phase: "Phase 1", Step: "Step 1", Task: "Task 1", Value: "5" },
             { Phase: "Phase 1", Step: "Step 1", Task: "Task 2", Value: "10" }];

var res = alasql('SELECT Phase, Step, SUM(CAST([Value] AS INT)) AS [Value] \
                  FROM ? GROUP BY Phase, Step',[data]);

Experimente este exemplo em jsFiddle .

BTW: Em matrizes grandes (100000 registros e mais) Alasql mais rápido que o Linq. Veja o teste em jsPref .

Comentários:

  • Aqui eu coloco Value entre colchetes, porque VALUE é uma palavra-chave no SQL
  • Eu tenho que usar a função CAST () para converter valores de string em tipo de número.
agershun
fonte
18
Array.prototype.groupBy = function(keyFunction) {
    var groups = {};
    this.forEach(function(el) {
        var key = keyFunction(el);
        if (key in groups == false) {
            groups[key] = [];
        }
        groups[key].push(el);
    });
    return Object.keys(groups).map(function(key) {
        return {
            key: key,
            values: groups[key]
        };
    });
};
cezarypiatek
fonte
15

O MDN tem este exemplo em sua Array.reduce()documentação.

// Grouping objects by a property
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce#Grouping_objects_by_a_property#Grouping_objects_by_a_property

var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// { 
//   20: [
//     { name: 'Max', age: 20 }, 
//     { name: 'Jane', age: 20 }
//   ], 
//   21: [{ name: 'Alice', age: 21 }] 
// }
HoppyKamper
fonte
14

Embora a pergunta tenha algumas respostas e as respostas pareçam um pouco complicadas, sugiro usar o Javascript vanilla para agrupar por com um aninhado (se necessário) Map.

function groupBy(array, groups, valueKey) {
    var map = new Map;
    groups = [].concat(groups);
    return array.reduce((r, o) => {
        groups.reduce((m, k, i, { length }) => {
            var child;
            if (m.has(o[k])) return m.get(o[k]);
            if (i + 1 === length) {
                child = Object
                    .assign(...groups.map(k => ({ [k]: o[k] })), { [valueKey]: 0 });
                r.push(child);
            } else {
                child = new Map;
            }
            m.set(o[k], child);
            return child;
        }, map)[valueKey] += +o[valueKey];
        return r;
    }, [])
};

var data = [{ 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" }];

console.log(groupBy(data, 'Phase', 'Value'));
console.log(groupBy(data, ['Phase', 'Step'], 'Value'));
.as-console-wrapper { max-height: 100% !important; top: 0; }

Nina Scholz
fonte
9

Sem mutações:

const groupBy = (xs, key) => xs.reduce((acc, x) => Object.assign({}, acc, {
  [x[key]]: (acc[x[key]] || []).concat(x)
}), {})

console.log(groupBy(['one', 'two', 'three'], 'length'));
// => {3: ["one", "two"], 5: ["three"]}
Abençoe
fonte
8

Essa solução aceita qualquer função arbitrária (não uma chave), portanto, é mais flexível que as soluções acima e permite funções de seta , semelhantes às expressões lambda usadas no LINQ :

Array.prototype.groupBy = function (funcProp) {
    return this.reduce(function (acc, val) {
        (acc[funcProp(val)] = acc[funcProp(val)] || []).push(val);
        return acc;
    }, {});
};

NOTA: se você deseja estender Arrayo protótipo, é com você.

Exemplo suportado na maioria dos navegadores:

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(function(c){return c.a;})

Exemplo usando funções de seta (ES6):

[{a:1,b:"b"},{a:1,c:"c"},{a:2,d:"d"}].groupBy(c=>c.a)

Os dois exemplos acima retornam:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}
Diego
fonte
Gostei muito da solução ES6. Basta um pouco de semplification sem estender protótipo matriz:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
caneta
8

eu gostaria de sugerir minha abordagem. Primeiro, separe o agrupamento e a agregação. Vamos declarar prototípica a função "agrupar por". É necessária outra função para produzir uma string "hash" para cada elemento da matriz a ser agrupado.

Array.prototype.groupBy = function(hash){
  var _hash = hash ? hash : function(o){return o;};

  var _map = {};
  var put = function(map, key, value){
    if (!map[_hash(key)]) {
        map[_hash(key)] = {};
        map[_hash(key)].group = [];
        map[_hash(key)].key = key;

    }
    map[_hash(key)].group.push(value); 
  }

  this.map(function(obj){
    put(_map, obj, obj);
  });

  return Object.keys(_map).map(function(key){
    return {key: _map[key].key, group: _map[key].group};
  });
}

Quando o agrupamento é concluído, você pode agregar dados conforme necessário, no seu caso

data.groupBy(function(o){return JSON.stringify({a: o.Phase, b: o.Step});})
    /* aggreagating */
    .map(function(el){ 
         var sum = el.group.reduce(
           function(l,c){
             return l + parseInt(c.Value);
           },
           0
         );
         el.key.Value = sum; 
         return el.key;
    });

em comum, funciona. Eu testei esse código no console do chrome. e sinta-se livre para melhorar e encontrar erros;)

Anton
fonte
Obrigado ! Adore a abordagem e atenda perfeitamente às minhas necessidades (não preciso agregar).
precisa saber é
Eu acho que você deseja alterar sua linha em put (): map[_hash(key)].key = key;para map[_hash(key)].key = _hash(key);.
22420 Scotty.NET
6

Imagine que você tem algo parecido com isto:

[{id:1, cat:'sedan'},{id:2, cat:'sport'},{id:3, cat:'sport'},{id:4, cat:'sedan'}]

Ao fazer isso: const categories = [...new Set(cars.map((car) => car.cat))]

Você receberá isto: ['sedan','sport']

Explicação: 1. Primeiro, estamos criando um novo conjunto passando uma matriz. Como Set permite apenas valores exclusivos, todas as duplicatas serão removidas.

  1. Agora que as duplicatas se foram, vamos convertê-lo novamente em uma matriz usando o operador spread ...

Definir Doc: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set Spread OperatorDoc: https://developer.mozilla.org/en-US/docs/Web/JavaScript / Referência / Operadores / Spread_syntax

Yago Gehres
fonte
Eu gosto muito da sua resposta, é a mais curta, mas ainda não entendo a lógica, principalmente, quem faz o agrupamento aqui? é operador de propagação (...)? ou o 'novo conjunto ()'? por favor, explique-nos ... obrigado
Ivan
1
1. Primeiro, estamos criando um novo conjunto passando uma matriz. Como Set permite apenas valores exclusivos, todas as duplicatas serão removidas. 2. Agora que as duplicatas acabaram, vamos convertê-lo novamente em uma matriz usando o operador spread ... Set Doc: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Spread Operador: developer.mozilla.org/pt-BR/docs/Web/JavaScript/Reference/…
Yago Gehres
ok, entendi ... obrigado pela explicação senhor :)
Ivan
de nada!
Yago Gehres
6

Resposta verificada - apenas agrupamento superficial. É muito bom entender como reduzir. A pergunta também fornece o problema de cálculos agregados adicionais.

Aqui está um REAL GROUP BY para Array of Objects por alguns campos com 1) nome de chave calculado e 2) solução completa para cascata de agrupamento, fornecendo a lista das chaves desejadas e convertendo seus valores exclusivos em chaves raiz como SQL GROUP BY faz.

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

Jogue com estKey- você pode agrupar por mais de um campo, adicionar agregações, cálculos ou outro processamento adicional.

Além disso, você pode agrupar dados recursivamente. Por exemplo, agrupe inicialmente por Phase, depois por Stepcampo e assim por diante. Além disso, remova os dados de repouso gordo.

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

/**
 * Small helper to get SHALLOW copy of obj WITHOUT prop
 */
const rmProp = (obj, prop) => ( (({[prop]:_, ...rest})=>rest)(obj) )

/**
 * Group Array by key. Root keys of a resulting array is value
 * of specified key.
 *
 * @param      {Array}   src     The source array
 * @param      {String}  key     The by key to group by
 * @return     {Object}          Object with groupped objects as values
 */
const grpBy = (src, key) => src.reduce((a, e) => (
  (a[e[key]] = a[e[key]] || []).push(rmProp(e, key)),  a
), {});

/**
 * Collapse array of object if it consists of only object with single value.
 * Replace it by the rest value.
 */
const blowObj = obj => Array.isArray(obj) && obj.length === 1 && Object.values(obj[0]).length === 1 ? Object.values(obj[0])[0] : obj;

/**
 * Recoursive groupping with list of keys. `keyList` may be an array
 * of key names or comma separated list of key names whom UNIQUE values will
 * becomes the keys of the resulting object.
 */
const grpByReal = function (src, keyList) {
  const [key, ...rest] = Array.isArray(keyList) ? keyList : String(keyList).trim().split(/\s*,\s*/);
  const res = key ? grpBy(src, key) : [...src];
  if (rest.length) {
for (const k in res) {
  res[k] = grpByReal(res[k], rest)
}
  } else {
for (const k in res) {
  res[k] = blowObj(res[k])
}
  }
  return res;
}

console.log( JSON.stringify( grpByReal(inputArray, 'Phase, Step, Task'), null, 2 ) );

SynCap
fonte
5
groupByArray(xs, key) {
    return xs.reduce(function (rv, x) {
        let v = key instanceof Function ? key(x) : x[key];
        let el = rv.find((r) => r && r.key === v);
        if (el) {
            el.values.push(x);
        }
        else {
            rv.push({
                key: v,
                values: [x]
            });
        }
        return rv;
    }, []);
}

Este gera um array.

tomitrescak
fonte
4

Com base em respostas anteriores

const groupBy = (prop) => (xs) =>
  xs.reduce((rv, x) =>
    Object.assign(rv, {[x[prop]]: [...(rv[x[prop]] || []), x]}), {});

e é um pouco mais agradável olhar com sintaxe de propagação de objeto, se o seu ambiente suportar.

const groupBy = (prop) => (xs) =>
  xs.reduce((acc, x) => ({
    ...acc,
    [ x[ prop ] ]: [...( acc[ x[ prop ] ] || []), x],
  }), {});

Aqui, nosso redutor pega o valor de retorno parcialmente formado (começando com um objeto vazio) e retorna um objeto composto pelos membros espalhados do valor de retorno anterior, juntamente com um novo membro cuja chave é calculada a partir do valor atual do iteree em prope cujo valor é uma lista de todos os valores para esse suporte, juntamente com o valor atual.

Benny Powers
fonte
3

Array.prototype.groupBy = function (groupingKeyFn) {
    if (typeof groupingKeyFn !== 'function') {
        throw new Error("groupBy take a function as only parameter");
    }
    return this.reduce((result, item) => {
        let key = groupingKeyFn(item);
        if (!result[key])
            result[key] = [];
        result[key].push(item);
        return result;
    }, {});
}

var a = [
	{type: "video", name: "a"},
  {type: "image", name: "b"},
  {type: "video", name: "c"},
  {type: "blog", name: "d"},
  {type: "video", name: "e"},
]
console.log(a.groupBy((item) => item.type));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

Jean-Philippe
fonte
3

Aqui está uma solução desagradável e difícil de ler usando o ES6:

export default (arr, key) => 
  arr.reduce(
    (r, v, _, __, k = v[key]) => ((r[k] || (r[k] = [])).push(v), r),
    {}
  );

Para aqueles perguntando como é que este mesmo trabalho, aqui está uma explicação:

  • Em ambos =>você tem um livrereturn

  • A Array.prototype.reducefunção leva até 4 parâmetros. É por isso que um quinto parâmetro está sendo adicionado para que possamos ter uma declaração de variável barata para o grupo (k) no nível da declaração de parâmetro usando um valor padrão. (sim, isso é feitiçaria)

  • Se nosso grupo atual não existir na iteração anterior, criamos uma nova matriz vazia. ((r[k] || (r[k] = []))Isso avaliará a expressão mais à esquerda, ou seja, uma matriz existente ou vazia , é por isso que existe uma pushexpressão imediata após essa expressão, porque de qualquer maneira, você obterá uma matriz.

  • Quando houver um return, o ,operador de vírgula descartará o valor mais à esquerda, retornando o grupo anterior ajustado para esse cenário.

Uma versão mais fácil de entender que faz o mesmo é:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});
darkndream
fonte
2
Gostaria de explicar isso um pouco, ele funciona perfeito
Nuwan Dammika
@NuwanDammika - Nos dois => você tem um "retorno" gratuito - A função reduzir leva até 4 parâmetros. É por isso que um quinto parâmetro está sendo adicionado para que possamos ter uma declaração de variável barata para o grupo (k). - Se o valor anterior não tiver nosso grupo atual, criamos um novo grupo vazio ((r [k] || (r [k] = [])) Isso avaliará a expressão mais à esquerda, caso contrário, uma matriz ou um array vazio, é por isso que há um push imediato após essa expressão.- Quando houver um retorno, o operador de vírgula descartará o valor mais à esquerda, retornando o grupo anterior ajustado
darkndream
2

Vamos gerar uma Array.prototype.groupBy()ferramenta genérica . Apenas por variedade, vamos usar a fantasia ES6, o operador de espalhamento para alguns padrões Haskellesque correspondentes em uma abordagem recursiva. Também vamos Array.prototype.groupBy()aceitar aceitar um retorno de chamada que leve o item ( e) o índice ( i) e a matriz aplicada ( a) como argumentos.

Array.prototype.groupBy = function(cb){
                            return function iterate([x,...xs], i = 0, r = [[],[]]){
                                     cb(x,i,[x,...xs]) ? (r[0].push(x), r)
                                                       : (r[1].push(x), r);
                                     return xs.length ? iterate(xs, ++i, r) : r;
                                   }(this);
                          };

var arr = [0,1,2,3,4,5,6,7,8,9],
    res = arr.groupBy(e => e < 5);
console.log(res);

Redu
fonte
2

A resposta de Ceasar é boa, mas funciona apenas para propriedades internas dos elementos dentro da matriz (comprimento em caso de string).

essa implementação funciona mais como: este link

const groupBy = function (arr, f) {
    return arr.reduce((out, val) => {
        let by = typeof f === 'function' ? '' + f(val) : val[f];
        (out[by] = out[by] || []).push(val);
        return out;
    }, {});
};

espero que isto ajude...

Roey
fonte
2

From @mortb, @jmarceli answer e desta publicação ,

Aproveito JSON.stringify()para ser a identidade do VALOR PRIMITIVO várias colunas de do grupo por.

Sem terceiros

function groupBy(list, keyGetter) {
    const map = new Map();
    list.forEach((item) => {
        const key = keyGetter(item);
        if (!map.has(key)) {
            map.set(key, [item]);
        } else {
            map.get(key).push(item);
        }
    });
    return map;
}

const pets = [
    {type:"Dog", age: 3, name:"Spot"},
    {type:"Cat", age: 3, name:"Tiger"},
    {type:"Dog", age: 4, name:"Rover"}, 
    {type:"Cat", age: 3, name:"Leo"}
];

const grouped = groupBy(pets,
pet => JSON.stringify({ type: pet.type, age: pet.age }));

console.log(grouped);

Com terceiros da Lodash

const pets = [
    {type:"Dog", age: 3, name:"Spot"},
    {type:"Cat", age: 3, name:"Tiger"},
    {type:"Dog", age: 4, name:"Rover"}, 
    {type:"Cat", age: 3, name:"Leo"}
];

let rslt = _.groupBy(pets, pet => JSON.stringify(
 { type: pet.type, age: pet.age }));

console.log(rslt);
Pranithan T.
fonte
keyGetter retorna indefinido
Asbar Ali
@AsbarAli Testei meu snippet com o console do Chrome - versão 66.0.3359.139 (versão oficial) (64 bits). E tudo corre bem. Você poderia colocar o ponto de interrupção da depuração e ver por que keyGetter é indefinido. Talvez seja por causa da versão do navegador.
Pranithan T.
2

reduceVersão com base no ES6, com suporte para a função iteratee.

Funciona exatamente como o esperado se a iterateefunção não for fornecida:

const data = [{id: 1, score: 2},{id: 1, score: 3},{id: 2, score: 2},{id: 2, score: 4}]

const group = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});

const groupBy = (arr, k, fn = () => true) => 
  arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});

console.log(group(data, 'id'))     // grouping via `reduce`
console.log(groupBy(data, 'id'))   // same result if `fn` is omitted
console.log(groupBy(data, 'score', x => x > 2 )) // group with the iteratee

No contexto da pergunta do OP:

const data = [ { 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" } ]

const groupBy = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});
const groupWith = (arr, k, fn = () => true) => 
  arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});

console.log(groupBy(data, 'Phase'))
console.log(groupWith(data, 'Value', x => x > 30 ))  // group by `Value` > 30

Outra ES6 versão que inverte o agrupamento e utiliza o valuescomo keyseo keysquanto a grouped values:

const data = [{A: "1"}, {B: "10"}, {C: "10"}]

const groupKeys = arr => 
  arr.reduce((r,c) => (Object.keys(c).map(x => r[c[x]] = [...r[c[x]] || [], x]),r),{});

console.log(groupKeys(data))

Nota: as funções são postadas em sua forma abreviada (uma linha) por questões de brevidade e para relacionar apenas a ideia. Você pode expandi-los e adicionar verificação de erro adicional, etc.

Akrion
fonte
2

Gostaria de verificar declarative-js groupBy , parece fazer exatamente o que você está procurando. Isso é também:

  • muito bom desempenho ( benchmark de desempenho )
  • escrito em texto datilografado, para que todas as digitações sejam incluídas.
  • Não é obrigatório o uso de objetos de matriz de terceiros.
import { Reducers } from 'declarative-js';
import groupBy = Reducers.groupBy;
import Map = Reducers.Map;

const data = [
    { 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" }
];

data.reduce(groupBy(element=> element.Step), Map());
data.reduce(groupBy('Step'), Map());
Pasa89
fonte
1
let groupbyKeys = function(arr, ...keys) {
  let keysFieldName = keys.join();
  return arr.map(ele => {
    let keysField = {};
    keysField[keysFieldName] = keys.reduce((keyValue, key) => {
      return keyValue + ele[key]
    }, "");
    return Object.assign({}, ele, keysField);
  }).reduce((groups, ele) => {
    (groups[ele[keysFieldName]] = groups[ele[keysFieldName]] || [])
      .push([ele].map(e => {
        if (keys.length > 1) {
          delete e[keysFieldName];
        }
        return e;
    })[0]);
    return groups;
  }, {});
};

console.log(groupbyKeys(array, 'Phase'));
console.log(groupbyKeys(array, 'Phase', 'Step'));
console.log(groupbyKeys(array, 'Phase', 'Step', 'Task'));
Tom Jiang
fonte
1

Aqui está uma versão do ES6 que não será interrompida em membros nulos

function groupBy (arr, key) {
  return (arr || []).reduce((acc, x = {}) => ({
    ...acc,
    [x[key]]: [...acc[x[key]] || [], x]
  }), {})
}
bigkahunaburger
fonte
1

Apenas para adicionar à resposta de Scott Sauyet , algumas pessoas estavam perguntando nos comentários como usar sua função para agrupar por valor1, valor2, etc., em vez de agrupar apenas um valor.

Basta editar sua função de soma:

DataGrouper.register("sum", function(item) {
    return _.extend({}, item.key,
        {VALUE1: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.VALUE1);}, 0)},
        {VALUE2: _.reduce(item.vals, function(memo, node) {
        return memo + Number(node.VALUE2);}, 0)}
    );
});

deixando o principal (DataGrouper) inalterado:

var DataGrouper = (function() {
    var has = function(obj, target) {
        return _.any(obj, function(value) {
            return _.isEqual(value, target);
        });
    };

    var keys = function(data, names) {
        return _.reduce(data, function(memo, item) {
            var key = _.pick(item, names);
            if (!has(memo, key)) {
                memo.push(key);
            }
            return memo;
        }, []);
    };

    var group = function(data, names) {
        var stems = keys(data, names);
        return _.map(stems, function(stem) {
            return {
                key: stem,
                vals:_.map(_.where(data, stem), function(item) {
                    return _.omit(item, names);
                })
            };
        });
    };

    group.register = function(name, converter) {
        return group[name] = function(data, names) {
            return _.map(group(data, names), converter);
        };
    };

    return group;
}());
Telho
fonte
1

Com recurso de classificação

export const groupBy = function groupByArray(xs, key, sortKey) {
      return xs.reduce(function(rv, x) {
        let v = key instanceof Function ? key(x) : x[key];
        let el = rv.find(r => r && r.key === v);

        if (el) {
          el.values.push(x);
          el.values.sort(function(a, b) {
            return a[sortKey].toLowerCase().localeCompare(b[sortKey].toLowerCase());
          });
        } else {
          rv.push({ key: v, values: [x] });
        }

        return rv;
      }, []);
    };

Amostra:

var state = [
    {
      name: "Arkansas",
      population: "2.978M",
      flag:
  "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg",
      category: "city"
    },{
      name: "Crkansas",
      population: "2.978M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/9/9d/Flag_of_Arkansas.svg",
      category: "city"
    },
    {
      name: "Balifornia",
      population: "39.14M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/0/01/Flag_of_California.svg",
      category: "city"
    },
    {
      name: "Florida",
      population: "20.27M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Florida.svg",
      category: "airport"
    },
    {
      name: "Texas",
      population: "27.47M",
      flag:
        "https://upload.wikimedia.org/wikipedia/commons/f/f7/Flag_of_Texas.svg",
      category: "landmark"
    }
  ];
console.log(JSON.stringify(groupBy(state,'category','name')));
amorenew
fonte