Metodo più efficiente per raggruppare su una matrice di oggetti


507

Qual è il modo più efficiente per raggruppare gli oggetti in un array?

Ad esempio, dato questo array di oggetti:

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

Sto visualizzando queste informazioni in una tabella. Vorrei raggruppare metodi diversi, ma voglio sommare i valori.

Sto usando Underscore.js per la sua funzione groupby, che è utile, ma non fa tutto il trucco, perché non li voglio "divisi" ma "uniti", più come il group bymetodo SQL .

Quello che sto cercando sarebbe in grado di totalizzare valori specifici (se richiesto).

Quindi, se facessi il groupby Phase, vorrei ricevere:

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

E se facessi groupy Phase/ Step, riceverei:

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

Esiste uno script utile per questo, o dovrei attenermi all'uso di Underscore.js e quindi scorrere in sequenza l'oggetto risultante per fare il totale da solo?

Risposte:


755

Se si desidera evitare librerie esterne, è possibile implementare in modo conciso una versione vanilla in questo 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"]}


18
Vorrei modificare in questo modo: `` return xs.reduce (function (rv, x) {var v = key instanceof Function? key (x): x [key]; (rv [v] = rv [v] || []). push (x); return rv;}, {}); `` consentire alle funzioni di callback di restituire un criterio di ordinamento
y_nk

110
Eccone uno che genera array e non oggetto: 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 ({chiave: v, valori: [x] });} return rv;}, []); }
tomitrescak,

24
Fantastico, proprio quello di cui avevo bisogno. Nel caso in cui qualcun altro ne abbia bisogno, ecco la firma TypeScript:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Michael Sandino,

4
Per quello che vale, la soluzione di tomitrescak, sebbene conveniente, è significativamente meno efficiente, poiché find () è probabilmente O (n). La soluzione nella risposta è O (n), dalla riduzione (l'assegnazione dell'oggetto è O (1), come è push), mentre il commento è O (n) * O (n) o O (n ^ 2) o at almeno O (nlgn)
narthur157,

21
Se qualcuno è interessato, ho creato una versione più leggibile e annotata di questa funzione e l'ho messa in una sintesi : gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d L' ho trovata utile per capire cosa sta realmente accadendo qui.
robmathers,

228

Utilizzando l'oggetto 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"}]
    
    

Informazioni sulla mappa: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map


@mortb, come ottenerlo senza chiamare il get()metodo? che voglio che l'output sia visualizzato senza passare la chiave
Fai Zal Dong il

@FaiZalDong: Non sono sicuro di cosa sarebbe meglio per il tuo caso? Se scrivo console.log(grouped.entries());nell'esempio jsfiddle, viene restituito un iterabile che si comporta come una matrice di chiavi + valori. Puoi provarlo e vedere se aiuta?
Mortb,

7
Potresti anche provareconsole.log(Array.from(grouped));
Mortb,

Adoro questa risposta, molto flessibile
benshabatnoam,

per vedere il numero di elementi nei gruppi: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,
    ],
  }), 
  {},
);

3
Ci vuole un po 'per abituarsi, ma anche la maggior parte dei modelli C ++
Levi Haskell,

2
Mi sono distrutto il cervello e non riuscivo ancora a capire come funziona il mondo a partire da ...result. Ora non riesco a dormire per questo.

8
Elegante, ma dolorosamente lento su array più grandi!
infinity1975,

4
lol cos'è questo
Nino Škopac,

1
soluzione facile. Grazie
Ezequiel Tavares il

59

Sebbene la risposta al linq sia interessante, è anche piuttosto pesante. Il mio approccio è leggermente diverso:

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

Puoi vederlo in azione su JSBin .

In Underscore non ho visto nulla che fa ciò che hasfa, anche se potrei mancarlo. È più o meno lo stesso di _.contains, ma utilizza _.isEqualpiuttosto che ===per i confronti. A parte questo, il resto è specifico del problema, anche se con un tentativo di essere generico.

Ora DataGrouper.sum(data, ["Phase"])ritorna

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

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

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

Ma sumè solo una potenziale funzione qui. Puoi registrarne altri come preferisci:

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 ora DataGrouper.max(data, ["Phase", "Step"])tornerà

[
    {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 se lo hai registrato:

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

quindi chiamare DataGrouper.tasks(data, ["Phase", "Step"])ti prenderà

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

DataGrouperstesso è una funzione. Puoi chiamarlo con i tuoi dati e un elenco delle proprietà che vuoi raggruppare. Restituisce una matrice i cui elementi sono oggetti con due proprietà: keyè la raccolta di proprietà raggruppate, valsè una matrice di oggetti contenente le restanti proprietà non presenti nella chiave. Ad esempio, DataGrouper(data, ["Phase", "Step"])produrrà:

[
    {
        "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.registeraccetta una funzione e crea una nuova funzione che accetta i dati iniziali e le proprietà da raggruppare. Questa nuova funzione prende quindi il formato di output come sopra ed esegue la funzione su ciascuno di essi a turno, restituendo un nuovo array. La funzione che viene generata viene memorizzata come proprietà di DataGroupersecondo un nome fornito e restituita anche se si desidera solo un riferimento locale.

Bene, questo è un sacco di spiegazioni. Il codice è ragionevolmente semplice, spero!


Ciao .. Posso vederti raggruppare e sommare solo per un valore, ma nel caso in cui voglio somma per valore1 e valore2 e valore3 ... hai una soluzione?
SAMUEL OSPINA,

@SAMUELOSPINA hai mai trovato un modo per farlo?
howMuchCheeseIsTooMuchCheese

50

Vorrei controllare il gruppo lodashPerché sembra fare esattamente quello che stai cercando. È anche abbastanza leggero e molto semplice.

Esempio di violino: https://jsfiddle.net/r7szvt5k/

A condizione che il nome dell'array sia arril groupBy con lodash è 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

1
Questa risposta non è problematica? Esistono diversi modi in cui il risultato lodash _.groupBy non è nel formato del risultato richiesto dall'OP. (1) Il risultato non è un array. (2) Il "valore" è diventato la "chiave" nel risultato degli oggetti lodash.
mg1075,

44

Questo è probabilmente più facile da fare linq.js, che vuole essere una vera implementazione di LINQ in 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();

risultato:

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

Oppure, più semplicemente usando i selettori basati su stringhe ( DEMO ):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();

possiamo usare più proprietà mentre raggruppiamo qui:GroupBy(function(x){ return x.Phase; })
Tra il

38

È possibile creare un ES6 Mapda array.reduce().

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

Ciò presenta alcuni vantaggi rispetto alle altre soluzioni:

  • Non richiede alcuna libreria (diversamente da es _.groupBy() )
  • Ottieni un JavaScript Mapanziché un oggetto (ad es. Come restituito da _.groupBy()). Questo ha molti vantaggi , tra cui:
    • ricorda l'ordine in cui gli articoli sono stati aggiunti per la prima volta,
    • le chiavi possono essere di qualsiasi tipo anziché solo stringhe.
  • A Mapè un risultato più utile di una matrice di array. Ma se si desidera una matrice di matrici, è quindi possibile chiamare Array.from(groupedMap.entries())(per una matrice di [key, group array]coppie) o Array.from(groupedMap.values())(per una semplice matrice di matrici).
  • È abbastanza flessibile; spesso, qualunque cosa tu stia pianificando di fare successivamente con questa mappa può essere fatta direttamente come parte della riduzione.

Come esempio dell'ultimo punto, immagina di avere una matrice di oggetti su cui voglio fondere (superficialmente) con id, in questo modo:

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

Per fare ciò, di solito vorrei iniziare raggruppando per id e quindi unendo ciascuno degli array risultanti. Invece, puoi eseguire l'unione direttamente in reduce():

const mergedArray = Array.from(
    objsToMerge.reduce(
        (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}),
        new Map()
    ).values()
);

1
Non so perché questo non abbia più voti. È conciso, leggibile (per me) e sembra efficiente. Non vola su IE11 , ma il retrofit non è troppo difficile ( a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map()), circa)
sblocca il


18

Puoi farlo con la libreria JavaScript 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]);

Prova questo esempio su jsFiddle .

A proposito: su array di grandi dimensioni (100000 record e altro) Alasql più veloce su Linq. Vedi test su jsPref .

Commenti:

  • Qui metto Value tra parentesi quadre, perché VALUE è una parola chiave in SQL
  • Devo usare la funzione CAST () per convertire i valori di stringa in tipo numerico.

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

15

MDN ha questo esempio nella sua Array.reduce()documentazione.

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

14

Anche se la domanda ha alcune risposte e le risposte sembrano un po 'complicate, suggerisco di usare Javascript vanilla per il raggruppamento con un nidificato (se necessario) 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; }


9

Senza mutazioni:

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

8

Questa soluzione accetta qualsiasi funzione arbitraria (non una chiave), quindi è più flessibile delle soluzioni precedenti e consente funzioni freccia simili alle espressioni lambda utilizzate in LINQ :

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

NOTA: decidere se estendere Arrayil prototipo dipende da te.

Esempio supportato nella maggior parte dei browser:

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

Esempio di utilizzo delle funzioni freccia (ES6):

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

Entrambi gli esempi sopra restituiscono:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}

Mi è piaciuta molto la soluzione ES6. Solo una piccola semplificazione senza estendere il prototipo di array:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
caneta,

8

vorrei suggerire il mio approccio. Innanzitutto, raggruppamento e aggregazione separati. Consente di dichiarare la funzione "raggruppa per" prototipica. Ci vuole un'altra funzione per produrre una stringa "hash" per ogni elemento dell'array da raggruppare.

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

al termine del raggruppamento è possibile aggregare i dati in base alle proprie necessità, nel proprio 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;
    });

in comune funziona. ho testato questo codice nella console di Chrome. e sentiti libero di migliorare e trovare errori;)


Grazie ! Adoro l'approccio e si adatta perfettamente alle mie esigenze (non ho bisogno di aggregazione).
Aberaud,

Penso che tu voglia cambiare linea in put (): map[_hash(key)].key = key;to map[_hash(key)].key = _hash(key);.
Scotty.NET,

6

Immagina di avere qualcosa del genere:

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

Facendo questo: const categories = [...new Set(cars.map((car) => car.cat))]

Otterrai questo: ['sedan','sport']

Spiegazione: 1. Innanzitutto, stiamo creando un nuovo set passando un array. Poiché Set consente solo valori univoci, tutti i duplicati verranno rimossi.

  1. Ora che i duplicati sono spariti, lo riconvertiremo in un array usando l'operatore spread ...

Set 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 / Riferimento / Operatori / Spread_syntax


mi piace molto la tua risposta, è la più breve, ma ancora non capisco la logica, in particolare, chi fa il raggruppamento qui? è l'operatore di diffusione (...)? o il 'nuovo set ()'? per favore spiegamelo ... grazie
Ivan

1
1. Innanzitutto, stiamo creando un nuovo set passando un array. Poiché Set consente solo valori univoci, tutti i duplicati verranno rimossi. 2. Ora i duplicati sono spariti, lo riconvertiremo in un array usando l'operatore spread ... Set Doc: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Spread Operatore: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Yago Gehres

ok, capito ... grazie per la spiegazione signore :)
Ivan il

prego!
Yago Gehres,

6

Risposta controllata - solo raggruppamento superficiale. È abbastanza bello capire la riduzione. La domanda fornisce anche il problema di calcoli aggregati aggiuntivi.

Ecco un REAL GROUP BY per array di oggetti per alcuni campi con 1) nome chiave calcolato e 2) soluzione completa per il collegamento in cascata del raggruppamento fornendo l'elenco delle chiavi desiderate e convertendo i suoi valori univoci in chiavi root come SQL GROUP BY lo fa.

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

Gioca con estKey: puoi raggruppare per più di un campo, aggiungere aggregazioni, calcoli o altre elaborazioni aggiuntive.

Inoltre puoi raggruppare i dati in modo ricorsivo. Ad esempio, inizialmente raggruppare per Phase, quindi per Stepcampo e così via. Inoltre, scarica i dati sul grasso corporeo.

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


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

Questo emette array.


4

Basato su risposte precedenti

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

ed è un po 'più bello guardare con la sintassi di diffusione dell'oggetto, se il tuo ambiente lo supporta.

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

Qui, il nostro riduttore prende il valore di ritorno parzialmente formato (a partire da un oggetto vuoto), e restituisce un oggetto composto dai membri sparsi del valore di ritorno precedente, insieme a un nuovo membro la cui chiave viene calcolata dal valore corrente dell'arbitro in prope il cui valore è un elenco di tutti i valori per quel prop insieme al valore corrente.


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>


3

Ecco una brutta soluzione difficile da leggere che utilizza ES6:

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

Per coloro che chiedono come fa questo anche il lavoro, ecco la spiegazione:

  • In entrambi =>hai un omaggioreturn

  • La Array.prototype.reducefunzione accetta fino a 4 parametri. Ecco perché viene aggiunto un quinto parametro in modo da poter avere una dichiarazione di variabile economica per il gruppo (k) a livello di dichiarazione di parametro utilizzando un valore predefinito. (sì, questa è stregoneria)

  • Se il nostro gruppo corrente non esiste sulla precedente iterazione, creiamo un nuovo array vuoto. ((r[k] || (r[k] = []))Questo valuterà l'espressione più a sinistra, in altre parole, un array esistente o un array vuoto , ecco perché c'è immediatamente pushdopo quell'espressione, perché in entrambi i casi otterrai un array.

  • Quando c'è un return, l' ,operatore virgola scarterà il valore più a sinistra, restituendo il gruppo precedente ottimizzato per questo scenario.

Una versione più semplice da comprendere che fa lo stesso è:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});

2
ti dispiacerebbe spiegarlo un po ', funziona perfettamente
Nuwan Dammika,

@NuwanDammika - In entrambi => hai un "ritorno" libero - La funzione di riduzione accetta fino a 4 parametri. Ecco perché viene aggiunto un quinto parametro in modo da poter avere una dichiarazione di variabile economica per il gruppo (k). - Se il valore precedente non ha il nostro gruppo corrente, creiamo un nuovo gruppo vuoto ((r [k] || (r [k] = [])) Questo valuterà l'espressione più a sinistra, altrimenti un array o un array vuoto, ecco perché c'è una spinta immediata dopo quell'espressione - Quando c'è un ritorno, l'operatore virgola
scarterà

2

Consente di generare un generico Array.prototype.groupBy() strumento . Solo per varietà, usiamo la fantasia ES6, l'operatore di diffusione per alcuni adattamenti del modello Haskellesque su un approccio ricorsivo. Facciamo anche in modo Array.prototype.groupBy()che accetti un callback che prenda l'elemento ( e) l'indice ( i) e l'array applicato ( a) come argomenti.

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


2

La risposta di Caesar è buona, ma funziona solo per le proprietà interne degli elementi all'interno dell'array (lunghezza in caso di stringa).

questa implementazione funziona più come: questo 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;
    }, {});
};

spero che sia di aiuto...


2

Da @mortb, @jmarceli answer e da questo post ,

Approfitto di JSON.stringify()essere l'identità per il PRIMITIVE VALUE più colonne del gruppo di.

Senza terze parti

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 Lodash di terze parti

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

keyGetter restituisce undefined
Asbar Ali

@AsbarAli Ho testato il mio frammento con la console di Chrome - Versione 66.0.3359.139 (build ufficiale) (64 bit). E tutto funziona bene. Potresti inserire il punto di interruzione del debug e capire perché keyGetter non è definito. Forse è a causa della versione del browser.
Pranithan T.

2

reduceVersione versione basata su ES6 con supporto per la funzione iteratee.

Funziona esattamente come previsto se iterateenon viene fornita la funzione:

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

Nel contesto della domanda del PO:

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

Un'altra versione ES6 che inverte il raggruppamento e utilizza valuesas keyse as keyscome 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: le funzioni sono pubblicate nella loro forma breve (una riga) per brevità e per mettere in relazione solo l'idea. Puoi espanderli e aggiungere ulteriori controlli degli errori, ecc.


2

Controllerei dichiarative-js groupBy sembra fare esattamente quello che stai cercando. È anche:

  • molto performante ( benchmark delle prestazioni )
  • scritto in dattiloscritto in modo da includere tutti gli errori di battitura.
  • Non è obbligatorio utilizzare oggetti simili a array di terze parti.
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());

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

1

Ecco una versione ES6 che non si romperà sui membri null

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

1

Solo per aggiungere alla risposta di Scott Sauyet , alcune persone chiedevano nei commenti come usare la sua funzione per raggruppare valore1, valore2, ecc., Invece di raggruppare un solo valore.

Basta modificare la sua funzione di somma:

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

lasciando invariato quello principale (DataGrouper):

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

1

Con funzionalità di ordinamento

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

Campione:

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')));
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.