Najbardziej wydajna metoda grupowania według tablicy obiektów


507

Jaki jest najskuteczniejszy sposób grupowania obiektów w tablicy?

Na przykład, biorąc pod uwagę tę tablicę obiektów:

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

Wyświetlam te informacje w tabeli. Chciałbym pogrupować według różnych metod, ale chcę zsumować wartości.

Korzystam z Underscore.js do funkcji grupowania, która jest pomocna, ale nie robi to całej sztuczki, ponieważ nie chcę, aby były „dzielone”, ale „łączone”, bardziej jak group bymetoda SQL .

To, czego szukam, byłoby w stanie zsumować określone wartości (na żądanie).

Więc jeśli zrobiłem grupowanie Phase, chciałbym otrzymać:

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

A gdybym zrobił groupy Phase/ Step, otrzymałbym:

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

Czy istnieje przydatny skrypt do tego, czy powinienem trzymać się Underscore.js, a następnie zapętlać wynikowy obiekt, aby samodzielnie sumować?

Odpowiedzi:


755

Jeśli chcesz uniknąć bibliotek zewnętrznych, możesz zwięźle zaimplementować waniliową wersję tego groupBy()typu:

var groupBy = function(xs, key) {
  return xs.reduce(function(rv, x) {
    (rv[x[key]] = rv[x[key]] || []).push(x);
    return rv;
  }, {});
};

console.log(groupBy(['one', 'two', 'three'], 'length'));

// => {3: ["one", "two"], 5: ["three"]}


18
zmodyfikowałbym w ten sposób: `` return xs.reduce (function (rv, x) {var v = key instanceof Function? key (x): x [key]; (rv [v] = rv [v] || []). push (x); return rv;}, {}); `` umożliwiając funkcjom zwrotnym zwrócenie kryteriów sortowania
y_nk

109
Oto jeden, który wyprowadza tablicę, a nie obiekt: 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, wartości: [x] });} return rv;}, []); }
tomitrescak,

24
Świetnie, właśnie tego potrzebowałem. Jeśli ktoś tego potrzebuje, oto podpis TypeScript:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Michael Sandino,

4
Jeśli chodzi o to, co jest warte, rozwiązanie tomitrescak, chociaż wygodne, jest znacznie mniej wydajne, ponieważ find () to prawdopodobnie O (n). Rozwiązaniem w odpowiedzi jest O (n), od redukcji (przypisanie obiektu to O (1), podobnie jak push), podczas gdy komentarz to O (n) * O (n) lub O (n ^ 2) lub w najmniej O (nlgn)
narthur157 11.07.18

21
Jeśli ktoś jest zainteresowany, stworzyłem bardziej czytelną i opatrzoną adnotacjami wersję tej funkcji i umieściłem ją w skrócie : gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d Uznałem, że jest to pomocne w zrozumieniu, co się tu właściwie dzieje.
robmathers,

228

Za pomocą obiektu mapy 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"}]
    
    

Informacje o mapie: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map


@mortb, jak go zdobyć bez wywoływania get()metody? czyli chcę, aby wyjście wyświetlało się bez podania klucza
Fai Zal Dong,

@FaiZalDong: Nie jestem pewien, co byłoby najlepsze dla twojej sprawy? Jeśli piszę console.log(grouped.entries());w przykładzie jsfiddle, zwraca iterowalność, która zachowuje się jak tablica kluczy + wartości. Czy możesz spróbować i sprawdzić, czy to pomaga?
mortb

7
Możesz także spróbowaćconsole.log(Array.from(grouped));
mortb

Uwielbiam tę odpowiedź, bardzo elastyczną
benshabatnoam

aby zobaczyć liczbę elementów w grupach:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
Ahmet Şimşek

105

z ES6:

const groupBy = (items, key) => items.reduce(
  (result, item) => ({
    ...result,
    [item[key]]: [
      ...(result[item[key]] || []),
      item,
    ],
  }), 
  {},
);

3
Trochę się przyzwyczaja, ale także większość szablonów C ++
Levi Haskell,

2
Zniszczyłem mózg i nadal nie rozumiem, jak na świecie to działa, poczynając od ...result. Teraz nie mogę z tego powodu spać.

8
Elegancki, ale boleśnie powolny na większych tablicach!
infinity1975,

4
lol co to jest
Nino Škopac

1
łatwe rozwiązanie. Dzięki
Ezequiel Tavares

59

Chociaż odpowiedź linq jest interesująca, jest również dość ciężka. Moje podejście jest nieco inne:

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

Możesz to zobaczyć w akcji na JSBin .

W Underscore nie widziałem nic, co by działało has, chociaż może mi tego brakować. Jest bardzo podobny _.contains, ale używa _.isEqualraczej niż ===do porównań. Poza tym reszta tego jest specyficzna dla problemu, chociaż z próbą bycia ogólną.

Teraz DataGrouper.sum(data, ["Phase"])wraca

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

I DataGrouper.sum(data, ["Phase", "Step"])wraca

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

Ale sumjest tu tylko jedna potencjalna funkcja. Możesz zarejestrować innych, jak chcesz:

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

a teraz DataGrouper.max(data, ["Phase", "Step"])wróci

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

lub jeśli to zarejestrowałeś:

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

następnie wywołanie DataGrouper.tasks(data, ["Phase", "Step"])będzie Ci

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

DataGroupersama w sobie jest funkcją. Możesz to nazwać danymi i listą właściwości, według których chcesz pogrupować. Zwraca tablicę, której elementy są obiektami o dwóch właściwościach: keyjest zbiorem właściwości pogrupowanych, valsjest tablicą obiektów zawierających pozostałe właściwości nie zawarte w kluczu. Na przykład DataGrouper(data, ["Phase", "Step"])da:

[
    {
        "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.registerakceptuje funkcję i tworzy nową funkcję, która akceptuje początkowe dane i właściwości do grupowania według. Ta nowa funkcja przyjmuje format wyjściowy jak wyżej i uruchamia twoją funkcję kolejno dla każdej z nich, zwracając nową tablicę. Wygenerowana funkcja jest przechowywana jako właściwość DataGrouperwedług podanej przez Ciebie nazwy, a także zwracana, jeśli potrzebujesz tylko lokalnego odwołania.

To wiele wyjaśnień. Mam nadzieję, że kod jest dość prosty!


Cześć .. Widzisz, że grupujesz według sumy według wartości, ale w przypadku, gdy chcę sumy według wartości1 i wartości2 i wartości3 ... masz rozwiązanie?
SAMUEL OSPINA

@SAMUELOSPINA, czy kiedykolwiek znalazłeś sposób, aby to zrobić?
howMuchCheeseIsTooMuchCheese

50

Sprawdziłbym grupę lodash, ponieważ wydaje się, że robi dokładnie to, czego szukasz. Jest również dość lekki i bardzo prosty.

Przykład Fiddle: https://jsfiddle.net/r7szvt5k/

Pod warunkiem, że twoja nazwa tablicy to arrgrupa, a z lodash jest po prostu:

import groupBy from 'lodash/groupBy';
// if you still use require:
// const groupBy = require('lodash/groupBy');

const a = groupBy(arr, function(n) {
  return n.Phase;
});
// a is your array grouped by Phase attribute

1
Czy ta odpowiedź nie jest problematyczna? Istnieje wiele sposobów, w których wynik lodash _.groupBy nie jest w formacie wyniku, o który prosi OP. (1) Wynikiem nie jest tablica. (2) „Wartość” stała się „kluczem” w wyniku obiektu (obiektów) lodash.
mg1075

44

Prawdopodobnie łatwiej to zrobić linq.js, co ma być prawdziwą implementacją LINQ w 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();

wynik:

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

Lub, po prostu, za pomocą selektorów opartych na łańcuchach ( DEMO ):

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

czy możemy użyć wielu właściwości podczas grupowania tutaj:GroupBy(function(x){ return x.Phase; })
Amit

37

Możesz zbudować ES6 Mapz array.reduce().

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

Ma to kilka zalet w stosunku do innych rozwiązań:

  • Nie wymaga żadnych bibliotek (w przeciwieństwie do np _.groupBy() )
  • Otrzymujesz JavaScript Mapzamiast obiektu (np. W postaci zwróconej przez _.groupBy()). Ma to wiele zalet , w tym:
    • pamięta kolejność, w jakiej elementy zostały dodane po raz pierwszy,
    • klucze mogą być dowolnego typu, a nie tylko ciągami znaków.
  • A Mapjest bardziej użytecznym wynikiem niż tablica tablic. Ale jeśli chcesz tablicę tablic, możesz następnie wywołać Array.from(groupedMap.entries())(dla tablicy [key, group array]par) lub Array.from(groupedMap.values())(dla prostej tablicy tablic).
  • Jest dość elastyczny; często to, co planujesz zrobić z tą mapą, można zrobić bezpośrednio w ramach redukcji.

Jako przykład ostatniego punktu, wyobraźmy sobie, że mam tablicę obiektów, na których chcę wykonać (płytkie) scalenie według identyfikatora:

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

Aby to zrobić, zwykle zaczynam od pogrupowania według identyfikatora, a następnie scalenia każdej z powstałych tablic. Zamiast tego możesz wykonać scalanie bezpośrednio w reduce():

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

1
Nie wiem, dlaczego nie ma więcej głosów. Jest zwięzły, czytelny (dla mnie) i wygląda wydajnie. Nie leci na IE11 , ale modernizacja nie jest zbyt trudna ( a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map()), w przybliżeniu)
unbob


18

Możesz to zrobić za pomocą biblioteki 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]);

Spróbuj tego przykładu w jsFiddle .

BTW: Na dużych tablicach (100000 rekordów i więcej) Alasql szybciej niż Linq. Zobacz test w jsPref .

Komentarze:

  • Tutaj wstawiam wartość w nawiasach kwadratowych, ponieważ VALUE jest słowem kluczowym w SQL
  • Muszę użyć funkcji CAST (), aby przekonwertować ciąg Wartości na typ liczbowy.

18
Array.prototype.groupBy = function(keyFunction) {
    var groups = {};
    this.forEach(function(el) {
        var key = keyFunction(el);
        if (key in groups == false) {
            groups[key] = [];
        }
        groups[key].push(el);
    });
    return Object.keys(groups).map(function(key) {
        return {
            key: key,
            values: groups[key]
        };
    });
};

15

MDN ma ten przykład w swojej Array.reduce()dokumentacji.

// Grouping objects by a property
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce#Grouping_objects_by_a_property#Grouping_objects_by_a_property

var people = [
  { name: 'Alice', age: 21 },
  { name: 'Max', age: 20 },
  { name: 'Jane', age: 20 }
];

function groupBy(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {
    var key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

var groupedPeople = groupBy(people, 'age');
// groupedPeople is:
// { 
//   20: [
//     { name: 'Max', age: 20 }, 
//     { name: 'Jane', age: 20 }
//   ], 
//   21: [{ name: 'Alice', age: 21 }] 
// }

14

Chociaż pytanie ma kilka odpowiedzi, a odpowiedzi wyglądają na nieco skomplikowane, sugeruję użycie JavaScript waniliowego do grupowania z zagnieżdżonym (jeśli to konieczne) Map.

function groupBy(array, groups, valueKey) {
    var map = new Map;
    groups = [].concat(groups);
    return array.reduce((r, o) => {
        groups.reduce((m, k, i, { length }) => {
            var child;
            if (m.has(o[k])) return m.get(o[k]);
            if (i + 1 === length) {
                child = Object
                    .assign(...groups.map(k => ({ [k]: o[k] })), { [valueKey]: 0 });
                r.push(child);
            } else {
                child = new Map;
            }
            m.set(o[k], child);
            return child;
        }, map)[valueKey] += +o[valueKey];
        return r;
    }, [])
};

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

console.log(groupBy(data, 'Phase', 'Value'));
console.log(groupBy(data, ['Phase', 'Step'], 'Value'));
.as-console-wrapper { max-height: 100% !important; top: 0; }


9

Bez mutacji:

const groupBy = (xs, key) => xs.reduce((acc, x) => Object.assign({}, acc, {
  [x[key]]: (acc[x[key]] || []).concat(x)
}), {})

console.log(groupBy(['one', 'two', 'three'], 'length'));
// => {3: ["one", "two"], 5: ["three"]}

8

To rozwiązanie przyjmuje dowolną funkcję (nie klucz), więc jest bardziej elastyczne niż powyższe rozwiązania i umożliwia funkcje strzałek , które są podobne do wyrażeń lambda używanych w LINQ :

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

UWAGA: to, czy chcesz przedłużyć Arrayprototyp, zależy od Ciebie.

Przykład obsługiwany w większości przeglądarek:

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

Przykład użycia funkcji strzałek (ES6):

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

Oba powyższe przykłady zwracają:

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

Bardzo podobało mi się rozwiązanie ES6. Tylko trochę semplification bez rozszerzania prototyp tablicy:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
caneta

8

chciałbym zasugerować moje podejście. Po pierwsze, oddzielne grupowanie i agregowanie. Deklarujmy prototypową funkcję „grupuj według”. Do wygenerowania łańcucha „haszującego” dla każdego elementu tablicy potrzebna jest inna funkcja.

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

po zakończeniu grupowania możesz w swoim przypadku agregować dane według potrzeb

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

wspólne to działa. Przetestowałem ten kod w konsoli Chrome. i nie krępuj się ulepszać i znajdować błędy;)


Dzięki ! Uwielbiam to podejście i idealnie odpowiada moim potrzebom (nie potrzebuję agregacji).
aberaud

Myślę, że chcesz zmienić linię w put (): map[_hash(key)].key = key;to map[_hash(key)].key = _hash(key);.
Scotty.NET,

6

Wyobraź sobie, że masz coś takiego:

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

Robiąc to: const categories = [...new Set(cars.map((car) => car.cat))]

Otrzymasz to: ['sedan','sport']

Objaśnienie: 1. Najpierw tworzymy nowy zestaw, przekazując tablicę. Ponieważ zestaw dopuszcza tylko unikalne wartości, wszystkie duplikaty zostaną usunięte.

  1. Teraz, gdy duplikaty zniknęły, przekonwertujemy je z powrotem na tablicę za pomocą operatora rozprzestrzeniania ...

Ustaw 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 / Reference / Operators / Spread_syntax


bardzo podoba mi się twoja odpowiedź, jest najkrótsza, ale nadal nie rozumiem logiki, szczególnie, kto tutaj grupuje? czy jest operatorem rozprzestrzeniania (...)? lub „nowy Set ()”? wyjaśnij nam to ... dziękuję
Ivan

1
1. Najpierw tworzymy nowy zestaw, przekazując tablicę. Ponieważ zestaw dopuszcza tylko unikalne wartości, wszystkie duplikaty zostaną usunięte. 2. Teraz, gdy duplikaty zniknęły, przekonwertujemy je z powrotem na tablicę za pomocą operatora rozprzestrzeniania ... Ustaw Doc: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/... Spread Operator: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Yago Gehres

ok, rozumiem ... dziękuję za wyjaśnienie, proszę pana :)
Ivan

nie ma za co!
Yago Gehres

6

Sprawdzona odpowiedź - tylko płytkie grupowanie. Miło jest zrozumieć redukcję. Pytanie zawiera również problem dodatkowych obliczeń zagregowanych.

Oto PRAWDZIWA GRUPA BY dla Array of Objects według niektórych pól z 1) obliczoną nazwą klucza i 2) kompletnym rozwiązaniem do kaskadowania grupowania poprzez dostarczenie listy pożądanych kluczy i przekształcenie ich unikalnych wartości w klucze główne, takie jak SQL GROUP BY robi.

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

Graj z estKey- możesz grupować według więcej niż jednego pola, dodawać dodatkowe agregacje, obliczenia lub inne przetwarzanie.

Możesz także grupować dane rekurencyjnie. Na przykład najpierw pogrupuj według Phase, następnie według Steppól i tak dalej. Dodatkowo zdmuchnij dane dotyczące odpoczynku tłuszczu.

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

/**
 * Small helper to get SHALLOW copy of obj WITHOUT prop
 */
const rmProp = (obj, prop) => ( (({[prop]:_, ...rest})=>rest)(obj) )

/**
 * Group Array by key. Root keys of a resulting array is value
 * of specified key.
 *
 * @param      {Array}   src     The source array
 * @param      {String}  key     The by key to group by
 * @return     {Object}          Object with groupped objects as values
 */
const grpBy = (src, key) => src.reduce((a, e) => (
  (a[e[key]] = a[e[key]] || []).push(rmProp(e, key)),  a
), {});

/**
 * Collapse array of object if it consists of only object with single value.
 * Replace it by the rest value.
 */
const blowObj = obj => Array.isArray(obj) && obj.length === 1 && Object.values(obj[0]).length === 1 ? Object.values(obj[0])[0] : obj;

/**
 * Recoursive groupping with list of keys. `keyList` may be an array
 * of key names or comma separated list of key names whom UNIQUE values will
 * becomes the keys of the resulting object.
 */
const grpByReal = function (src, keyList) {
  const [key, ...rest] = Array.isArray(keyList) ? keyList : String(keyList).trim().split(/\s*,\s*/);
  const res = key ? grpBy(src, key) : [...src];
  if (rest.length) {
for (const k in res) {
  res[k] = grpByReal(res[k], rest)
}
  } else {
for (const k in res) {
  res[k] = blowObj(res[k])
}
  }
  return res;
}

console.log( JSON.stringify( grpByReal(inputArray, 'Phase, Step, Task'), null, 2 ) );


5
groupByArray(xs, key) {
    return xs.reduce(function (rv, x) {
        let v = key instanceof Function ? key(x) : x[key];
        let el = rv.find((r) => r && r.key === v);
        if (el) {
            el.values.push(x);
        }
        else {
            rv.push({
                key: v,
                values: [x]
            });
        }
        return rv;
    }, []);
}

Ten wyprowadza tablicę.


4

Na podstawie poprzednich odpowiedzi

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

i ładniej jest patrzeć na składnię rozproszenia obiektów, jeśli twoje środowisko to obsługuje.

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

W tym przypadku nasz reduktor przyjmuje częściowo uformowaną wartość zwracaną (zaczynając od pustego obiektu) i zwraca obiekt złożony z rozłożonych elementów poprzedniej wartości zwracanej wraz z nowym elementem, którego klucz jest obliczany na podstawie bieżącej wartości sędziego w propi którego wartość jest listą wszystkich wartości tego rekwizytu wraz z bieżącą wartością.


3

Array.prototype.groupBy = function (groupingKeyFn) {
    if (typeof groupingKeyFn !== 'function') {
        throw new Error("groupBy take a function as only parameter");
    }
    return this.reduce((result, item) => {
        let key = groupingKeyFn(item);
        if (!result[key])
            result[key] = [];
        result[key].push(item);
        return result;
    }, {});
}

var a = [
	{type: "video", name: "a"},
  {type: "image", name: "b"},
  {type: "video", name: "c"},
  {type: "blog", name: "d"},
  {type: "video", name: "e"},
]
console.log(a.groupBy((item) => item.type));
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>


3

Oto nieprzyjemne, trudne do odczytania rozwiązanie za pomocą ES6:

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

Dla tych, którzy pytają, jak to w ogóle działa, oto wyjaśnienie:

  • W obu =>masz bezpłatnyreturn

  • Array.prototype.reduceFunkcji zajmuje do 4 parametrów. Właśnie dlatego dodaje się piąty parametr, abyśmy mogli uzyskać deklarację taniej zmiennej dla grupy (k) na poziomie deklaracji parametru przy użyciu wartości domyślnej. (tak, to jest czary)

  • Jeśli nasza bieżąca grupa nie istnieje w poprzedniej iteracji, tworzymy nową pustą tablicę. ((r[k] || (r[k] = []))Będzie to oceniać do skrajnego lewego wyrażenia, innymi słowy, istniejącą tablicę lub pustą tablicę , dlatego jest to natychmiast pushpo tym wyrażeniu, ponieważ w obu przypadkach otrzymasz tablicę.

  • Gdy jest znak return, ,operator przecinka odrzuci skrajnie lewą wartość, zwracając zmodyfikowaną poprzednią grupę dla tego scenariusza.

Łatwiejsza do zrozumienia wersja, która robi to samo, to:

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

2
czy chciałbyś to trochę wyjaśnić, działa idealnie
Nuwan Dammika

@NuwanDammika - W obu => masz darmowy „powrót” - Funkcja redukcji zajmuje do 4 parametrów. Właśnie dlatego dodaje się piąty parametr, abyśmy mogli uzyskać deklarację taniej zmiennej dla grupy (k). - Jeśli poprzednia wartość nie ma naszej bieżącej grupy, tworzymy nową pustą grupę ((r [k] || (r [k] = [])) Spowoduje to wyrażenie do skrajnego lewego wyrażenia, w przeciwnym razie tablica lub pusta tablica, dlatego po tym wyrażeniu następuje natychmiastowe naciśnięcie. - Gdy nastąpi powrót, operator przecinka odrzuci wartość skrajnie lewą, zwracając
poprawioną

2

Pozwala wygenerować ogólne Array.prototype.groupBy()narzędzie. Dla urozmaicenia zastosujmy fanciness ES6, operator rozprzestrzeniania się, aby dopasować wzór Haskellesa do podejścia rekurencyjnego. Zmodyfikujmy także Array.prototype.groupBy()funkcję zwrotną, która przyjmuje argument ( e) indeks ( i) i zastosowaną tablicę ( 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);


2

Odpowiedź Ceasara jest dobra, ale działa tylko dla wewnętrznych właściwości elementów wewnątrz tablicy (długość w przypadku łańcucha).

ta implementacja działa bardziej jak: ten 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;
    }, {});
};

mam nadzieję że to pomoże...


2

Z @mortb, @jmarceli odpowiedź i z tego postu ,

Korzystam z JSON.stringify()bycia tożsamością dla WARTOŚCI PIERWOTNEJ wielu kolumn grupy według.

Bez strony trzeciej

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

Z firmą zewnętrzną Lodash

const pets = [
    {type:"Dog", age: 3, name:"Spot"},
    {type:"Cat", age: 3, name:"Tiger"},
    {type:"Dog", age: 4, name:"Rover"}, 
    {type:"Cat", age: 3, name:"Leo"}
];

let rslt = _.groupBy(pets, pet => JSON.stringify(
 { type: pet.type, age: pet.age }));

console.log(rslt);

keyGetter zwraca wartość niezdefiniowaną
Asbar Ali

@AsbarAli Przetestowałem mój fragment z konsolą Chrome - wersja 66.0.3359.139 (oficjalna wersja) (64-bit). I wszystko działa dobrze. Czy możesz podać punkt przerwania debugowania i zobaczyć, dlaczego keyGetter jest niezdefiniowany. Być może wynika to z wersji przeglądarki.
Pranithan T.

2

reduceWersja oparta na ES6 z obsługą funkcji iteratee.

Działa zgodnie z oczekiwaniami, jeśli iterateefunkcja nie jest dostępna:

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

W kontekście pytania PO:

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

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

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

Kolejna wersja ES6, która odwraca grupowanie i używa valuesas keysoraz keysas 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))

Uwaga: funkcje są publikowane w krótkiej formie (jedna linia) dla zwięzłości i w odniesieniu do samego pomysłu. Możesz je rozwinąć i dodać dodatkowe sprawdzanie błędów itp.


2

Sprawdziłbym deklaratywne-js groupBy wydaje się robić dokładnie to, czego szukasz. To jest również:

  • bardzo wydajny ( test wydajności )
  • napisane pismem maszynowym, aby wszystkie pisma zostały uwzględnione.
  • Nie wymusza używania obiektów tablicowych innych firm.
import { Reducers } from 'declarative-js';
import groupBy = Reducers.groupBy;
import Map = Reducers.Map;

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

data.reduce(groupBy(element=> element.Step), Map());
data.reduce(groupBy('Step'), Map());

1
let groupbyKeys = function(arr, ...keys) {
  let keysFieldName = keys.join();
  return arr.map(ele => {
    let keysField = {};
    keysField[keysFieldName] = keys.reduce((keyValue, key) => {
      return keyValue + ele[key]
    }, "");
    return Object.assign({}, ele, keysField);
  }).reduce((groups, ele) => {
    (groups[ele[keysFieldName]] = groups[ele[keysFieldName]] || [])
      .push([ele].map(e => {
        if (keys.length > 1) {
          delete e[keysFieldName];
        }
        return e;
    })[0]);
    return groups;
  }, {});
};

console.log(groupbyKeys(array, 'Phase'));
console.log(groupbyKeys(array, 'Phase', 'Step'));
console.log(groupbyKeys(array, 'Phase', 'Step', 'Task'));

1

Oto wersja ES6, która nie zepsuje się na pustych elementach

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

1

Wystarczy dodać do Scotta Sauyet za odpowiedź , niektórzy ludzie pytali w komentarzach jak korzystać z jego funkcji GroupBy wartość1, wartość2, itp, zamiast grupowania tylko jedną wartość.

Wystarczy edytować jego funkcję sumowania:

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

pozostawiając główny (DataGrouper) bez zmian:

var DataGrouper = (function() {
    var has = function(obj, target) {
        return _.any(obj, function(value) {
            return _.isEqual(value, target);
        });
    };

    var keys = function(data, names) {
        return _.reduce(data, function(memo, item) {
            var key = _.pick(item, names);
            if (!has(memo, key)) {
                memo.push(key);
            }
            return memo;
        }, []);
    };

    var group = function(data, names) {
        var stems = keys(data, names);
        return _.map(stems, function(stem) {
            return {
                key: stem,
                vals:_.map(_.where(data, stem), function(item) {
                    return _.omit(item, names);
                })
            };
        });
    };

    group.register = function(name, converter) {
        return group[name] = function(data, names) {
            return _.map(group(data, names), converter);
        };
    };

    return group;
}());

1

Z funkcją sortowania

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

Próba:

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')));
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.