网站搜索

javascript - 按键值递归分组(组中组)


我想按多个键进行分组,以便像这样的调用:

groupBy(winners, ['country', 'athlete'])

关于以下数据:

[
  { athlete: "Michael Phelps", age: 19, country: "United States" },
  { athlete: "Michael Phelps", age: 27, country: "United States" },
  { athlete: "Kirsty Coventry", age: 24, country: "Zimbabwe" },
  { athlete: "Allison Schmitt", age: 22, country: "United States" },
]

将产生(按键的顺序嵌套):

{
  'United States': {
    'Michael Phelps': [
      { athlete: "Michael Phelps", age: 19, country: "United States" },
      { athlete: "Michael Phelps", age: 27, country: "United States" }
    ],
    'Allison Schmitt': [
      { athlete: "Allison Schmitt", age: 22 country: "United States" }
    ]
  },
  'Zimbabwe': {
     'Kirsty Coventry': [
       { athlete: "Kirsty Coventry", age: 24, country: "Zimbabwe" }
     ]
  }
}

按一个键分组很容易,但我卡在如何让它递归地将每个分组按下一个键继续分组上。这只是把每个键都分组在同一层级:

const get = (obj: Record<string, any>, k: string) =>
  k.split(".").reduce((o, i) => (o ? o[i] : o), obj);

type GetValue<Item> = (item: Item) => string;

function groupBy<Item>(items: Item[], keys: (string | GetValue<Item>)[]) {
  return keys.reduce(
    (acc, key) => {
      return items.reduce((accc, item) => {
        const value =
          typeof key === "function" ? key(item) : get(item, key);
          (accc[value] = accc[value] || []).push(item);
        return accc;
      }, acc);
    },
    {} as Record<string, any>
  );
}

const r = groupBy<Athlete>(winners.slice(0, 50), [
  athlete => athlete.country,
  "athlete"
]);

这是一个可运行的示例:https://codesandbox.io/s/groupby-ebgly?file=/src/index.ts:244-929

抱歉,额外的复杂性在于使其更容易通过点符号或函数来指定键,以处理更复杂的情况,例如数组中的某个值。

谢谢

答案#1

你可以缩减数组并减少键,然后为最后一个键取一个数组来推入对象。

const
    groupBy = (array, keys) => array.reduce((r, o) => {
        keys
            .reduce((q, k, i, { length }) => q[o[k]] = q[o[k]] || (i + 1 === length ? [] : {}), r)
            .push(o);
        return r;
    }, {}),
    winners = [{ athlete: "Michael Phelps", age: 19, country: "United States" }, { athlete: "Michael Phelps", age: 27, country: "United States" }, { athlete: "Kirsty Coventry", age: 24, country: "Zimbabwe" }, { athlete: "Allison Schmitt", age: 22, country: "United States" }];
    
console.log(groupBy(winners, ['country', 'athlete']));
.as-console-wrapper { max-height: 100% !important; top: 0; }

答案#2

你可以通过在每一步减少键来对 groupBy 进行递归。

像这样的东西:

const winners = [{
    athlete: "Michael Phelps",
    age: 19,
    country: "United States"
  },
  {
    athlete: "Michael Phelps",
    age: 27,
    country: "United States"
  },
  {
    athlete: "Kirsty Coventry",
    age: 24,
    country: "Zimbabwe"
  },
  {
    athlete: "Allison Schmitt",
    age: 22,
    country: "United States"
  },
]


const get = (obj, k) => k.split(".").reduce((o, i) => (o ? o[i] : o), obj);

function groupBy(items, keys) {

  const key = keys[0]

  const res = items.reduce((accc, item) => {
    const k =
      typeof key === "function" ? key(item) : get(item, key);
    if (typeof k === "string") {
      (accc[k] = accc[k] || []).push(item);
    }
    return accc;
  }, {});

  if (keys.length - 1 > 0)
    return Object.fromEntries(Object.entries(res).map(([key, val]) => [key, groupBy(val, keys.slice(1))]))
  else
    return res // recursion base

}

const r = groupBy(winners, ['country', 'athlete'])

console.log(r)