在一个对象数组中进行分组的最有效方法

915

什么是在数组中对对象进行groupby的最有效方法?

例如,给定以下对象数组:

[ 
    { 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 方法将它们“合并”起来。

我想要的是能够对特定值进行汇总(如果被请求的话)。

所以如果我按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,然后循环遍历结果对象自己进行总计?


虽然 _.groupBy 本身不能完成工作,但它可以与其他 Underscore 函数结合使用来完成所需的操作,无需手动循环。请参考此回答: https://dev59.com/uGYq5IYBdhLWcg3wfgnd#66112210。 - Julian
更易读的答案版本:function groupBy(data, key){ return data.reduce( (acc, cur) => { acc[cur[key]] = acc[cur[key]] || []; // 如果键是新的,则将其值初始化为数组,否则保留其自己的数组值 acc[cur[key]].push(cur); return acc; } , []) } - aderchox
62个回答

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

1
在我的特定用例中,我需要按属性分组,然后删除分组属性。该属性仅为分组目的而添加到记录中,对于向用户呈现来说是没有意义的。
    group (arr, key) {

        let prop;

        return arr.reduce(function(rv, x) {
            prop = x[key];
            delete x[key];
            (rv[prop] = (rv[prop] || [])).push(x);
            return rv;
        }, {});

    },

感谢@caesar-bautista提供了排名靠前的函数起始代码。

1

您重复使用现有的JS扩展(如上所述的polyfill)似乎对于特别是不支持其他依赖项中的groupBy的旧JavaScript环境有效。也许您可以在此问题的上下文中说明其用法。这将完善您的答案 :) - hc_dev

1
我已经扩展了被接受的答案,包括按多个属性分组,添加thenby,并使其纯函数且没有变异。在https://stackblitz.com/edit/typescript-ezydzv上查看演示。
export interface Group {
  key: any;
  items: any[];
}

export interface GroupBy {
  keys: string[];
  thenby?: GroupBy;
}

export const groupBy = (array: any[], grouping: GroupBy): Group[] => {
  const keys = grouping.keys;
  const groups = array.reduce((groups, item) => {
    const group = groups.find(g => keys.every(key => item[key] === g.key[key]));
    const data = Object.getOwnPropertyNames(item)
      .filter(prop => !keys.find(key => key === prop))
      .reduce((o, key) => ({ ...o, [key]: item[key] }), {});
    return group
      ? groups.map(g => (g === group ? { ...g, items: [...g.items, data] } : g))
      : [
          ...groups,
          {
            key: keys.reduce((o, key) => ({ ...o, [key]: item[key] }), {}),
            items: [data]
          }
        ];
  }, []);
  return grouping.thenby ? groups.map(g => ({ ...g, items: groupBy(g.items, grouping.thenby) })) : groups;
};

1
function groupBy(array, groupBy){
        return array.reduce((acc,curr,index,array) => {
           var  idx = curr[groupBy]; 
              if(!acc[idx]){
                    acc[idx] = array.filter(item => item[groupBy] === idx)
              } 
            return  acc; 

        },{})
    }

// call
groupBy(items,'Step')

1
let x  = [
  {
    "id": "6",
    "name": "SMD L13",
    "equipmentType": {
      "id": "1",
      "name": "SMD"
    }
  },
  {
    "id": "7",
    "name": "SMD L15",
    "equipmentType": {
      "id": "1",
      "name": "SMD"
    }
  },
  {
    "id": "2",
    "name": "SMD L1",
    "equipmentType": {
      "id": "1",
      "name": "SMD"
    }
  }
];

function groupBy(array, property) {
  return array.reduce((accumulator, current) => {
    const object_property = current[property];
    delete current[property]

    let classified_element = accumulator.find(x => x.id === object_property.id);
    let other_elements = accumulator.filter(x => x.id !== object_property.id);

   if (classified_element) {
     classified_element.children.push(current)
   } else {
     classified_element = {
       ...object_property, 
       'children': [current]
     }
   }
   return [classified_element, ...other_elements];
 }, [])
}

console.log( groupBy(x, 'equipmentType') )

/* output 

[
  {
    "id": "1",
    "name": "SMD",
    "children": [
      {
        "id": "6",
        "name": "SMD L13"
      },
      {
        "id": "7",
        "name": "SMD L15"
      },
      {
        "id": "2",
        "name": "SMD L1"
      }
    ]
  }
]

*/

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
const objsToMerge =    [ 
      { 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" }   ];

finalobj=[];
objsToMerge.forEach(e=>{
  finalobj.find(o => o.Phase === e.Phase)==null?  finalobj.push(e): (      
     finalobj[ finalobj.findIndex(instance => instance.Phase == e.Phase) ].Value= Number(finalobj[ finalobj.findIndex(instance => instance.Phase == e.Phase) ].Value)+ Number(e.Value)
  );
})
    
console.log(finalobj);

返回结果符合预期。

返回

    [
  { Phase: 'Phase 1', Step: 'Step 1', Task: 'Task 1', Value: 50 },
  { Phase: 'Phase 2', Step: 'Step 1', Task: 'Task 1', Value: 130 }
]

我曾经遇到过一个类似的问题,我试图合并一个包含不同组名的对象数组,以标识需要合并的数据。这给了我一个灵感,让我意识到自己之前忽略了什么。 - Ken Ingram
我没有考虑使用过滤器来查找对象,并使用查找功能获取正确的索引以添加数据。 - Ken Ingram

0
var arr = [ 
    { Phase: "Phase 1", `enter code here`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" }
];

创建一个空对象。循环遍历arr并使用Phase作为obj的唯一键添加。在遍历arr时不断更新objkey的总数。
const obj = {};
arr.forEach((item) => {
  obj[item.Phase] = obj[item.Phase] ? obj[item.Phase] + 
  parseInt(item.Value) : parseInt(item.Value);
});

结果将会是这样的:

{ "Phase 1": 50, "Phase 2": 130 }

循环遍历 obj 以形成 resultArr 。
const resultArr = [];
for (item in obj) {
  resultArr.push({ Phase: item, Value: obj[item] });
}
console.log(resultArr);

2
请至少用几句话解释您的解决方案。 - Hexfire
对解决方案进行了优化,并为代码添加了说明。 - Vishal Raut
谢谢你的回答。这个解决方案是我唯一理解的。 - M.Hassan Nasir

0

基于@Ceasar Bautista的创意,我使用typescript修改了代码并创建了一个groupBy函数。

static groupBy(data: any[], comparator: (v1: any, v2: any) => boolean, onDublicate: (uniqueRow: any, dublicateRow: any) => void) {
    return data.reduce(function (reducedRows, currentlyReducedRow) {
      let processedRow = reducedRows.find(searchedRow => comparator(searchedRow, currentlyReducedRow));

      if (processedRow) {
        // currentlyReducedRow is a dublicateRow when processedRow is not null.
        onDublicate(processedRow, currentlyReducedRow)
      } else {
        // currentlyReducedRow is unique and must be pushed in the reducedRows collection.
        reducedRows.push(currentlyReducedRow);
      }

      return reducedRows;
    }, []);
  };

该函数接受一个回调函数(比较器),用于比较行并找到重复项,以及第二个回调函数(onDuplicate),用于聚合重复项。

使用示例:

data = [
    { name: 'a', value: 10 },
    { name: 'a', value: 11 },
    { name: 'a', value: 12 },
    { name: 'b', value: 20 },
    { name: 'b', value: 1 }
  ]

  private static demoComparator = (v1: any, v2: any) => {
    return v1['name'] === v2['name'];
  }

  private static demoOnDublicate = (uniqueRow, dublicateRow) => {
    uniqueRow['value'] += dublicateRow['value'];    
  };

调用

groupBy(data, demoComparator, demoOnDublicate) 

将执行一个分组操作,计算值的总和。

{name: "a", value: 33}
{name: "b", value: 21}

我们可以根据项目需要创建多个回调函数并根据需要汇总价值。例如,在某种情况下,我需要合并两个数组而不是对数据求和。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接