925 Stimmen

Effizienteste Methode zur Gruppierung von Objekten in einem Array

Wie kann man Objekte in einem Array am effizientesten gruppieren?

Nehmen wir zum Beispiel dieses 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 würde gerne nach verschiedenen Methoden gruppieren, aber ich möchte die Werte summieren.

Ich verwende Underscore.js für seine groupby-Funktion, die hilfreich ist, aber nicht den ganzen Trick, weil ich nicht will, dass sie "aufgeteilt", sondern "zusammengeführt", mehr wie die SQL group by método.

Was ich suche, ist die Möglichkeit, bestimmte Werte zu summieren (falls gewünscht).

Wenn ich also groupby Phase würde ich gerne erhalten:

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

Und wenn ich Groupy wäre Phase / 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 ein hilfreiches Skript für diese, oder sollte ich bleiben mit Underscore.js, und dann Schleife durch das resultierende Objekt zu tun, die Summen selbst?

6voto

Yago Gehres Punkte 69

Stellen Sie sich vor, Sie haben etwas wie dieses:

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

Das werden Sie bekommen: ['sedan','sport']

Erläuterung: 1. Zunächst erstellen wir ein neues Set, indem wir ein Array übergeben. Da Set nur eindeutige Werte zulässt, werden alle Duplikate entfernt.

  1. Jetzt, wo die Duplikate verschwunden sind, konvertieren wir sie wieder in ein Array, indem wir den Spread-Operator verwenden ...

Doku einstellen: 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

6voto

marko424 Punkte 3398

Sie können natives JavaScript verwenden group Array-Methode ( derzeit in Phase 3 ).

Ich denke, die Lösung ist viel eleganter, im Vergleich zu reduzieren, oder erreichen für Drittanbieter-Libs wie lodash etc.

const products = [{
    name: "milk",
    type: "dairy"
  },
  {
    name: "cheese",
    type: "dairy"
  },
  {
    name: "beef",
    type: "meat"
  },
  {
    name: "chicken",
    type: "meat"
  }
];

const productsByType = products.group((product) => product.type);

console.log("Grouped products by type: ", productsByType);

<script src="https://cdn.jsdelivr.net/npm/core-js-bundle@3.23.2/minified.min.js"></script>

4voto

Benny Powers Punkte 4558

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 zu sehen, mit Object Spread Syntax, wenn Ihre Umgebung 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 aus den verteilten Elementen des vorherigen Rückgabewerts sowie einem neuen Element besteht, dessen Schlüssel aus dem Wert des aktuellen Empfängers unter prop und dessen Wert eine Liste aller Werte für diese Eigenschaft zusammen mit dem aktuellen Wert ist.

3voto

Pasa89 Punkte 31

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

  • sehr leistungsfähig (Leistung Benchmark )
  • in Maschinenschrift geschrieben, so dass alle Tippfehler enthalten sind.
  • Es ist 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());

3voto

Julian Punkte 3558

Beantworten wir die ursprüngliche Frage vollständig, indem wir bereits geschriebenen Code (d. h. Underscore) wiederverwenden. Du kannst viel mehr mit Underscore machen, wenn du Mähdrescher seine >100 Funktionen. Die folgende Lösung veranschaulicht dies.

Schritt 1: Gruppieren Sie die Objekte im Array nach einer beliebigen Kombination von Eigenschaften. Dies nutzt die Tatsache, dass _.groupBy akzeptiert eine Funktion, die die Gruppe eines Objekts zurückgibt. Sie verwendet auch _.chain , _.pick , _.values , _.join y _.value . Beachten Sie, dass _.value ist hier nicht unbedingt erforderlich, da verkettete Werte bei der Verwendung als Eigenschaftsname automatisch abgewickelt werden. Ich füge sie ein, um Verwirrung vorzubeugen, falls jemand versucht, ähnlichen Code in einem Kontext zu schreiben, in dem das automatische Entpacken nicht stattfindet.

// Given an object, return a string naming the group it belongs to.
function category(obj) {
    return _.chain(obj).pick(propertyNames).values().join(' ').value();
}

// Perform the grouping.
const intermediate = _.groupBy(arrayOfObjects, category);

Angesichts der arrayOfObjects in der ursprünglichen Frage und Einstellung propertyNames zu ['Phase', 'Step'] , intermediate wird der folgende Wert angezeigt:

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

Schritt 2: Reduktion jeder Gruppe auf ein einzelnes flaches Objekt und Rückgabe der Ergebnisse in einem Array. Neben den Funktionen, die wir bereits gesehen haben, verwendet der folgende Code _.pluck , _.first , _.pick , _.extend , _.reduce y _.map . _.first wird in diesem Fall garantiert ein Objekt zurückgeben, da _.groupBy erzeugt keine leeren Gruppen. _.value ist in diesem Fall notwendig.

// Sum two numbers, even if they are contained in strings.
const addNumeric = (a, b) => +a + +b;

// Given a `group` of objects, return a flat object with their common
// properties and the sum of the property with name `aggregateProperty`.
function summarize(group) {
    const valuesToSum = _.pluck(group, aggregateProperty);
    return _.chain(group).first().pick(propertyNames).extend({
        [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
    }).value();
}

// Get an array with all the computed aggregates.
const result = _.map(intermediate, summarize);

Angesichts der intermediate die wir zuvor erhalten haben, und die Einstellung aggregateProperty zu Value erhalten wir die result die der Fragesteller wünscht:

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

Wir können dies alles in einer Funktion zusammenfassen, die arrayOfObjects , propertyNames y aggregateProperty als Parameter. Beachten Sie, dass arrayOfObjects kann eigentlich auch ein einfaches Objekt mit String-Schlüsseln sein, denn _.groupBy akzeptiert entweder. Aus diesem Grund habe ich den Namen arrayOfObjects zu collection .

function aggregate(collection, propertyNames, aggregateProperty) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    const addNumeric = (a, b) => +a + +b;
    function summarize(group) {
        const valuesToSum = _.pluck(group, aggregateProperty);
        return _.chain(group).first().pick(propertyNames).extend({
            [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
        }).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

aggregate(arrayOfObjects, ['Phase', 'Step'], 'Value') wird uns nun die gleiche result wieder.

Wir können noch einen Schritt weiter gehen und dem Aufrufer ermöglichen, Folgendes zu berechnen jede Statistik über die Werte in jeder Gruppe. Wir können dies tun und dem Aufrufer außerdem ermöglichen, der Zusammenfassung jeder Gruppe beliebige Eigenschaften hinzuzufügen. All dies können wir erreichen, indem wir unseren Code kürzer . Wir ersetzen die aggregateProperty Parameter durch einen iteratee Parameter und übergeben diesen direkt an _.reduce :

function aggregate(collection, propertyNames, iteratee) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    function summarize(group) {
        return _.chain(group).first().pick(propertyNames)
            .extend(_.reduce(group, iteratee)).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

Wir verlagern einen Teil der Verantwortung auf den Anrufer; er muss eine iteratee die übergeben werden können an _.reduce , so dass der Aufruf von _.reduce ein Objekt mit den Aggregateigenschaften, die sie hinzufügen möchte, erzeugt. Wir erhalten zum Beispiel das gleiche result wie zuvor mit dem folgenden Ausdruck:

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: +memo.Value + +value.Value
}));

Ein Beispiel für ein etwas anspruchsvolleres iteratee Angenommen, wir wollen die maximal Value der einzelnen Gruppen anstelle der Summe, und dass wir eine Tasks Eigenschaft, die alle Werte von Task die in der Gruppe auftreten. Hier ist eine Möglichkeit, wie wir dies tun können, indem wir die letzte Version von aggregate oben (und _.union ):

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: Math.max(memo.Value, value.Value),
    Tasks: _.union(memo.Tasks || [memo.Task], [value.Task])
}));

Wir erhalten das folgende Ergebnis:

[
    { Phase: "Phase 1", Step: "Step 1", Value: 10, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 1", Step: "Step 2", Value: 20, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 1", Value: 30, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 2", Value: 40, Tasks: [ "Task 1", "Task 2" ] }
]

Dank an @much2learn, der auch eine Antwort die beliebige reduzierende Funktionen verarbeiten kann. Ich habe ein paar weitere SO-Antworten geschrieben, die zeigen, wie man durch die Kombination mehrerer Underscore-Funktionen anspruchsvolle Dinge erreichen kann:

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X