Найефективніший метод групування по масиву об'єктів


507

Який найбільш ефективний спосіб групувати об’єкти в масиві?

Наприклад, з урахуванням цього масиву об'єктів:

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

Я відображаю цю інформацію в таблиці. Я хотів би згрупувати різні методи, але я хочу підсумувати значення.

Я використовую Underscore.js для його групової функції, яка є корисною, але не робить цілої хитрості, тому що я не хочу, щоб вони «розкололися», а «зрослися», більше як group byметод SQL .

Те, що я шукаю, зможе підрахувати конкретні значення (за запитом).

Тож якби я зробив групу Phase, я хотів би отримати:

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

І якби я зробив групу Phase/ Step, я отримав би:

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

Чи є корисний скрипт для цього, або я повинен дотримуватися використання Underscore.js, а потім перебирати отриманий об'єкт, щоб зробити підсумки самостійно?

Відповіді:


755

Якщо ви хочете уникати зовнішніх бібліотек, ви можете коротко реалізувати ванільну версію groupBy()подібного типу:

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
я б змінив таким чином: `` `return xs.reduce (функція (rv, x) {var v = key instanceof Функція? key (x): x [key]; (rv [v] = rv [v] || []). push (x); повернути rv;}, {}); ``, що дозволяє функціям зворотного виклику повертати критерії сортування
y_nk

110
Ось такий, який виводить масив, а не об’єкт: groupByArray (xs, ключ) {return xs.reduce (функція (rv, x) {нехай v = ключовий примірник функції? Ключ (x): x [ключ]; нехай el = rv .find ((r) => r && r.key === v); if (el) {el.values.push (x);} else {rv.push ({ключ: v, значення: [x] });} повернути rv;}, []); }
tomitrescak

24
Чудово, просто те, що мені було потрібно. Якщо хтось потребує цього, ось підпис TypeScript:var groupBy = function<TItem>(xs: TItem[], key: string) : {[key: string]: TItem[]} { ...
Майкл Сандіно

4
Для чого це варто, рішення tomitrescak, хоча і зручно, є значно менш ефективним, оскільки find (), ймовірно, O (n). Рішення у відповіді - O (n), від зменшення (призначення об'єкта O (1), як і натискання), тоді як коментар - O (n) * O (n) або O (n ^ 2) або в щонайменше O (nlgn)
narthur157

21
Якщо когось цікавить, я зробив більш читану та помічену версію цієї функції і вклав її в суть: gist.github.com/robmathers/1830ce09695f759bf2c4df15c29dd22d, я вважаю корисним зрозуміти, що насправді відбувається тут.
robmathers

228

Використання об’єкта ES6 Map:

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

Про карту: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map


@mortb, як це отримати, не викликаючи get()метод? Я хочу, щоб результат відображався без передачі ключа
Fai Zal Dong,

@FaiZalDong: Я не впевнений, що було б найкраще для вашої справи? Якщо я пишу console.log(grouped.entries());в прикладі jsfiddle, він повертає ітерабельний режим, який веде себе як масив ключів + значень. Чи можете ви спробувати це і побачити, чи допомагає це?
mortb

7
Ви також можете спробуватиconsole.log(Array.from(grouped));
mortb

Мені подобається ця відповідь, дуже гнучка
benshabatnoam

щоб побачити кількість елементів у групах:Array.from(groupBy(jsonObj, item => i.type)).map(i => ( {[i[0]]: i[1].length} ))
Ahmet Şimşek

105

з ES6:

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

3
Потрібно трохи звикнути, але так само роблять і більшість шаблонів C ++
Леві Хаскелл

2
Я закрутив мізки і досі не зрозумів, як у світі це працює, починаючи з цього ...result. Зараз я не можу спати через це.

8
Елегантний, але болісно повільний на більших масивах!
infinity1975


1
просте рішення. Спасибі
Езекіль Таварес

59

Хоча відповідь linq цікава, вона також досить вагома . Мій підхід дещо інший:

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

Ви можете бачити це в дії на JSBin .

У підкресленні я нічого не бачив, що робить те, що hasробить, хоча я, можливо, цього не пропускаю. Це майже те саме, що _.contains, але використовує, _.isEqualа не ===для порівнянь. Крім усього іншого, все інше є специфічним для проблеми, хоча і намагається бути загальним.

Тепер DataGrouper.sum(data, ["Phase"])повертається

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

І DataGrouper.sum(data, ["Phase", "Step"])повертається

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

Але тут sumє лише одна потенційна функція. Ви можете зареєструвати інших, як вам подобається:

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

і тепер DataGrouper.max(data, ["Phase", "Step"])повернеться

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

або якщо ви зареєстрували це:

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

то дзвінок DataGrouper.tasks(data, ["Phase", "Step"])отримає вас

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

DataGrouperсама по собі є функцією. Ви можете зателефонувати йому зі своїми даними та списком властивостей, за якими хочете згрупуватися. Він повертає масив, елементи якого є об'єктом з двома властивостями: keyце колекція згрупованих властивостей, valsце масив об'єктів, що містять решта властивостей, які не знаходяться в ключі. Наприклад, DataGrouper(data, ["Phase", "Step"])вийде:

[
    {
        "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.registerприймає функцію та створює нову функцію, яка приймає початкові дані та властивості до групи. Потім ця нова функція приймає вихідний формат, як описано вище, і запускає вашу функцію проти кожного з них по черзі, повертаючи новий масив. Згенерована функція зберігається як властивість DataGrouperвідповідно до імені, яке ви надаєте, а також повертається, якщо ви просто хочете локальну посилання.

Ну це багато пояснень. Код досить зрозумілий, сподіваюся!


Привіт .. Чи можу я бачити вас за групою та підсумовувати лише за значенням, але у випадку, якщо я хочу суму за value1 та valu2 та value3 ... у вас є рішення?
SAMUEL OSPINA

@SAMUELOSPINA Ви коли-небудь знаходили спосіб це зробити?
howMuchCheeseIsTooMuchCheese

50

Я би перевірив групу квартир, але, здається, робиш саме те, що ти шукаєш. Він також досить легкий і справді простий.

Приклад скрипки: https://jsfiddle.net/r7szvt5k/

За умови, що ваше ім'я масиву є arrгрупоюBy з lodash є просто:

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
Чи не є ця відповідь проблематичною? Існує кілька способів, коли результат lodash _.groupBy не у форматі результату, який запитує ОП. (1) Результат - це не масив. (2) "Значення" стало "ключем" у результаті об'єктів (лодашів).
mg1075

44

Це, мабуть, простіше зробити linq.js, що має бути справжньою реалізацією LINQ в 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();

результат:

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

Або, простіше, використовуючи селекторні селектори ( DEMO ):

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

Чи можемо ми використати кілька властивостей під час групування тут:GroupBy(function(x){ return x.Phase; })
Amit

37

Ви можете створити ES6 Mapз array.reduce().

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

Це має ряд переваг перед іншими рішеннями:

  • Тут не потрібні бібліотеки (на відміну від наприклад _.groupBy())
  • Ви отримуєте JavaScript, Mapа не об’єкт (наприклад, як повернуто _.groupBy()). Це має багато переваг , зокрема:
    • він пам'ятає порядок, коли вперше були додані елементи,
    • ключі можуть бути будь-якого типу, а не просто рядки.
  • Більш Mapкорисним результатом є масив масивів. Але якщо ви хочете масив масивів, ви можете зателефонувати Array.from(groupedMap.entries())(для масиву [key, group array]пар) або Array.from(groupedMap.values())(для простого масиву масивів).
  • Це досить гнучко; часто все, що ви планували робити далі з цією картою, можна зробити безпосередньо в рамках скорочення.

Як приклад останньої точки, уявіть, що у мене є масив об'єктів, для яких я хочу зробити (дрібне) злиття за допомогою id, наприклад:

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

Для цього я б зазвичай починав з групування за id, а потім об'єднання кожного з отриманих масивів. Натомість ви можете зробити злиття безпосередньо в reduce():

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

1
Я не знаю, чому це не має більше голосів. Це лаконічно, читається (на мене) і виглядає ефективно. Він не летить на IE11 , але оновлення не надто важке ( a.reduce(function(em, e){em.set(e.id, (em.get(e.id)||[]).concat([e]));return em;}, new Map())приблизно)
відключити


18

Ви можете зробити це за допомогою бібліотеки 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]);

Спробуйте цей приклад на jsFiddle .

BTW: На великих масивах (100000 записів і більше) Alasql швидше, ніж Linq. Дивіться тест на jsPref .

Коментарі:

  • Тут я ставлю значення у квадратні дужки, оскільки VALUE - це ключове слово у SQL
  • Мені потрібно використовувати функцію CAST () для перетворення рядкових значень у тип номера.

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 має такий приклад у своїй Array.reduce()документації.

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

Хоча на запитання є кілька відповідей, і відповіді виглядають надто складними, я пропоную використовувати ванільний Javascript для групи з вкладеними (якщо потрібно) 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

Без мутацій:

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

Це рішення приймає будь-яку довільну функцію (не ключову), тому воно є більш гнучким, ніж рішення, наведені вище, і дозволяє стрілочним функціям , подібним до лямбда-виразів, що використовуються в LINQ :

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

ПРИМІТКА: чи ви хочете розширити Arrayпрототип протоколу, залежить від вас.

Приклад підтримується в більшості браузерів:

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

Приклад за допомогою функцій стрілок (ES6):

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

Обидва приклади вище повертаються:

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

Мені дуже сподобалось рішення ES6. Лише невелике посилення без розширення прототипу Array:let key = 'myKey'; let newGroupedArray = myArrayOfObjects.reduce(function (acc, val) { (acc[val[key]] = acc[val[key]] || []).push(val); return acc;});
caneta

8

Я хотів би запропонувати свій підхід. По-перше, окреме групування та агрегування. Дозволяє оголошувати прототипну функцію "група за". Потрібна інша функція, щоб створити "хеш" рядок для кожного елемента масиву, згрупувати по.

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

коли групування виконано, ви можете зібрати дані, як вам потрібно, у вашому випадку

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

спільно це працює. Я перевірив цей код на хромованій консолі. і сміливо вдосконалюйтесь та знаходите помилки;)


Дякую ! Люблю підхід і ідеально відповідає моїм потребам (мені не потрібно агрегувати).
абера

Я думаю, ви хочете змінити рядок на put (): map[_hash(key)].key = key;to map[_hash(key)].key = _hash(key);.
Scotty.NET

6

Уявіть, що у вас є щось подібне:

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

Роблячи це: const categories = [...new Set(cars.map((car) => car.cat))]

Ви отримаєте це: ['sedan','sport']

Пояснення: 1. По-перше, ми створюємо новий набір, передаючи масив. Оскільки Set встановлює лише унікальні значення, всі дублікати будуть видалені.

  1. Тепер дублікатів уже немає, ми збираємося перетворити його назад у масив, використовуючи оператор розповсюдження ...

Установіть 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 / Довідка / Оператори / Spread_syntax


мені дуже подобається ваша відповідь, це найкоротший, але я все ще не розумію логіки, особливо, хто тут робить групування? це оператор розповсюдження (...)? чи "новий комплект ()"? поясніть, будь ласка, нам ... дякую
Іван

1
1. По-перше, ми створюємо новий набір, передаючи масив. Оскільки Set встановлює лише унікальні значення, всі дублікати будуть видалені. 2. Тепер дублікати пішли, ми перетворимо їх назад у масив, використовуючи оператор розповсюдження ... Установіть Doc: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… Spread Оператор: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
Яго Герес

ок, зрозумів ... дякую у за пояснення, сер :)
Іван

Ласкаво просимо!
Яго Герес

6

Перевірена відповідь - просто неглибоке групування. Дуже приємно розуміти скорочення. Питання також передбачає проблему додаткових сукупних обчислень.

Ось РЕАЛЬНА ГРУПА для масиву об’єктів за деякими полями з 1) обчисленою назвою ключа та 2) повним рішенням для каскадування групування шляхом надання списку потрібних ключів та перетворення його унікальних значень у кореневі ключі, такі як SQL GROUP BY робить.

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

Грайте з estKey- ви можете згрупувати більше кількох полів, додати додаткові агрегації, обчислення чи іншу обробку.

Також ви можете групувати дані рекурсивно. Наприклад, спочатку групуйте по Phase, потім по Stepполям тощо. Додатково здуйте дані про відпочинок жиру.

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

Цей виводить масив.


4

На основі попередніх відповідей

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

і трохи приємніше дивитися на синтаксис поширення об’єктів, якщо ваше середовище підтримує.

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

Тут наш редуктор приймає частково сформоване значення повернення (починаючи з порожнього об'єкта) і повертає об'єкт, що складається з розкладених членів попереднього значення повернення, разом з новим членом, ключ якого обчислюється від поточного значення ітерації при propі значення якого - це список усіх значень для цієї опори разом із поточним значенням.


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

Ось неприємне важке для читання рішення за допомогою ES6:

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

Для тих , хто запитує , як робить це навіть роботу, ось пояснення:

  • В обох у =>вас є вільнийreturn

  • Array.prototype.reduceФункція приймає до 4 -х параметрів. Ось чому додається п'ятий параметр, щоб ми могли мати дешеве оголошення змінної для групи (k) на рівні оголошення параметра, використовуючи значення за замовчуванням. (так, це чаклунство)

  • Якщо нашої попередньої групи не існує попередньої ітерації, ми створюємо новий порожній масив. ((r[k] || (r[k] = []))Це буде оцінюватися до крайнього лівого виразу, іншими словами, існуючого масиву або порожнього масиву , тому pushпісля цього виразу є негайне , оскільки в будь-якому випадку ви отримаєте масив.

  • Коли з'явиться a return, ,оператор з комою відкине найменше значення, повертаючи попередньо підгрупувану групу для цього сценарію.

Простіша для розуміння версія, яка робить те саме:

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

2
Ви хочете трохи пояснити це, воно працює ідеально
Nuwan Dammika

@NuwanDammika - В обох => у вас вільний "повернення" - функція зменшення займає до 4 параметрів. Ось чому додається п'ятий параметр, щоб ми могли мати дешеве оголошення змінної для групи (k). - Якщо в попередньому значенні немає нашої поточної групи, ми створюємо нову порожню групу ((r [k] || (r [k] = [])) Це буде оцінюватися до виразу зліва, інакше масив або порожній масив, тому після цього виразу відбувається негайне поштовх. - Коли буде повернення, оператор комами
відкине найменше

2

Дозволяє генерувати загальний Array.prototype.groupBy()інструмент. Для розмаїття використовуємо ES6-фантастичний оператор розповсюдження для деякої відповідності шаблону Haskellesque на рекурсивному підході. Також давайте змусимо Array.prototype.groupBy()прийняти зворотний виклик, який бере в якості аргументів елемент ( e) індекс ( i) та застосований масив ( 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

Відповідь Цезара хороша, але працює лише для внутрішніх властивостей елементів всередині масиву (довжина у разі рядка).

ця реалізація працює більше як: це посилання

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

сподіваюся, що це допоможе ...


2

Від @mortb, @jmarceli відповіді та з цього повідомлення ,

Я використовую перевагу, JSON.stringify()щоб бути тотожністю для ПРИМІТИВНОЇ ЗНАЧЕННЯ кількох стовпців групи.

Без сторонніх

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

З Лодашем стороннім

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 повертається невизначеним
Асбар Алі

@AsbarAli Я випробував свій фрагмент на консолі Chrome - Версія 66.0.3359.139 (Офіційна збірка) (64-розрядна). І все працює нормально. Ви можете, будь ласка, поставити точку перерви налагодження і побачити, чому keyGetter не визначений. Це, мабуть, через версію браузера.
Пранітан Т.

2

Версія reduce на базі ES6 з підтримкою функції iteratee.

Працює так, як очікувалося, якщо iterateeфункція не передбачена:

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

У контексті питання про ОП:

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

Ще одна версія ES6, яка повертає групування і використовує valuesяк keysі keysяк 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))

Примітка: функції розміщуються в їх короткій формі (в одному рядку) для стислості та співвіднесення лише ідеї. Ви можете розширити їх та додати додаткову перевірку помилок тощо.


2

Я перевірив би декларативне-js, groupBy здається, робить саме те, що ви шукаєте. Це також:

  • дуже продуктивні (продуктивність тести )
  • написано машинописним шрифтом, щоб усі машинописи були включені.
  • Це не вимагає використання сторонніх об’єктів, схожих на масив.
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

Ось версія ES6, яка не зламається на нульових членах

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

1

Просто , щоб додати до Скотт Sauyet в відповідь , деякі люди питали в коментарях , як використовувати свою функцію GroupBy значенію1, значення2 і т.д., замість того , щоб групувати тільки одне значення.

Все, що потрібно - це відредагувати його суму:

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

залишаючи основним (DataGrouper) без змін:

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

З функцією сортування

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

Зразок:

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')));
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.