Gruppieren von Elementen im Array nach mehreren Eigenschaften

158362
Saike

Während der Arbeit wurde mir folgende Aufgabe erteilt: Elemente mit ähnlichen Eigenschaften im Array zu gruppieren.

Im Allgemeinen ist das Problem wie folgt:

var list = [
    {name: "1", lastname: "foo1", age: "16"},
    {name: "2", lastname: "foo", age: "13"},
    {name: "3", lastname: "foo1", age: "11"},
    {name: "4", lastname: "foo", age: "11"},
    {name: "5", lastname: "foo1", age: "16"},
    {name: "6", lastname: "foo", age: "16"},
    {name: "7", lastname: "foo1", age: "13"},
    {name: "8", lastname: "foo1", age: "16"},
    {name: "9", lastname: "foo", age: "13"},
    {name: "0", lastname: "foo", age: "16"}
];

Wenn ich diese Elemente nach lastnameund gruppiere age, erhalte ich folgendes Ergebnis:

var result = [
    [
        {name: "1", lastname: "foo1", age: "16"},
        {name: "5", lastname: "foo1", age: "16"}, 
        {name: "8", lastname: "foo1", age: "16"}
    ],
    [
        {name: "2", lastname: "foo", age: "13"},
        {name: "9", lastname: "foo", age: "13"}
    ],
    [
        {name: "3", lastname: "foo1", age: "11"}
    ],
    [
        {name: "4", lastname: "foo", age: "11"}
    ],
    [
        {name: "6", lastname: "foo", age: "16"},
        {name: "0", lastname: "foo", age: "16"}
    ],
    [
        {name: "7", lastname: "foo1", age: "13"}
    ]         
];

Nach einigen Versuchen kam ich zu folgender Lösung:

    Array.prototype.groupByProperties = function(properties){
        var arr = this;
        var groups = [];
        for(var i = 0, len = arr.length; i<len; i+=1){
            var obj = arr[i];
            if(groups.length == 0){
                groups.push([obj]);
            }
            else{
                var equalGroup = false;
                for(var a = 0, glen = groups.length; a<glen;a+=1){
                    var group = groups[a];
                    var equal = true;
                    var firstElement = group[0];
                    properties.forEach(function(property){

                        if(firstElement[property] !== obj[property]){
                            equal = false;
                        }

                    });
                    if(equal){
                        equalGroup = group;
                    }
                }
                if(equalGroup){
                    equalGroup.push(obj);
                }
                else {
                    groups.push([obj]);
                }
            }
        }
        return groups;
    };

Diese Lösung funktioniert, aber ist dies ein richtiger und bester Weg? Es sieht für mich immer noch ein wenig hässlich aus.

Antworten
47
Es gibt ein npm-Modul mit dem Namen [group-array] (https://www.npmjs.com/package/group-array), das dieselbe Anforderung erfüllt. Gagan vor 4 Jahren 0
Warum nicht einfach das Array nach diesen Werten sortieren? Greg Burghardt vor 4 Jahren 0

4 Antworten auf die Frage

52
konijn

I felt compelled to write that you probably should combine forEach and map with the answer of Alexey Lebedev.

function groupBy( array, f )
{
  var groups = {};
  array.forEach( function( o )
  {
    var group = JSON.stringify( f(o) );
    groups[group] = groups[group] || [];
    groups[group].push( o );  
  });
  return Object.keys(groups).map( function( group )
  {
    return groups[group]; 
  })
}

var result = groupBy(list, function(item)
{
  return [item.lastname, item.age];
});
21
Alexey Lebedev

The main problem with your function is quadratic time complexity in the worst case. Also, if we first implement a general groupBy function, grouping by properties becomes trivial.

function arrayFromObject(obj) {
    var arr = [];
    for (var i in obj) {
        arr.push(obj[i]);
    }
    return arr;
}

function groupBy(list, fn) {
    var groups = {};
    for (var i = 0; i < list.length; i++) {
        var group = JSON.stringify(fn(list[i]));
        if (group in groups) {
            groups[group].push(list[i]);
        } else {
            groups[group] = [list[i]];
        }
    }
    return arrayFromObject(groups);
}

var result = groupBy(list, function(item) {
    return [item.lastname, item.age];
});

You might want to add hasOwnProperty check into arrayFromObject, if your coding convention doesn't forbid extending object prototype.

Vielleicht sollten Sie erwägen, eine Serialisierungsfunktion an Ihre groupBy-Methode zu übergeben. Bei toString gibt es mehrere Probleme: `item1 = {lastName: [1,2], age: 3}` und `item2 = {lastName: 1, age: [2,3]}` und `item3 = {lastName: ' 1,2 ', age: 3} `werden in Ihrem Beispiel alle in dieselbe Gruppe gestellt. Tibos vor 6 Jahren 0
Tibos: Du hast recht, das ist ein Fehler, der wartet. Ich habe `toString ()` in `JSON.stringify ()` geändert. Die Geschwindigkeit ist vergleichbar, vorausgesetzt, JSON wird nativ implementiert. Alexey Lebedev vor 6 Jahren 0
Die Verwendung des Stringify als Schlüssel ist brillant, +1 konijn vor 6 Jahren 2
@AlexeyLebedev eine brillante Lösung! Angenommen, ich möchte Kleinbuchstaben, um den Fall zu ignorieren, wie es geht? loretoparisi vor einem Jahr 0
@loretoparisi `return [item.lastname.toLowerCase (), item.age];` Alexey Lebedev vor einem Jahr 1
7
Tibos

I find the functional aspect of JavaScript to be a big advantage. When it comes to looping, Array.prototype.forEach and cousins can help your code be more descriptive:

Array.prototype.defineProperty('groupByProperties', {
    value : function(properties){                       
        // will contain grouped items
        var result = []; 

        // iterate over each item in the original array
        this.forEach(function(item){
            // check if the item belongs in an already created group
            var added = result.some(function(group){
                // check if the item belongs in this group
                var shouldAdd = properties.every(function(prop){
                    return (group[0][prop] === item[prop]);
                });
                // add item to this group if it belongs 
                if (shouldAdd) {
                    group.push(item);
                }
                // exit the loop when an item is added, continue if not
                return shouldAdd;
            });

            // no matching group was found, so a new group needs to be created for this item
            if (!added) {
                result.push([item]);
            }
        });
        return result;
    }
});

While i don't consider it a best practice to add custom functionality to predefined objects (Array.prototype in this case), i left that part of your solution in. However, I added groupByProperties as a non-enumerable property of Array.prototype so it doesn't appear in for..in enumerations.

Großartig! Einige Tests zeigen jedoch, dass die forEach-Methode viel langsamer ist als die Schleife mit vordefinierter Länge. Sie sind anderer Meinung? =) Saike vor 6 Jahren 0
ForJach kann in der Tat etwa fünfmal langsamer sein als eine for-Schleife. Es ist immer noch ziemlich schnell und ich bin ein Fan von klarem Schreiben und der Optimierung von Engpässen, falls erforderlich. Tibos vor 6 Jahren 0
Das gleiche Problem besteht beim Erstellen einer lokalen Variablen, in der die Länge des Arrays gespeichert wird, über das Sie eine Schleife bilden. Es ist wahrscheinlich viel schneller, auf die lokale Variable zuzugreifen als auf array.length, aber wenn Sie iterieren, ist dies nicht der Engpass. Eine Schleife mit gespeicherten Variablen ist also ungefähr so ​​schnell wie eine reguläre Schleife mit array.length. (Die Ausnahme zu dieser Regel ist, wenn length keine statische Eigenschaft ist, sondern wie in einer Live-NodeList-Datei berechnet wird). Tibos vor 6 Jahren 0
0
nerijusgood

Eine andere Möglichkeit besteht darin, _lodash.groupBy oder _lodash.keyBy zu verwenden :

  1. Sie müssen nur wenige Codezeilen schreiben, um dasselbe Ergebnis zu erzielen:

    const Results = _.groupBy(list, 'lastname')
    

    Dadurch werden Ihre Ergebnisse nach dem Nachnamen gruppiert. In Ihrem Fall müssen Sie jedoch nach mehreren Eigenschaften gruppieren. Sie können dieses Snippet verwenden, um diese Funktion zu verzaubern.

  2. Natürlich können Sie diesen Code mehrmals verwenden.

  3. Mit Lodash können Sie die Module einzeln installieren (npm i lodash.groupby).

Ich glaube, auf diese Weise erhalten Sie einen kürzeren, wartungsfähigeren Code mit klaren Funktionen. Ich denke, das ist eine Alternative.

Willkommen bei Code Review! Sie haben eine alternative Lösung vorgestellt, den Code jedoch noch nicht überprüft. Bitte erläutern Sie Ihre Gründe (wie Ihre Lösung funktioniert und wie sie das Original verbessert), damit der Autor von Ihrem Denkprozess lernen kann. Pimgd vor 4 Jahren 0
Es ist vielleicht keine Codelösung, aber im Moment haben Sie keine Erklärung dafür, warum ** diese Funktionen verwendet werden sollten, außer "einfach sauberer Code", was nichts erklärt. Pimgd vor 4 Jahren 0