Die effizienteste Methode zum Gruppieren in einem Array von Objekten

507

Was ist der effizienteste Weg, um Objekte in einem Array zu gruppieren?

Beispiel: Bei diesem Array von Objekten:

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

Ich zeige diese Informationen in einer Tabelle an. Ich möchte nach verschiedenen Methoden gruppieren, aber ich möchte die Werte summieren.

Ich verwende Underscore.js für die groupby-Funktion, was hilfreich ist, aber nicht den ganzen Trick macht, weil ich nicht möchte, dass sie "aufgeteilt", sondern "zusammengeführt" werden, eher wie die SQL- group byMethode.

Was ich suche, kann bestimmte Werte summieren (falls gewünscht).

Wenn ich also Groupby machen würde Phase, würde ich Folgendes erhalten wollen:

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

Und wenn ich Groupy Phase/ machen würde Step, würde ich erhalten:

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

Gibt es dafür ein hilfreiches Skript, oder sollte ich mich an die Verwendung von Underscore.js halten und dann das resultierende Objekt durchlaufen, um die Summen selbst zu erstellen?

Rail24
quelle

Antworten:

755

Wenn Sie externe Bibliotheken vermeiden möchten, können Sie eine Vanille-Version groupBy()wie folgt präzise implementieren :

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
quelle
18
Ich würde dies folgendermaßen ändern: `` `return xs.reduce (Funktion (rv, x) {var v = Schlüsselinstanz der Funktion? Taste (x): x [Schlüssel]; (rv [v] = rv [v] || []). push (x); return rv;}, {}); `` `Rückruffunktionen erlauben, ein Sortierkriterium zurückzugeben
y_nk
110
Hier ist eine, die ein Array und kein Objekt ausgibt: groupByArray (xs, key) {return xs.reduce (Funktion (rv, x) {let v = Schlüsselinstanz der Funktion? 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;}, []); }
Tomitrescak
24
Großartig, genau das, was ich brauchte. Falls jemand anderes es braucht, hier ist die TypeScript-Signatur:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Michael Sandino
4
Für das, was es wert ist, ist die Lösung von Tomitrescak zwar praktisch, aber deutlich weniger effizient, da find () wahrscheinlich O (n) ist. Die Lösung in der Antwort ist O (n) aus der Reduzierung (Objektzuweisung ist O (1), wie Push), während der Kommentar O (n) * O (n) oder O (n ^ 2) oder at ist mindestens O (nlgn)
narthur157
21
Wenn jemand interessiert ist, habe ich eine lesbarere und kommentiertere Version dieser Funktion erstellt und sie in eine Zusammenfassung eingefügt: gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d Ich fand sie hilfreich, um zu verstehen, was hier tatsächlich passiert.
Robmathers
228

Verwenden des ES6-Kartenobjekts:

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

Informationen zur Karte: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

mortb
quelle
@mortb, wie bekomme ich es, ohne die get()Methode aufzurufen ? Ich möchte, dass die Ausgabe angezeigt wird, ohne den Schlüssel zu übergeben
Fai Zal Dong
@FaiZalDong: Ich bin nicht sicher, was für Ihren Fall am besten wäre? Wenn ich console.log(grouped.entries());in das jsfiddle-Beispiel schreibe , wird eine Iterable zurückgegeben, die sich wie ein Array von Schlüsseln + Werten verhält. Können Sie das versuchen und sehen, ob es hilft?
Mortb
7
Sie könnten auch versuchenconsole.log(Array.from(grouped));
Mortb
Ich liebe diese Antwort, sehr flexibel
Benshabatnoam
um die Anzahl der Elemente in Gruppen zu sehen:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
Ahmet Şimşek
105

mit ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);
Joseph Nields
quelle
3
Es ist etwas gewöhnungsbedürftig, aber auch die meisten C ++ - Vorlagen
Levi Haskell,
2
Ich habe mir den Kopf zerbrochen und immer noch nicht verstanden, wie es in aller Welt funktioniert ...result. Jetzt kann ich deswegen nicht schlafen.
8
Elegant, aber auf größeren Arrays schmerzhaft langsam!
Infinity1975
4
lol was ist das
Nino Škopac
1
einfache Lösung. Danke
Ezequiel Tavares
59

Obwohl die Antwort von linq interessant ist, ist sie auch ziemlich schwer. Mein Ansatz ist etwas anders:

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

Sie können es in Aktion auf JSBin sehen .

Ich habe in Underscore nichts gesehen, was das hastut, obwohl ich es möglicherweise vermisse. Es ist ähnlich wie _.contains, wird aber _.isEqualeher als ===für Vergleiche verwendet. Davon abgesehen ist der Rest problemspezifisch, allerdings mit dem Versuch, generisch zu sein.

Kehrt jetzt DataGrouper.sum(data, ["Phase"])zurück

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

Und DataGrouper.sum(data, ["Phase", "Step"])kehrt zurück

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

Ist sumaber hier nur eine mögliche Funktion. Sie können andere nach Belieben registrieren:

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

und jetzt DataGrouper.max(data, ["Phase", "Step"])wird zurückkehren

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

oder wenn Sie dies registriert haben:

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

dann DataGrouper.tasks(data, ["Phase", "Step"])bringt dich ein Anruf

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

DataGrouperselbst ist eine Funktion. Sie können es mit Ihren Daten und einer Liste der Eigenschaften aufrufen, nach denen Sie gruppieren möchten. Es gibt ein Array zurück, dessen Elemente Objekte mit zwei Eigenschaften sind: keyIst die Sammlung gruppierter Eigenschaften, valsist ein Array von Objekten, die die verbleibenden Eigenschaften enthalten, die nicht im Schlüssel enthalten sind. Zum Beispiel DataGrouper(data, ["Phase", "Step"])ergibt sich:

[
    {
        "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.registerakzeptiert eine Funktion und erstellt eine neue Funktion, die die Anfangsdaten und die Eigenschaften akzeptiert, nach denen gruppiert werden soll. Diese neue Funktion nimmt dann das Ausgabeformat wie oben an und führt Ihre Funktion nacheinander für jedes von ihnen aus, wobei ein neues Array zurückgegeben wird. Die generierte Funktion wird als Eigenschaft von DataGroupergemäß einem von Ihnen angegebenen Namen gespeichert und auch zurückgegeben, wenn Sie nur eine lokale Referenz wünschen.

Das ist eine Menge Erklärung. Der Code ist ziemlich einfach, hoffe ich!

Scott Sauyet
quelle
Hallo .. Kannst du sehen, wie du nach einem Wert gruppierst und summierst, aber falls ich eine Summe nach Wert1 und Wert2 und Wert3 will ... hast du eine Lösung?
SAMUEL OSPINA
@SAMUELOSPINA Hast du jemals einen Weg gefunden, dies zu tun?
howMuchCheeseIsTooMuchCheese
50

Ich würde lodash groupBy überprüfen, indem es genau das zu tun scheint, wonach Sie suchen. Es ist auch ziemlich leicht und sehr einfach.

Geigenbeispiel: https://jsfiddle.net/r7szvt5k/

Vorausgesetzt, Ihr Array-Name ist arrgroupBy mit lodash ist nur:

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
quelle
1
Ist diese Antwort nicht problematisch? Es gibt mehrere Möglichkeiten, wie das Ergebnis von lodash _.groupBy nicht das Format des vom OP angeforderten Ergebnisses hat. (1) Das Ergebnis ist kein Array. (2) Der "Wert" ist zum "Schlüssel" im Ergebnis der lodash-Objekte geworden.
mg1075
44

Dies ist wahrscheinlich einfacher zu bewerkstelligen linq.js, was eine echte Implementierung von LINQ in JavaScript ( DEMO ) sein soll:

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

Ergebnis:

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

Oder einfacher mit den stringbasierten Selektoren ( DEMO ):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();
mellamokb
quelle
Können wir beim Gruppieren hier mehrere Eigenschaften verwenden:GroupBy(function(x){ return x.Phase; })
Amit
38

Sie können einen ES6 Mapaus erstellen array.reduce().

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

Dies hat einige Vorteile gegenüber den anderen Lösungen:

  • Es erfordert keine Bibliotheken ( im Gegensatz zu zB _.groupBy())
  • Sie erhalten Mapeher ein JavaScript als ein Objekt (z. B. wie von zurückgegeben _.groupBy()). Dies hat viele Vorteile , einschließlich:
    • Es merkt sich die Reihenfolge, in der die Artikel zuerst hinzugefügt wurden.
    • Schlüssel können ein beliebiger Typ sein und nicht nur Zeichenfolgen.
  • A Mapist ein nützlicheres Ergebnis als ein Array von Arrays. Wenn Sie jedoch ein Array von Arrays möchten, können Sie Array.from(groupedMap.entries())(für ein Array von [key, group array]Paaren) oder Array.from(groupedMap.values())(für ein einfaches Array von Arrays) aufrufen .
  • Es ist ziemlich flexibel; Was auch immer Sie als Nächstes mit dieser Karte vorhatten, kann häufig direkt im Rahmen der Reduzierung durchgeführt werden.

Stellen Sie sich als Beispiel für den letzten Punkt vor, ich habe eine Reihe von Objekten, für die ich eine (flache) Zusammenführung nach ID durchführen möchte, wie folgt:

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

Zu diesem Zweck beginne ich normalerweise damit, nach ID zu gruppieren und dann jedes der resultierenden Arrays zusammenzuführen. Stattdessen können Sie die Zusammenführung direkt in der reduce()folgenden Datei durchführen :

const mergedArray = Array.from(
    objsToMerge.reduce(
        (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}),
        new Map()
    ).values()
);
Arthur Tacca
quelle
1
Ich weiß nicht, warum dies nicht mehr Stimmen hat. Es ist prägnant, lesbar (für mich) und sieht effizient aus. Es fliegt nicht auf IE11 , aber die Nachrüstung ist nicht zu schwer ( a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map())ungefähr)
Unbob
24
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

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

Julio Marins
quelle
18

Sie können dies mit der Alasql JavaScript-Bibliothek tun :

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

Versuchen Sie dieses Beispiel bei jsFiddle .

Übrigens: Auf großen Arrays (100000 Datensätze und mehr) Alasql schneller als Linq. Siehe Test bei jsPref .

Bemerkungen:

  • Hier setze ich Value in eckige Klammern, da VALUE ein Schlüsselwort in SQL ist
  • Ich muss die CAST () - Funktion verwenden, um Zeichenfolgenwerte in den Zahlentyp zu konvertieren.
Agershun
quelle
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
quelle
15

MDN hat dieses Beispiel in seiner Array.reduce()Dokumentation.

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

Obwohl die Frage einige Antworten hat und die Antworten etwas kompliziert aussehen, empfehle ich, Vanille-Javascript für das Gruppieren mit einem verschachtelten (falls erforderlich) zu verwenden 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
quelle
9

Ohne Mutationen:

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"]}
Segnen
quelle
8

Diese Lösung verwendet eine beliebige Funktion (keine Taste), ist also flexibler als die oben genannten Lösungen und ermöglicht Pfeilfunktionen , die den in LINQ verwendeten Lambda-Ausdrücken ähneln :

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

HINWEIS: Ob Sie den ArrayPrototyp erweitern möchten, liegt bei Ihnen.

Beispiel, das in den meisten Browsern unterstützt wird:

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

Beispiel mit Pfeilfunktionen (ES6):

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

Beide obigen Beispiele geben Folgendes zurück:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}
Diego
quelle
Die ES6-Lösung hat mir sehr gut gefallen. Nur eine kleine Vereinfachung ohne Erweiterung des Array-Prototyps:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
Caneta
8

Ich möchte meinen Ansatz vorschlagen. Erstens, separate Gruppierung und Aggregation. Deklarieren wir die prototypische Funktion "Gruppieren nach". Es ist eine weitere Funktion erforderlich, um für jedes zu gruppierende Array-Element eine "Hash" -String zu erstellen.

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

Wenn die Gruppierung abgeschlossen ist, können Sie in Ihrem Fall Daten nach Bedarf aggregieren

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

gemeinsam funktioniert es. Ich habe diesen Code in Chrome Console getestet. und zögern Sie nicht, sich zu verbessern und Fehler zu finden;)

Anton
quelle
Vielen Dank ! Ich liebe den Ansatz und passe perfekt zu meinen Bedürfnissen (ich muss nicht aggregieren).
Aberaud
Ich denke, Sie möchten Ihre Zeile in put () ändern: map[_hash(key)].key = key;to map[_hash(key)].key = _hash(key);.
Scotty.NET
6

Stellen Sie sich vor, Sie haben so etwas:

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

Auf diese Weise: const categories = [...new Set(cars.map((car) => car.cat))]

Sie erhalten Folgendes: ['sedan','sport']

Erläuterung: 1. Zuerst erstellen wir einen neuen Satz, indem wir ein Array übergeben. Da Set nur eindeutige Werte zulässt, werden alle Duplikate entfernt.

  1. Jetzt sind die Duplikate verschwunden. Wir werden sie mithilfe des Spread-Operators wieder in ein Array konvertieren ...

Legen Sie Doc fest: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set Spread OperatorDoc: https://developer.mozilla.org/en-US/docs/Web/JavaScript / Reference / Operators / Spread_syntax

Yago Gehres
quelle
Ich mag deine Antwort sehr, es ist die kürzeste, aber ich verstehe die Logik immer noch nicht, besonders, wer macht die Gruppierung hier? ist es Spread Operator (...)? oder das 'neue Set ()'? Bitte erklären Sie es uns ... danke
Ivan
1
1. Zuerst erstellen wir ein neues Set, indem wir ein Array übergeben. Da Set nur eindeutige Werte zulässt, werden alle Duplikate entfernt. 2. Nachdem die Duplikate verschwunden sind, konvertieren wir sie mithilfe des Spread-Operators zurück in ein Array ... Set Doc: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Spread Betreiber: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Yago Gehres
ok, verstanden ... danke für die Erklärung, Sir :)
Ivan
Bitte!
Yago Gehres
6

Überprüfte Antwort - nur flache Gruppierung. Es ist ziemlich schön zu verstehen, wie man reduziert. Die Frage liefert auch das Problem zusätzlicher aggregierter Berechnungen.

Hier ist eine REAL GROUP BY für das Array von Objekten nach Feldern mit 1) berechnetem Schlüsselnamen und 2) vollständiger Lösung für die Kaskadierung von Gruppierungen, indem die Liste der gewünschten Schlüssel bereitgestellt und ihre eindeutigen Werte in Stammschlüssel wie SQL GROUP konvertiert werden BY tut.

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

Spielen Sie mit estKey- Sie können nach mehr als einem Feld gruppieren, zusätzliche Aggregationen, Berechnungen oder andere Verarbeitungen hinzufügen.

Sie können Daten auch rekursiv gruppieren. Zum Beispiel zunächst gruppieren nach Phase, dann nach StepFeld und so weiter. Zusätzlich die Daten zur Fettruhe abblasen.

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

Dieser gibt ein Array aus.

Tomitrescak
quelle
4

Basierend auf früheren Antworten

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

und es ist ein wenig schöner, mit der Objektverbreitungssyntax zu betrachten, wenn Ihre Umgebung dies unterstützt.

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

Hier nimmt unser Reduzierer den teilweise gebildeten Rückgabewert (beginnend mit einem leeren Objekt) und gibt ein Objekt zurück, das sich aus den ausgebreiteten Elementen des vorherigen Rückgabewerts zusammen mit einem neuen Element zusammensetzt, dessen Schlüssel aus dem Wert des aktuellen iteree bei berechnet wird propund dessen Wert eine Liste aller Werte für diese Requisite zusammen mit dem aktuellen Wert ist.

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

Hier ist eine böse, schwer lesbare Lösung mit ES6:

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

Für diejenigen, die fragen, wie das überhaupt funktioniert, hier eine Erklärung:

  • In beiden =>hast du eine freiereturn

  • Die Array.prototype.reduceFunktion benötigt bis zu 4 Parameter. Aus diesem Grund wird ein fünfter Parameter hinzugefügt, damit wir eine billige Variablendeklaration für die Gruppe (k) auf der Ebene der Parameterdeklaration unter Verwendung eines Standardwerts haben können. (Ja, das ist Zauberei)

  • Wenn unsere aktuelle Gruppe in der vorherigen Iteration nicht vorhanden ist, erstellen wir ein neues leeres Array. ((r[k] || (r[k] = []))Dadurch wird der Ausdruck ganz links ausgewertet, dh ein vorhandenes Array oder ein leeres Array . Aus diesem Grund wird unmittelbar pushnach diesem Ausdruck ein Array angezeigt In beiden Fällen erhalten Sie ein Array.

  • Wenn es ein gibt return, ,verwirft der Komma- Operator den Wert ganz links und gibt die optimierte vorherige Gruppe für dieses Szenario zurück.

Eine leichter verständliche Version, die dasselbe tut, ist:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});
Darkndream
quelle
2
Möchten Sie
@NuwanDammika - In beiden => haben Sie eine freie "Rückgabe" - Die Reduktionsfunktion akzeptiert bis zu 4 Parameter. Aus diesem Grund wird ein fünfter Parameter hinzugefügt, damit wir eine billige Variablendeklaration für die Gruppe (k) haben können. - Wenn der vorherige Wert nicht unsere aktuelle Gruppe enthält, erstellen wir eine neue leere Gruppe ((r [k] || (r [k] = [])). Dies wird bis zum Ausdruck ganz links ausgewertet, andernfalls ein Array oder ein leeres Array, aus diesem Grund wird nach diesem Ausdruck sofort ein Push ausgeführt. - Bei einer Rückgabe verwirft der Komma-Operator den Wert ganz links und gibt die optimierte vorherige Gruppe zurück.
darkndream
2

Generieren wir ein generisches Array.prototype.groupBy()Tool. Verwenden Sie nur zur Abwechslung die ES6-Fantasie, den Spread-Operator, für einige Haskellesque-Muster, die auf einem rekursiven Ansatz übereinstimmen. Lassen Sie uns Array.prototype.groupBy()auch einen Rückruf akzeptieren, der das Element ( e), den Index ( i) und das angewendete Array ( a) als Argumente verwendet.

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

Reduzieren
quelle
2

Ceasars Antwort ist gut, funktioniert aber nur für die inneren Eigenschaften der Elemente innerhalb des Arrays (Länge bei Zeichenfolge).

Diese Implementierung funktioniert eher wie folgt: dieser 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;
    }, {});
};

hoffe das hilft...

Roey
quelle
2

Von @mortb, @jmarceli antworten und von diesem Beitrag ,

Ich nutze den Vorteil JSON.stringify(), die Identität für die PRIMITIVE VALUE mehrere Spalten der Gruppe von zu sein.

Ohne Dritte

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

Mit Lodash -Drittanbietern

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.
quelle
keyGetter kehrt undefiniert zurück
Asbar Ali
@AsbarAli Ich habe mein Snippet mit der Chrome-Konsole getestet - Version 66.0.3359.139 (Official Build) (64-Bit). Und alles läuft gut. Könnten Sie bitte den Debugging-Haltepunkt setzen und sehen, warum keyGetter undefiniert ist. Möglicherweise liegt es an der Browserversion.
Pranithan T.
2

ES6- reduce basierte Versionsversion mit Funktionsunterstützung iteratee.

Funktioniert wie erwartet, wenn die iterateeFunktion nicht bereitgestellt wird:

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

Im Kontext der OP-Frage:

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

Eine weitere ES6- Version, die die Gruppierung umkehrt und das valuesas keysund das keysas verwendet 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))

Hinweis: Funktionen werden der Kürze halber und nur in Bezug auf die Idee in ihrer Kurzform (eine Zeile) angegeben. Sie können sie erweitern und zusätzliche Fehlerprüfungen usw. hinzufügen.

Akrion
quelle
2

Ich würde deklarative-js überprüfen, groupByes scheint genau das zu tun, wonach Sie suchen. Es ist auch:

  • sehr performant (Performance Benchmark )
  • geschrieben in Typoskript, so dass alle Typpings enthalten sind.
  • Es wird nicht erzwungen, Array-ähnliche Objekte von Drittanbietern zu verwenden.
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
quelle
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
quelle
1

Hier ist eine ES6-Version, die bei Null-Mitgliedern nicht kaputt geht

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

Um die Antwort von Scott Sauyet zu ergänzen, fragten einige Leute in den Kommentaren, wie man seine Funktion verwendet, um nach Wert1, Wert2 usw. zu gruppieren, anstatt nur einen Wert zu gruppieren.

Alles was es braucht ist seine Bearbeitungsfunktion zu bearbeiten:

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

Lassen Sie die Hauptversion (DataGrouper) unverändert:

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

Mit Sortierfunktion

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

Stichprobe:

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
quelle