762

配列内のオブジェクトをグループ化する最も効率的な方法は何ですか?

たとえば、次のオブジェクトの配列があるとします。

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

この情報を表に表示しています。さまざまな方法でグループ化したいのですが、値を合計したいです。

groupby 関数に Underscore.js を使用しています。これは役に立ちますが、SQLgroup 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 を使用し、結果のオブジェクトをループして自分で合計を計算する必要がありますか?

4

55 に答える 55

90

MapからES6 をビルドできますarray.reduce()

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

これには、他のソリューションよりもいくつかの利点があります。

  • ライブラリは必要ありません(例とは異なります_.groupBy()
  • MapオブジェクトではなくJavaScript を取得します (たとえば、 によって返され_.groupBy()ます)。これには、次のような多くの利点があります
    • アイテムが最初に追加された順序を記憶し、
    • キーは、文字列だけでなく任意の型にすることができます。
  • AMapは、配列の配列よりも有用な結果です。ただし、配列の配列が必要な場合は、(ペア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()
);
于 2017-12-11T12:11:58.880 に答える
73

私はlodash groupByをチェックします。あなたが探していることを正確に行うようです。また、非常に軽量で非常にシンプルです。

フィドルの例: https://jsfiddle.net/r7szvt5k/

配列名がarrlodash を使用した groupBy である場合、次のようになります。

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
于 2016-01-08T00:06:58.143 に答える
62

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ここでの潜在的な機能は 1 つだけです。好きなように他の人を登録できます:

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それ自体が関数です。データとグループ化するプロパティのリストで呼び出すことができます。要素が 2 つのプロパティを持つオブジェクトである配列を返します。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、ローカル参照が必要な場合にも返されます。

まあ、それは多くの説明です。コードはかなり簡単です。

于 2013-01-23T16:21:41.723 に答える
51

linq.jsこれは、JavaScript での LINQ の真の実装 ( 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();
于 2013-01-21T20:48:17.660 に答える
7
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;
    }, []);
}

これは配列を出力します。

于 2016-08-03T10:55:18.793 に答える
4

すでに書かれたコード (アンダースコアなど) を再利用しながら、元の質問に完全に答えましょう。Underscoreの 100 以上の機能を組み合わせると、さらに多くのことができます。次のソリューションは、これを示しています。

ステップ 1: プロパティの任意の組み合わせによって配列内のオブジェクトをグループ化します。_.groupByこれは、オブジェクトのグループを返す関数を受け入れるという事実を利用しています。_.chain_.pick_.values、も使用_.join_.valueます。_.valueプロパティ名として使用すると、チェーンされた値が自動的にアンラップされるため、ここでは厳密には必要ないことに注意してください。自動アンラップが行われないコンテキストで誰かが同様のコードを記述しようとした場合に備えて、混乱を防ぐためにこれを含めています。

// Given an object, return a string naming the group it belongs to.
function category(obj) {
    return _.chain(obj).pick(propertyNames).values().join(' ').value();
}

// Perform the grouping.
const intermediate = _.groupBy(arrayOfObjects, category);

arrayOfObjectsの質問の を に設定propertyNamesすると['Phase', 'Step']intermediate次の値が得られます。

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

ステップ 2: 各グループを 1 つのフラット オブジェクトに減らし、結果を配列で返します。前に見た関数に加えて、次のコードでは、、、、、および を使用してい_.pluckます。は空のグループを生成しないため、この場合はオブジェクトを返すことが保証されています。この場合は必要です。_.first_.pick_.extend_.reduce_.map_.first_.groupBy_.value

// Sum two numbers, even if they are contained in strings.
const addNumeric = (a, b) => +a + +b;

// Given a `group` of objects, return a flat object with their common
// properties and the sum of the property with name `aggregateProperty`.
function summarize(group) {
    const valuesToSum = _.pluck(group, aggregateProperty);
    return _.chain(group).first().pick(propertyNames).extend({
        [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
    }).value();
}

// Get an array with all the computed aggregates.
const result = _.map(intermediate, summarize);

前に取得した を にintermediate設定aggregatePropertyすると、質問者が希望する が得られます。Valueresult

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

arrayOfObjectsこれをすべて、propertyNamesaggregatePropertyをパラメーターとして受け取る関数にまとめることができます。いずれかを受け入れるarrayOfObjectsため、実際には文字列キーを持つプレーンオブジェクトにすることもできます。_.groupByこのため、 に改名arrayOfObjectsしましたcollection

function aggregate(collection, propertyNames, aggregateProperty) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    const addNumeric = (a, b) => +a + +b;
    function summarize(group) {
        const valuesToSum = _.pluck(group, aggregateProperty);
        return _.chain(group).first().pick(propertyNames).extend({
            [aggregateProperty]: _.reduce(valuesToSum, addNumeric)
        }).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

aggregate(arrayOfObjects, ['Phase', 'Step'], 'Value')再び同じことを私たちに与えるでしょうresult

これをさらに一歩進めて、呼び出し元が各グループの値の統計を計算できるようにすることができます。これを行うことができ、呼び出し元が各グループの概要に任意のプロパティを追加できるようにすることもできます。コードを短くしながら、これらすべてを行うことができます。aggregatePropertyパラメータをパラメータに置き換え、これを次のiterateeように直接渡します_.reduce

function aggregate(collection, propertyNames, iteratee) {
    function category(obj) {
        return _.chain(obj).pick(propertyNames).values().join(' ');
    }
    function summarize(group) {
        return _.chain(group).first().pick(propertyNames)
            .extend(_.reduce(group, iteratee)).value();
    }
    return _.chain(collection).groupBy(category).map(summarize).value();
}

実際には、責任の一部を呼び出し元に移します。iterateeに渡すことができる を提供する必要があります_.reduce。これにより、 への呼び出し_.reduceによって、追加したい集約プロパティを持つオブジェクトが生成されます。たとえばresult、次の式で前と同じものを取得します。

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: +memo.Value + +value.Value
}));

もう少し洗練された の例として、合計ではなく各グループの最大値iterateeを計算し、グループ内で発生するのすべての値をリストするプロパティを追加するとします。上記の (および)の最新バージョンを使用して、これを行う 1 つの方法を次に示します。 ValueTasksTaskaggregate_.union

aggregate(arrayOfObjects, ['Phase', 'Step'], (memo, value) => ({
    Value: Math.max(memo.Value, value.Value),
    Tasks: _.union(memo.Tasks || [memo.Task], [value.Task])
}));

次の結果が得られます。

[
    { Phase: "Phase 1", Step: "Step 1", Value: 10, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 1", Step: "Step 2", Value: 20, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 1", Value: 30, Tasks: [ "Task 1", "Task 2" ] },
    { Phase: "Phase 2", Step: "Step 2", Value: 40, Tasks: [ "Task 1", "Task 2" ] }
]

任意の縮小関数を処理できる回答も投稿した @ much2learn の功績です。複数のアンダースコア関数を組み合わせて洗練されたものを実現する方法を示すSOの回答をさらにいくつか書きました。

于 2021-02-09T02:39:28.603 に答える
2

汎用Array.prototype.groupBy()ツールを生成しましょう。多様性のために、再帰的アプローチで Haskelsque パターン マッチングに ES6 の奇抜な拡散演算子を使用しましょう。また、引数としてアイテム ( ) インデックス ( ) と適用された配列 ( )Array.prototype.groupBy()を取るコールバックを受け入れるようにしましょう。eia

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

于 2017-07-05T20:25:14.063 に答える
1

groupBy機能が JavaScript に導入されます。現在はステージ 3です。

これはトランスパイラー構成で有効にすることができます。ここでは、削減するよりもソリューションがはるかにエレガントであると思います。また、lodash などのサードパーティのライブラリに手を差し伸べることもできます。

const products = [
  { name: 'apples', category: 'fruits' },
  { name: 'oranges', category: 'fruits' },
  { name: 'potatoes', category: 'vegetables' }
];

const groupByCategory = products.groupBy(product => {
  return product.category;
});

console.log(groupByCategory);
// {
//   'fruits': [
//     { name: 'apples', category: 'fruits' }, 
//     { name: 'oranges', category: 'fruits' },
//   ],
//   'vegetables': [
//     { name: 'potatoes', category: 'vegetables' }
//   ]
// }
于 2022-01-26T14:17:06.417 に答える
1

これは、null メンバーで壊れない ES6 バージョンです。

function groupBy (arr, key) {
  return (arr || []).reduce((acc, x = {}) => ({
    ...acc,
    [x[key]]: [...acc[x[key]] || [], x]
  }), {})
}
于 2017-10-28T22:26:57.390 に答える