如何将数组按两个属性分组?

时间:2017-03-30 18:32:30

标签: javascript arrays ecmascript-6

实施例

const arr = [{
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 2,
  question: {
    templateId: 200
  }
}, {
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 1,
  question: {
    templateId: 300
  }
}];

预期结果 const result = groupBy(arr, 'group', 'question.templateId');

const result = [
  [{
    group: 1,
    question: {
      templateId: 100
    }
  }, {
    group: 1,
    question: {
      templateId: 100
    }
  }],
  [{
    group: 1,
    question: {
      templateId: 300
    }
  }],
  [{
    group: 2,
    question: {
      templateId: 200
    }
  }]
];

到目前为止:我可以使用Array.prototype.reduce()将结果分组到一个属性。

function groupBy(arr, key) {
  return [...arr.reduce((accumulator, currentValue) => {
    const propVal = currentValue[key],
      group = accumulator.get(propVal) || [];
    group.push(currentValue);
    return accumulator.set(propVal, group);
  }, new Map()).values()];
}

const arr = [{
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 2,
  question: {
    templateId: 200
  }
}, {
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 1,
  question: {
    templateId: 300
  }
}];

const result = groupBy(arr, 'group');

console.log(result);

3 个答案:

答案 0 :(得分:4)

我建议传递回调函数而不是属性名称,这样可以轻松地进行两级访问:

function groupBy(arr, key) {
  return Array.from(arr.reduce((accumulator, currentValue) => {
    const propVal = key(currentValue),
//                  ^^^^            ^
          group = accumulator.get(propVal) || [];
    group.push(currentValue);
    return accumulator.set(propVal, group);
  }, new Map()).values());
}

现在您可以groupBy(arr, o => o.group)groupBy(arr, o => o.question.templateId)

要获得预期结果,您需要做的就是按第一个属性分组,然后将每个结果分组到第二个属性:

function concatMap(arr, fn) {
  return [].concat(...arr.map(fn));
}
const result = concatMap(groupBy(arr, o => o.group), res =>
  groupBy(res, o => o.question.templateId)
);

答案 1 :(得分:4)

@ Bergi的回答非常实用,但我会告诉你如何构建一个多值的关键词#34;可以使用JavaScript原语 - 不要认为这意味着Bergi的答案总是;事实上,它实际上更好因为它的实用性。如果有的话,这个答案的存在是为了向你展示通过使用像他这样的方法节省了多少工作。

我会逐位检查代码,然后我会在最后有一个完整的可运行演示。

复合数据平等

比较JavaScript中的复合数据有点棘手,因此我们需要首先找到解决方法:



console.log([1,2] === [1,2]) // false




我想要涵盖多值键的 解决方案,因为我们的整个答案都将基于它 - 我在这里称之为CollationKey。我们的键包含一些值,并定义了自己的相等函数,用于比较键



const CollationKey = eq => x => ({
  x,
  eq: ({x: y}) => eq(x, y)
})

const myKey = CollationKey (([x1, x2], [y1, y2]) =>
  x1 === y1 && x2 === y2)

const k1 = myKey([1, 2])
const k2 = myKey([1, 2])
console.log(k1.eq(k2)) // true
console.log(k2.eq(k1)) // true

const k3 = myKey([3, 4])
console.log(k1.eq(k3)) // false




一厢情愿

现在我们有了比较复合数据的方法,我想创建一个自定义缩减函数,它使用我们的多值键来对值进行分组。我将此函数称为collateBy

// key = some function that makes our key
// reducer = some function that does our reducing
// xs = some input array
const collateBy = key => reducer => xs => {
  // ...?
}

// our custom key;
// equality comparison of `group` and `question.templateId` properties
const myKey = CollationKey ((x, y) =>
  x.group === y.group
    && x.question.templateId === y.question.templateId)

const result =
  collateBy (myKey) // multi-value key
            ((group=[], x) => [...group, x]) // reducing function: (accumulator, elem)
            (arr) // input array

现在我们知道我们希望collateBy如何工作,让我们实现它

const collateBy = key => reducer => xs => {
  return xs.reduce((acc, x) => {
    const k = key(x)
    return acc.set(k, reducer(acc.get(k), x))
  }, Collation())
}

整理数据容器

好的,我们在那里使用Collation()作为xs.reduce调用的起始值也有点乐观。 Collation应该是什么?

我们所知道的:

  • someCollation.set接受CollationKey和某个值,并返回新的Collation
  • someCollation.get接受CollationKey并返回一些值

好吧,让我们开始工作吧!

const Collation = (pairs=[]) => ({
  has (key) {
    return pairs.some(([k, v]) => key.eq(k))
  },
  get (key) {
    return (([k, v]=[]) => v)(
      pairs.find(([k, v]) => k.eq(key))
    )
  },
  set (key, value) {
    return this.has(key)
      ? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
      : Collation([...pairs, [key, value]])
  },
})

完成

到目前为止,我们的collateBy函数返回一个Collation数据容器,该容器在内部使用[key, value]对数组实现,但我们真正想要的(根据您的问题)只是一组值

让我们以最轻微的方式修改collateBy - 提取值粗体

const collateBy = key => reducer => xs => {
  return xs.reduce((acc, x) => {
    let k = key(x)
    return acc.set(k, reducer(acc.get(k), x))
  }, Collation()).values()
}

现在我们将values方法添加到Collation容器

values () {
  return pairs.map(([k, v]) => v)
}

可运行的演示

这就是所有内容,所以让我们现在看到一切正常 - 我在输出中使用JSON.stringify,以便深层嵌套的对象显示所有内容



// data containers
const CollationKey = eq => x => ({
  x,
  eq: ({x: y}) => eq(x, y)
})

const Collation = (pairs=[]) => ({
  has (key) {
    return pairs.some(([k, v]) => key.eq(k))
  },
  get (key) {
    return (([k, v]=[]) => v)(
      pairs.find(([k, v]) => k.eq(key))
    )
  },
  set (key, value) {
    return this.has(key)
      ? Collation(pairs.map(([k, v]) => k.eq(key) ? [key, value] : [k, v]))
      : Collation([...pairs, [key, value]])
  },
  values () {
    return pairs.map(([k, v]) => v)
  }
})

// collateBy
const collateBy = key => reducer => xs => {
  return xs.reduce((acc, x) => {
    const k = key(x)
    return acc.set(k, reducer(acc.get(k), x))
  }, Collation()).values()
}

// custom key used for your specific collation
const myKey =
  CollationKey ((x, y) =>
    x.group === y.group
      && x.question.templateId === y.question.templateId)

// your data
const arr = [ { group: 1, question: { templateId: 100 } }, { group: 2, question: { templateId: 200 } }, { group: 1, question: { templateId: 100 } }, { group: 1, question: { templateId: 300 } } ]

// your answer
const result =
  collateBy (myKey) ((group=[], x) => [...group, x]) (arr)

console.log(result)
// [
//   [
//     {group:1,question:{templateId:100}},
//     {group:1,question:{templateId:100}}
//   ],
//   [
//     {group:2,question:{templateId:200}}
//   ],
//   [
//     {group:1,question:{templateId:300}}
//   ]
// ]




<强>摘要

我们制作了一个自定义整理功能,它使用多值键对我们的整理值进行分组。这是使用JavaScript原语和高阶函数完成的。我们现在有办法通过数据集进行迭代,并使用任意复杂的键以任意方式对其进行整理。

如果您对此有任何疑问,我很乐意回答他们^ _ ^

答案 2 :(得分:1)

如果您可以对输入进行硬编码,那么@ Bergi的回答非常好。

如果您想使用字符串输入,可以使用sort()方法,并根据需要遍历对象。

此解决方案将处理任意数量的参数:

&#13;
&#13;
function groupBy(arr) {
  var arg = arguments;
  
  return arr.sort((a, b) => {
    var i, key, aval, bval;
    
    for(i = 1 ; i < arguments.length ; i++) {
      key = arguments[i].split('.');
      aval = a[key[0]];
      bval = b[key[0]];
      key.shift();
      while(key.length) {  //walk the objects
        aval = aval[key[0]];
        bval = bval[key[0]];
        key.shift();
      };
      if     (aval < bval) return -1;
      else if(aval > bval) return  1;
    }
    return 0;
  });
}

const arr = [{
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 2,
  question: {
    templateId: 200
  }
}, {
  group: 1,
  question: {
    templateId: 100
  }
}, {
  group: 1,
  question: {
    templateId: 300
  }
}];

const result = groupBy(arr, 'group', 'question.templateId');

console.log(result);
&#13;
&#13;
&#13;