El método más eficiente para agrupar en una matriz de objetos

507

¿Cuál es la forma más eficiente de agrupar objetos en una matriz?

Por ejemplo, dada esta 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" }
]

Estoy mostrando esta información en una tabla. Me gustaría agrupar por diferentes métodos, pero quiero sumar los valores.

Estoy usando Underscore.js para su función groupby, que es útil, pero no hace todo el truco, porque no quiero que se "dividan" sino que "se fusionen", más como el group bymétodo SQL .

Lo que estoy buscando podría sumar valores específicos (si se solicita).

Entonces, si hice groupby Phase, me gustaría recibir:

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

Y si hiciera groupy Phase/ Step, recibiría:

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

¿Hay un script útil para esto, o debería seguir usando Underscore.js, y luego recorrer el objeto resultante para hacer los totales yo mismo?

Rail24
fuente

Respuestas:

755

Si desea evitar bibliotecas externas, puede implementar de manera concisa una versión estándar de esta groupBy()manera:

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
fuente
18
Lo modificaría de esta manera: `` `return xs.reduce (function (rv, x) {var v = key instanceof Function? key (x): x [key]; (rv [v] = rv [v] || []). push (x); return rv;}, {}); `` `permitir que las funciones de devolución de llamada devuelvan un criterio de clasificación
y_nk
110
Aquí hay uno que genera una matriz y no un objeto: 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 ({clave: v, valores: [x] });} return rv;}, []); }
tomitrescak
24
Genial, justo lo que necesitaba. En caso de que alguien más lo necesite, aquí está la firma de TypeScript:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Michael Sandino
44
Para lo que vale, la solución de tomitrescak, si bien es conveniente, es significativamente menos eficiente, ya que find () es probablemente O (n). La solución en la respuesta es O (n), desde la reducción (la asignación de objetos es O (1), como es push), mientras que el comentario es O (n) * O (n) u O (n ^ 2) o en less O (nlgn)
narthur157
21
Si alguien está interesado, hice una versión más legible y anotada de esta función y la puse en un resumen : gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d Me pareció útil para comprender lo que realmente está sucediendo aquí.
ladrones del
228

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

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

mortb
fuente
@mortb, ¿cómo obtenerlo sin llamar al get()método? lo que quiero es que la salida se muestre sin pasar la clave
Fai Zal Dong
@FaiZalDong: ¿No estoy seguro de cuál sería el mejor para su caso? Si escribo console.log(grouped.entries());en el ejemplo jsfiddle, devuelve un iterable que se comporta como una matriz de claves + valores. ¿Puedes probar eso y ver si te ayuda?
mortb
77
También puedes intentarloconsole.log(Array.from(grouped));
mortb
Me encanta esta respuesta, muy flexible
benshabatnoam
para ver el número de elementos en grupos:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
Ahmet Şimşek
105

con ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);
Joseph Nields
fuente
3
Cuesta un poco acostumbrarse, pero también la mayoría de las plantillas de C ++ también
Levi Haskell el
2
Me rompí el cerebro y aún no entendí cómo funciona desde el mundo ...result. Ahora no puedo dormir por eso.
8
¡Elegante, pero dolorosamente lento en matrices más grandes!
infinity1975
44
jajaja qué es esto
Nino Škopac
1
Solución fácil. Gracias
Ezequiel Tavares
59

Aunque la respuesta de linq es interesante, también es bastante pesada. Mi enfoque es algo 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)});
});

Puedes verlo en acción en JSBin .

No vi nada en Underscore que haga lo que hashace, aunque podría estar perdiéndolo. Es casi lo mismo que _.contains, pero utiliza en _.isEquallugar de ===comparaciones. Aparte de eso, el resto de esto es específico del problema, aunque con un intento de ser genérico.

Ahora DataGrouper.sum(data, ["Phase"])vuelve

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

Y DataGrouper.sum(data, ["Phase", "Step"])vuelve

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

Pero aquí sumsolo hay una función potencial. Puedes registrar a otros como quieras:

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

y ahora DataGrouper.max(data, ["Phase", "Step"])volveremos

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

o si registraste esto:

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

entonces llamar DataGrouper.tasks(data, ["Phase", "Step"])te llevará

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

DataGrouperEn sí es una función. Puede llamarlo con sus datos y una lista de las propiedades por las que desea agrupar. Devuelve una matriz cuyos elementos son objetos con dos propiedades: keyes la colección de propiedades agrupadas, valses una matriz de objetos que contiene las propiedades restantes que no están en la clave. Por ejemplo, DataGrouper(data, ["Phase", "Step"])rendirá:

[
    {
        "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.registeracepta una función y crea una nueva función que acepta los datos iniciales y las propiedades para agrupar. Esta nueva función luego toma el formato de salida como se indica arriba y ejecuta su función contra cada una de ellas, devolviendo una nueva matriz. La función que se genera se almacena como una propiedad de DataGrouperacuerdo con un nombre que proporcione y también se devuelve si solo desea una referencia local.

Bueno, eso es mucha explicación. ¡El código es razonablemente sencillo, espero!

Scott Sauyet
fuente
Hola ... Puedo verlo agrupar y sumar solo por un valor, pero en caso de que quiera sumar por valor1 y valor2 y valor3 ... ¿tiene una solución?
SAMUEL OSPINA
@SAMUELOSPINA ¿alguna vez encontraste una manera de hacer esto?
howMuchCheeseIsTooMuchCheese
50

Verificaría lodash group: parece que hace exactamente lo que estás buscando. También es bastante ligero y realmente simple.

Ejemplo de Fiddle: https://jsfiddle.net/r7szvt5k/

Siempre que su nombre de matriz sea arrel grupo, con lodash es solo:

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
fuente
1
¿No es problemática esta respuesta? Hay varias formas en que el resultado lodash _.groupBy no está en el formato del resultado que solicita el OP. (1) El resultado no es una matriz. (2) El "valor" se ha convertido en la "clave" en el resultado de los objetos lodash.
mg1075
44

Probablemente esto se haga más fácilmente linq.js, lo que pretende ser una verdadera implementación de LINQ en 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 }
]

O, más simplemente usando los selectores basados ​​en cadenas ( DEMO ):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();
mellamokb
fuente
¿podemos usar varias propiedades al agrupar aquí:GroupBy(function(x){ return x.Phase; })
Amit
38

Puedes construir un ES6 Mapdesde array.reduce().

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

Esto tiene algunas ventajas sobre las otras soluciones:

  • No requiere ninguna biblioteca (a diferencia de, por ejemplo, _.groupBy() )
  • Obtiene un JavaScript en Maplugar de un objeto (por ejemplo, como lo devuelve _.groupBy()). Esto tiene muchos beneficios , que incluyen:
    • recuerda el orden en que se agregaron los artículos por primera vez,
    • Las teclas pueden ser de cualquier tipo en lugar de solo cadenas.
  • A Mapes un resultado más útil que una matriz de matrices. Pero si desea una matriz de matrices, puede llamar Array.from(groupedMap.entries())(para una matriz de [key, group array]pares) o Array.from(groupedMap.values())(para una matriz simple de matrices).
  • Es bastante flexible; a menudo, lo que estaba planeando hacer a continuación con este mapa puede hacerse directamente como parte de la reducción.

Como ejemplo del último punto, imagine que tengo una matriz de objetos en los que quiero hacer una fusión (superficial) por id, como esta:

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 hacer esto, generalmente comenzaría agrupando por id y luego fusionando cada una de las matrices resultantes. En cambio, puede hacer la fusión directamente en reduce():

const mergedArray = Array.from(
    objsToMerge.reduce(
        (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}),
        new Map()
    ).values()
);
Arthur Tacca
fuente
1
No sé por qué esto no tiene más votos. Es conciso, legible (para mí) y se ve eficiente. No vuela en IE11 , pero la actualización no es demasiado difícil ( a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map())aproximadamente)
desactive el
18

Puedes hacerlo con la biblioteca JavaScript de 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]);

Pruebe este ejemplo en jsFiddle .

Por cierto: en matrices grandes (100000 registros y más) Alasql más rápido que Linq. Ver prueba en jsPref .

Comentarios:

  • Aquí pongo Value entre corchetes, porque VALUE es una palabra clave en SQL
  • Tengo que usar la función CAST () para convertir valores de cadena a tipo de número.
agershun
fuente
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
fuente
15

MDN tiene este ejemplo en su Array.reduce()documentación.

// 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
fuente
14

Aunque la pregunta tiene algunas respuestas y las respuestas parecen un poco complicadas, sugiero usar Javascript de vainilla para agrupar con un anidado (si es necesario) 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
fuente
9

Sin mutaciones:

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"]}
Bendecir
fuente
8

Esta solución toma cualquier función arbitraria (no una clave), por lo que es más flexible que las soluciones anteriores y permite funciones de flecha , que son similares a las expresiones lambda utilizadas en LINQ :

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

NOTA: si desea extender Arrayel prototipo depende de usted.

Ejemplo admitido en la mayoría de los navegadores:

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

Ejemplo usando funciones de flecha (ES6):

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

Ambos ejemplos anteriores devuelven:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}
Diego
fuente
Me gustó mucho la solución ES6. Solo un poco de amplificación sin extender el prototipo de matriz:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
caneta
8

Me gustaría sugerir mi enfoque. Primero, agrupar y agregar por separado. Vamos a declarar la función prototípica "agrupar por". Se necesita otra función para producir una cadena "hash" para cada elemento de matriz para agrupar.

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

cuando se realiza la agrupación, puede agregar datos como lo necesite, en su 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;
    });

en común funciona. He probado este código en la consola de Chrome. y siéntase libre de mejorar y encontrar errores;)

Anton
fuente
Gracias ! Me encanta el enfoque y se adapta perfectamente a mis necesidades (no necesito agregar).
aberaud
Creo que quieres cambiar tu línea en put (): map[_hash(key)].key = key;a map[_hash(key)].key = _hash(key);.
Scotty.NET
6

Imagina que tienes algo como esto:

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

Al hacer esto: const categories = [...new Set(cars.map((car) => car.cat))]

Obtendrás esto: ['sedan','sport']

Explicación: 1. Primero, estamos creando un nuevo conjunto pasando una matriz. Debido a que Set solo permite valores únicos, se eliminarán todos los duplicados.

  1. Ahora que los duplicados se han ido, vamos a convertirlo de nuevo a una matriz usando el operador de propagación ...

Establecer documento: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set Spread OperatorDoc: https://developer.mozilla.org/en-US/docs/Web/JavaScript / Referencia / Operadores / Spread_syntax

Yago Gehres
fuente
Me gusta mucho tu respuesta, es la más corta, pero aún no entiendo la lógica, especialmente, ¿quién agrupa aquí? ¿Es un operador extendido (...)? o el 'nuevo Set ()'? explícanoslo ... gracias
Ivan
1
1. Primero, estamos creando un nuevo conjunto pasando una matriz. Debido a que Set solo permite valores únicos, se eliminarán todos los duplicados. 2. Ahora que los duplicados se han ido, vamos a convertirlo de nuevo a una matriz utilizando el operador de propagación ... Establecer Doc: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Spread Operador: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Yago Gehres
ok, lo tengo ... gracias por la explicación señor :)
Ivan
¡De nada!
Yago Gehres
6

Respuesta marcada: solo agrupación superficial. Es bastante agradable entender la reducción. La pregunta también proporciona el problema de los cálculos agregados adicionales.

Aquí hay un GRUPO REAL POR para la matriz de objetos por algunos campos con 1) nombre de clave calculado y 2) solución completa para la agrupación en cascada al proporcionar la lista de las claves deseadas y convertir sus valores únicos a claves raíz como SQL GROUP BY lo hace.

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

Juegue con estKey: puede agrupar por más de un campo, agregar agregaciones adicionales, cálculos u otro procesamiento.

También puede agrupar datos de forma recursiva. Por ejemplo, inicialmente agrupar por Phase, luego por Stepcampo y así sucesivamente. Además, elimine los datos de reposo 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
fuente
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;
    }, []);
}

Esta salida de matriz.

tomitrescak
fuente
4

Basado en respuestas anteriores

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

y es un poco más agradable de ver con la sintaxis de propagación de objetos, si su entorno lo admite.

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

Aquí, nuestro reductor toma el valor de retorno parcialmente formado (comenzando con un objeto vacío) y devuelve un objeto compuesto por los miembros extendidos del valor de retorno anterior, junto con un nuevo miembro cuya clave se calcula a partir del valor actual del árbol en propy cuyo valor es una lista de todos los valores para ese accesorio junto con el valor actual.

Benny Powers
fuente
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
fuente
3

Aquí hay una solución desagradable y difícil de leer con ES6:

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

Para los que preguntan cómo hace esto , incluso el trabajo, aquí hay una explicación:

  • En ambos =>tienes un librereturn

  • La Array.prototype.reducefunción toma hasta 4 parámetros. Es por eso que se agrega un quinto parámetro para que podamos tener una declaración de variable barata para el grupo (k) en el nivel de declaración de parámetro mediante el uso de un valor predeterminado. (sí, esto es brujería)

  • Si nuestro grupo actual no existe en la iteración anterior, creamos una nueva matriz vacía ((r[k] || (r[k] = []))Esto evaluará a la expresión más a la izquierda, en otras palabras, una matriz existente o una matriz vacía , es por eso que hay una inmediata pushdespués de esa expresión, porque De cualquier manera obtendrá una matriz.

  • Cuando hay un return, el ,operador de coma descartará el valor más a la izquierda, devolviendo el grupo anterior ajustado para este escenario.

Una versión más fácil de entender que hace lo mismo es:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});
sueño oscuro
fuente
2
¿te importaría explicar esto un poco? Funciona perfectamente
Nuwan Dammika
@NuwanDammika - En ambos => tienes un "retorno" gratuito - La función de reducción toma hasta 4 parámetros. Es por eso que se agrega un quinto parámetro para que podamos tener una declaración de variable barata para el grupo (k). - Si el valor anterior no tiene nuestro grupo actual, creamos un nuevo grupo vacío ((r [k] || (r [k] = [])) Esto evaluará a la expresión más a la izquierda, de lo contrario, una matriz o un matriz vacía, es por eso que hay un impulso inmediato después de esa expresión. - Cuando hay un retorno, el operador de coma descartará el valor más a la izquierda, devolviendo el grupo anterior ajustado.
darkndream
2

Vamos a generar una Array.prototype.groupBy()herramienta genérica . Solo por variedad, usemos la fantasía de ES6, el operador de propagación para algunas coincidencias de patrones Haskellesque en un enfoque recursivo. También hagamos Array.prototype.groupBy()que aceptemos una devolución de llamada que toma el elemento ( e), el índice ( i) y la 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
fuente
2

La respuesta de Ceasar es buena, pero solo funciona para las propiedades internas de los elementos dentro de la matriz (longitud en el caso de una cadena).

esta implementación funciona más como: este enlace

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 esto ayude...

Roey
fuente
2

De @mortb, @jmarceli responde y de esta publicación ,

Aprovecho JSON.stringify()para ser la identidad del VALOR PRIMITIVO de varias columnas de grupo por.

Sin un tercero

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

Con un tercero de 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.
fuente
keyGetter regresa indefinido
Asbar Ali
@AsbarAli Probé mi fragmento con la consola de Chrome - Versión 66.0.3359.139 (Versión oficial) (64 bits). Y todo funciona bien. ¿Podría por favor poner el punto de interrupción de depuración y ver por qué keyGetter no está definido? Es, tal vez, debido a la versión del navegador.
Pranithan T.
2

reduceVersión basada en ES6 con soporte para función iteratee.

Funciona como se esperaba si iterateeno se proporciona la función:

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

En el contexto de la pregunta 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

Otra versión de ES6 que invierte la agrupación y usa el valuesas keysy el keysas el 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: las funciones se publican en su forma abreviada (una línea) por brevedad y para relacionar solo la idea. Puede expandirlos y agregar verificación de errores adicional, etc.

Akrion
fuente
2

Verificaría declarative-js groupBy , parece que hace exactamente lo que está buscando. Tambien es:

  • muy eficaz ( punto de referencia de rendimiento )
  • escrito en mecanografiado para que se incluyan todos los tipos.
  • No es obligatorio usar objetos de tipo matriz de terceros.
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
fuente
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
fuente
1

Aquí hay una versión ES6 que no se romperá en miembros nulos

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

Solo para agregar a la respuesta de Scott Sauyet , algunas personas preguntaban en los comentarios cómo usar su función para agrupar por valor1, valor2, etc., en lugar de agrupar solo un valor.

Todo lo que se necesita es editar su función de suma:

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

dejando el principal (DataGrouper) sin cambios:

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
fuente
1

Con función de clasificación

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;
      }, []);
    };

Muestra:

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
fuente