Méthode la plus efficace pour grouper sur un tableau d'objets

507

Quelle est la manière la plus efficace de grouper des objets dans un tableau?

Par exemple, étant donné ce tableau d'objets:

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

J'affiche ces informations dans un tableau. Je voudrais regrouper différentes méthodes, mais je veux additionner les valeurs.

J'utilise Underscore.js pour sa fonction groupby, ce qui est utile, mais ne fait pas tout le tour, parce que je ne veux pas qu'ils soient "divisés" mais "fusionnés", plus comme la group byméthode SQL .

Ce que je recherche serait en mesure de totaliser des valeurs spécifiques (si demandé).

Donc, si je faisais du groupby Phase, je voudrais recevoir:

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

Et si je faisais du groupy Phase/ Step, je recevrais:

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

Existe-t-il un script utile pour cela, ou devrais-je m'en tenir à utiliser Underscore.js, puis parcourir en boucle l'objet résultant pour faire les totaux moi-même?

Rail24
la source

Réponses:

755

Si vous voulez éviter les bibliothèques externes, vous pouvez implémenter de manière concise une version vanille de groupBy()like so:

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
la source
18
je modifierais ainsi: `` `return xs.reduce (function (rv, x) {var v = key instanceof Function? key (x): x [key]; (rv [v] = rv [v] || []). push (x); return rv;}, {}); `` `permettant aux fonctions de rappel de renvoyer un critère de tri
y_nk
110
En voici un qui génère un tableau et non un objet: 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;}, []); }
tomitrescak
24
Super, juste ce dont j'avais besoin. Au cas où quelqu'un d'autre en aurait besoin, voici la signature TypeScript:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Michael Sandino
4
Pour ce qu'elle vaut, la solution de tomitrescak, bien que pratique, est nettement moins efficace, car find () est probablement O (n). La solution dans la réponse est O (n), à partir de la réduction (l'affectation d'objet est O (1), tout comme push), tandis que le commentaire est O (n) * O (n) ou O (n ^ 2) ou à moins O (nlgn)
narthur157
21
Si quelqu'un est intéressé, j'ai créé une version plus lisible et annotée de cette fonction et l' ai mise dans un résumé : gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d J'ai trouvé utile de comprendre ce qui se passe réellement ici.
robmathers
228

Utilisation de l'objet Carte ES6:

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

À propos de Map: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

mortb
la source
@mortb, comment l'obtenir sans appeler la get()méthode? qui est je veux que la sortie soit affichée sans passer la clé
Fai Zal Dong
@FaiZalDong: Je ne sais pas ce qui serait le mieux pour votre cas? Si j'écris console.log(grouped.entries());dans l'exemple jsfiddle, il retourne un itérable qui se comporte comme un tableau de clés + valeurs. Pouvez-vous essayer cela et voir si cela aide?
mortb
7
Vous pouvez également essayerconsole.log(Array.from(grouped));
mortb
J'adore cette réponse, très flexible
benshabatnoam
pour voir le nombre d'éléments en groupes:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
Ahmet Şimşek
105

avec ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);
Joseph Nields
la source
3
Il faut un peu de temps pour s'y habituer, mais la plupart des modèles C ++ aussi
Levi Haskell
2
J'ai épuisé mes cerveaux et je n'ai toujours pas compris comment ça marche dans le monde ...result. Maintenant je ne peux pas dormir à cause de ça.
8
Élégant, mais douloureusement lent sur les tableaux plus grands!
infinity1975
4
lol qu'est-ce que c'est
Nino Škopac
1
solution simple. Merci
Ezequiel Tavares
59

Bien que la réponse linq soit intéressante, elle est également assez lourde. Mon approche est quelque peu différente:

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

Vous pouvez le voir en action sur JSBin .

Je n'ai rien vu dans Underscore qui fasse ce qui se haspasse, bien que je puisse le manquer. C'est à peu près la même chose _.contains, mais utilise _.isEqualplutôt que=== pour des comparaisons. En dehors de cela, le reste est spécifique au problème, bien qu'avec une tentative d'être générique.

DataGrouper.sum(data, ["Phase"])Retourne maintenant

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

Et DataGrouper.sum(data, ["Phase", "Step"])retourne

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

Mais ce sumn'est qu'une fonction potentielle ici. Vous pouvez en enregistrer d'autres à votre guise:

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

et maintenant DataGrouper.max(data, ["Phase", "Step"])je reviendrai

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

ou si vous l'avez enregistré:

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

puis appeler DataGrouper.tasks(data, ["Phase", "Step"]) vous

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

DataGrouperlui-même est une fonction. Vous pouvez l'appeler avec vos données et une liste des propriétés que vous souhaitez regrouper. Il renvoie un tableau dont les éléments sont des objets avec deux propriétés: keyest la collection de propriétés groupées, valsest un tableau d'objets contenant les propriétés restantes qui ne sont pas dans la clé. Par exemple,DataGrouper(data, ["Phase", "Step"]) donnera:

[
    {
        "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.registeraccepte une fonction et crée une nouvelle fonction qui accepte les données initiales et les propriétés à regrouper. Cette nouvelle fonction prend ensuite le format de sortie comme ci-dessus et exécute votre fonction contre chacun d'eux à son tour, en retournant un nouveau tableau. La fonction générée est stockée en tant que propriété deDataGrouper selon un nom que vous fournissez et est également renvoyée si vous souhaitez simplement une référence locale.

Eh bien, c'est beaucoup d'explications. Le code est assez simple, j'espère!

Scott Sauyet
la source
Salut .. Pouvez-vous voir grouper et additionner juste par une valeur, mais au cas où je voudrais additionner par value1 et valu2 et value3 ... u avez-vous une solution?
SAMUEL OSPINA du
@SAMUELOSPINA avez-vous déjà trouvé un moyen de le faire?
howMuchCheeseIsTooMuchCheese
50

Je vérifierais le groupe lodash il semble faire exactement ce que vous recherchez. Il est également assez léger et très simple.

Exemple de violon: https://jsfiddle.net/r7szvt5k/

À condition que le nom de votre tableau soit arrle groupBy avec lodash, c'est juste:

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
la source
1
Cette réponse n'est-elle pas problématique? Il existe plusieurs façons dont le résultat lodash _.groupBy n'est pas au format du résultat demandé par l'OP. (1) Le résultat n'est pas un tableau. (2) La "valeur" est devenue la "clé" du résultat du ou des objets lodash.
mg1075
44

Cela se fait probablement plus facilement avec linq.js, qui est destiné à être une véritable implémentation de LINQ en JavaScript ( DEMO ):

var linq = Enumerable.From(data);
var result =
    linq.GroupBy(function(x){ return x.Phase; })
        .Select(function(x){
          return {
            Phase: x.Key(),
            Value: x.Sum(function(y){ return y.Value|0; })
          };
        }).ToArray();

résultat:

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

Ou, plus simplement en utilisant les sélecteurs basés sur des chaînes ( DEMO ):

linq.GroupBy("$.Phase", "",
    "k,e => { Phase:k, Value:e.Sum('$.Value|0') }").ToArray();
mellamokb
la source
pouvons-nous utiliser plusieurs propriétés lors du regroupement ici:GroupBy(function(x){ return x.Phase; })
Amit
38

Vous pouvez construire un ES6 à Mappartir de array.reduce().

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

Cela présente quelques avantages par rapport aux autres solutions:

  • Il ne nécessite aucune bibliothèque (contrairement par exemple _.groupBy())
  • Vous obtenez un JavaScript Mapplutôt qu'un objet (par exemple tel que renvoyé par _.groupBy()). Cela présente de nombreux avantages , notamment:
    • il se souvient de l'ordre dans lequel les éléments ont été ajoutés pour la première fois,
    • les clés peuvent être de n'importe quel type plutôt que de simples chaînes.
  • A Mapest un résultat plus utile qu'un tableau de tableaux. Mais si vous voulez un tableau de tableaux, vous pouvez alors appeler Array.from(groupedMap.entries())(pour un tableau de [key, group array]paires) ou Array.from(groupedMap.values())(pour un tableau simple de tableaux).
  • C'est assez flexible; souvent, tout ce que vous prévoyez de faire ensuite avec cette carte peut être fait directement dans le cadre de la réduction.

Comme exemple du dernier point, imaginez que j'ai un tableau d'objets sur lequel je veux faire une fusion (superficielle) par id, comme ceci:

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

Pour ce faire, je commence généralement par regrouper par id, puis par fusionner chacun des tableaux résultants. Au lieu de cela, vous pouvez effectuer la fusion directement dans reduce():

const mergedArray = Array.from(
    objsToMerge.reduce(
        (entryMap, e) => entryMap.set(e.id, {...entryMap.get(e.id)||{}, ...e}),
        new Map()
    ).values()
);
Arthur Tacca
la source
1
Je ne sais pas pourquoi cela n'a pas plus de votes. C'est concis, lisible (pour moi) et semble efficace. Il ne vole pas sur IE11 , mais la mise à niveau n'est pas trop difficile ( a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map()), environ)
débob
24
_.groupBy([{tipo: 'A' },{tipo: 'A'}, {tipo: 'B'}], 'tipo');
>> Object {A: Array[2], B: Array[1]}

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

Julio Marins
la source
18

Vous pouvez le faire avec la bibliothèque 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]);

Essayez cet exemple sur jsFiddle .

BTW: Sur de grands tableaux (100 000 enregistrements et plus) Alasql plus rapide que Linq. Voir test sur jsPref .

Commentaires:

  • Ici, je mets Value entre crochets, car VALUE est un mot clé en SQL
  • Je dois utiliser la fonction CAST () pour convertir les valeurs de chaîne en type numérique.
agershun
la source
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
la source
15

MDN a cet exemple dans sa Array.reduce()documentation.

// 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
la source
14

Bien que la question ait des réponses et que les réponses semblent un peu trop compliquées, je suggère d'utiliser Javascript vanilla pour le regroupement avec un imbriqué (si nécessaire) 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
la source
9

Sans mutations:

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"]}
Bénir
la source
8

Cette solution prend n'importe quelle fonction arbitraire (pas une clé), elle est donc plus flexible que les solutions ci-dessus et autorise les fonctions fléchées , qui sont similaires aux expressions lambda utilisées dans LINQ :

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

REMARQUE: vous décidez si vous souhaitez étendre Arrayle prototype de.

Exemple pris en charge dans la plupart des navigateurs:

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

Exemple d'utilisation des fonctions fléchées (ES6):

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

Les deux exemples ci-dessus renvoient:

{
  "1": [{"a": 1, "b": "b"}, {"a": 1, "c": "c"}],
  "2": [{"a": 2, "d": "d"}]
}
Diego
la source
J'ai beaucoup aimé la solution ES6. Juste une petite semplification sans étendre le prototype Array:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
caneta
8

je voudrais suggérer mon approche. Premièrement, regroupement et agrégation séparés. Permet de déclarer la fonction prototypique "group by". Il faut une autre fonction pour produire une chaîne de «hachage» pour chaque élément de tableau à regrouper.

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

lorsque le regroupement est effectué, vous pouvez agréger les données selon vos besoins, dans votre cas

data.groupBy(function(o){return JSON.stringify({a: o.Phase, b: o.Step});})
    /* aggreagating */
    .map(function(el){ 
         var sum = el.group.reduce(
           function(l,c){
             return l + parseInt(c.Value);
           },
           0
         );
         el.key.Value = sum; 
         return el.key;
    });

en commun ça marche. j'ai testé ce code dans la console chrome. et n'hésitez pas à vous améliorer et à trouver des erreurs;)

Anton
la source
Merci ! J'adore l'approche et convient parfaitement à mes besoins (je n'ai pas besoin d'agréger).
aberaud
Je pense que vous voulez changer votre ligne en put (): map[_hash(key)].key = key;to map[_hash(key)].key = _hash(key);.
Scotty.NET
6

Imaginez que vous ayez quelque chose comme ça:

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

En faisant cela: const categories = [...new Set(cars.map((car) => car.cat))]

Vous obtiendrez ceci: ['sedan','sport']

Explication: 1. Premièrement, nous créons un nouvel ensemble en passant un tableau. Étant donné que Set n'autorise que des valeurs uniques, tous les doublons seront supprimés.

  1. Maintenant que les doublons ont disparu, nous allons le reconvertir en tableau à l'aide de l'opérateur d'étalement ...

Définissez 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 / Référence / Opérateurs / Spread_syntax

Yago Gehres
la source
j'aime beaucoup votre réponse, c'est la plus courte, mais je ne comprends toujours pas la logique, surtout, qui fait le regroupement ici? est-ce un opérateur de spread (...)? ou le «nouveau Set ()»? veuillez nous l'expliquer ... merci
Ivan
1
1. Premièrement, nous créons un nouvel ensemble en passant un tableau. Étant donné que Set n'autorise que des valeurs uniques, tous les doublons seront supprimés. 2. Maintenant que les doublons ont disparu, nous allons le reconvertir en un tableau à l'aide de l'opérateur de diffusion ... Définissez Doc: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Spread Opérateur: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Yago Gehres
ok, j'ai compris ... merci pour l'explication monsieur :)
Ivan
vous êtes les bienvenus!
Yago Gehres
6

Réponse vérifiée - juste un regroupement superficiel. C'est assez agréable de comprendre la réduction. La question pose également le problème des calculs agrégés supplémentaires.

Voici un REAL GROUP BY pour Array of Objects par certains champs avec 1) le nom de clé calculé et 2) une solution complète pour la mise en cascade du regroupement en fournissant la liste des clés souhaitées et en convertissant ses valeurs uniques en clés racine comme SQL GROUP BY le fait.

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

Jouez avec estKey- vous pouvez regrouper par plus d'un champ, ajouter des agrégations, des calculs ou d'autres traitements supplémentaires.

Vous pouvez également regrouper les données de manière récursive. Par exemple, groupez d'abord par Phase, puis par Stepchamp, etc. Soufflez également les données de repos graisseux.

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

Celui-ci génère un tableau.

tomitrescak
la source
4

Basé sur les réponses précédentes

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

et c'est un peu plus agréable à regarder avec la syntaxe de propagation d'objet, si votre environnement le prend en charge.

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

Ici, notre réducteur prend la valeur de retour partiellement formée (en commençant par un objet vide) et retourne un objet composé des membres étalés de la valeur de retour précédente, ainsi qu'un nouveau membre dont la clé est calculée à partir de la valeur de l'itéré en cours à propet dont la valeur est une liste de toutes les valeurs de cet accessoire avec la valeur actuelle.

Benny Powers
la source
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
la source
3

Voici une solution désagréable et difficile à lire avec ES6:

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

Pour ceux qui demandent comment ce même travail, voici une explication:

  • Dans les deux, =>vous avez unreturn

  • La Array.prototype.reducefonction prend jusqu'à 4 paramètres. C'est pourquoi un cinquième paramètre est ajouté afin que nous puissions avoir une déclaration de variable bon marché pour le groupe (k) au niveau de la déclaration de paramètre en utilisant une valeur par défaut. (oui, c'est de la sorcellerie)

  • Si notre groupe actuel n'existe pas sur l'itération précédente, nous créons un nouveau tableau vide ((r[k] || (r[k] = []))Cela évaluera à l'expression la plus à gauche, en d'autres termes, un tableau existant ou un tableau vide , c'est pourquoi il y a un immédiat pushaprès cette expression, car de toute façon, vous obtiendrez un tableau.

  • Lorsqu'il y a un return, l' ,opérateur virgule supprimera la valeur la plus à gauche, renvoyant le groupe précédent modifié pour ce scénario.

Une version plus facile à comprendre qui fait de même est:

export default (array, key) => 
  array.reduce((previous, currentItem) => {
    const group = currentItem[key];
    if (!previous[group]) previous[group] = [];
    previous[group].push(currentItem);
    return previous;
  }, {});
darkndream
la source
2
voudriez-vous expliquer un peu cela, cela fonctionne parfaitement
Nuwan Dammika
@NuwanDammika - Dans les deux => vous avez un "retour" gratuit - La fonction de réduction prend jusqu'à 4 paramètres. C'est pourquoi un cinquième paramètre est ajouté afin que nous puissions avoir une déclaration de variable bon marché pour le groupe (k). - Si la valeur précédente n'a pas notre groupe actuel, nous créons un nouveau groupe vide ((r [k] || (r [k] = [])) Ceci évaluera l'expression la plus à gauche, sinon un tableau ou un tableau vide, c'est pourquoi il y a une poussée immédiate après cette expression. - Quand il y a un retour, l'opérateur de virgule supprimera la valeur la plus à gauche, retournant le groupe précédent modifié.
darkndream
2

Permet de générer un Array.prototype.groupBy()outil générique . Pour plus de variété, utilisons ES6 fanciness l'opérateur de propagation pour une correspondance de motifs haskellesque sur une approche récursive. Faisons également en sorte Array.prototype.groupBy()d'accepter un rappel qui prend comme élément l' argument item ( e), l'index ( i) et le tableau appliqué ( a).

Array.prototype.groupBy = function(cb){
                            return function iterate([x,...xs], i = 0, r = [[],[]]){
                                     cb(x,i,[x,...xs]) ? (r[0].push(x), r)
                                                       : (r[1].push(x), r);
                                     return xs.length ? iterate(xs, ++i, r) : r;
                                   }(this);
                          };

var arr = [0,1,2,3,4,5,6,7,8,9],
    res = arr.groupBy(e => e < 5);
console.log(res);

Redu
la source
2

La réponse de Ceasar est bonne, mais ne fonctionne que pour les propriétés internes des éléments à l'intérieur du tableau (longueur en cas de chaîne).

cette implémentation fonctionne plus comme: ce lien

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

J'espère que cela t'aides...

Roey
la source
2

De @mortb, @jmarceli réponse et de ce post ,

J'en profite JSON.stringify()pour être l'identité des multiples colonnes de PRIMITIVE VALUE de group by.

Sans tiers

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

Avec Lodash tiers

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.
la source
keyGetter revient indéfini
Asbar Ali
@AsbarAli J'ai testé mon extrait de code avec la console de Chrome - Version 66.0.3359.139 (version officielle) (64 bits). Et tout se passe bien. Pourriez-vous s'il vous plaît mettre le point d'arrêt de débogage et voir pourquoi keyGetter n'est pas défini. C'est peut-être à cause de la version du navigateur.
Pranithan T.
2

Version reduce basée sur ES6 version avec prise en charge de la fonction iteratee.

Fonctionne comme prévu si la iterateefonction n'est pas fournie:

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

Dans le cadre de la question OP:

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

const groupBy = (arr, k) => arr.reduce((r, c) => (r[c[k]] = [...r[c[k]] || [], c], r), {});
const groupWith = (arr, k, fn = () => true) => 
  arr.reduce((r, c) => (fn(c[k]) ? r[c[k]] = [...r[c[k]] || [], c] : null, r), {});

console.log(groupBy(data, 'Phase'))
console.log(groupWith(data, 'Value', x => x > 30 ))  // group by `Value` > 30

Une autre version ES6 qui inverse le regroupement et utilise les valuesas keyset les keysas les 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))

Remarque: les fonctions sont affichées sous leur forme abrégée (une ligne) par souci de concision et pour ne relier que l'idée. Vous pouvez les développer et ajouter une vérification d'erreur supplémentaire, etc.

Akrion
la source
2

Je vérifierais déclarative-js, groupBy il semble faire exactement ce que vous recherchez. C'est aussi:

  • très performant ( benchmark de performance )
  • écrit en tapuscript afin que tous les fautes de frappe soient incluses.
  • Il n'est pas obligatoire d'utiliser des objets de type tableau tiers.
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
la source
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
la source
1

Voici une version ES6 qui ne se cassera pas sur les membres nuls

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

Pour ajouter à la réponse de Scott Sauyet , certaines personnes demandaient dans les commentaires comment utiliser sa fonction pour grouper valeur1, valeur2, etc., au lieu de regrouper une seule valeur.

Il suffit d'éditer sa fonction somme:

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

laissant le principal (DataGrouper) inchangé:

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
la source
1

Avec fonction de tri

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

Échantillon:

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
la source