[javascript] 객체 배열을 그룹화하는 가장 효율적인 방법

배열에서 객체를 그룹화하는 가장 효율적인 방법은 무엇입니까?

예를 들어,이 객체 배열이 주어진 경우 :

[
    { 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를 groupby 함수에 사용하고 있습니다. 이는 도움이되지만 SQL group by메서드 와 같이 “분할”하지만 “병합”되기를 원하지 않기 때문에 모든 트릭을 수행하지는 않습니다 .

내가 찾고있는 것은 특정 값을 합계 할 수있을 것입니다 (요청한 경우).

따라서 groupby를 수행 Phase하면 다음을 수신하고 싶습니다.

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

그리고 groupy 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를 사용하고 결과 객체를 반복하여 총계를 직접 수행해야합니까?



답변

외부 라이브러리를 피하려면 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"]}


답변

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


답변

ES6 사용시 :

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


답변

있지만 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에서 실제로 볼 수 있습니다 .

Underscore에서 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제공 한 이름 에 따라 속성으로 저장되며 로컬 참조를 원할 경우 반환됩니다.

글쎄요, 그것은 많은 설명입니다. 코드는 매우 간단합니다.


답변

lodash 그룹 을 확인 하여 원하는 것을 정확하게 수행하는 것 같습니다. 또한 매우 가볍고 간단합니다.

바이올린 예 : https://jsfiddle.net/r7szvt5k/

배열 이름이 arrgroupBy이면 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


답변

이것은 아마도 더 쉽게 이루어집니다 linq.js자바 스크립트에서 LINQ의 진정한 구현 (의도 된, 데모 ) :

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


답변

에서 ES6 Map을 빌드 할 수 있습니다 array.reduce().

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

이것은 다른 솔루션보다 몇 가지 장점이 있습니다.

  • 라이브러리가 필요하지 않습니다 (예 : _.groupBy() )
  • Map객체가 아닌 자바 스크립트를받습니다 (예 :에서 반환 _.groupBy()). 이것은 많은 이점이 있습니다 .
    • 항목이 처음 추가 된 순서를 기억합니다.
    • 키는 문자열이 아닌 모든 유형이 될 수 있습니다.
  • A 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()
);