Group and merge an array of objects

Group by building a Map keyed on the grouping field, then reduce each group into one record. A Map preserves insertion order, which gives first-seen ordering for free, and a Set per group collects unique values before being sorted into an array.

Easy to hard array grouping object immutability

The question

This is the realistic data question: given a flat list of records, produce one row per entity with the numbers totalled and the lists deduplicated. It tests whether you reach for the right structures rather than nesting loops. The follow-ups are always about ordering, both of the groups and of the values inside them.

The solution

A Map of accumulators, then a single pass to convert them into the output shape. The Set is what makes deduplication free.

function mergeData(sessions) {
  const byUser = new Map();

  for (const session of sessions) {
    // Map preserves insertion order, which gives first-seen user order for free.
    if (!byUser.has(session.user)) {
      byUser.set(session.user, { user: session.user, duration: 0, equipment: new Set() });
    }
    const merged = byUser.get(session.user);
    merged.duration += session.duration;
    for (const item of session.equipment) merged.equipment.add(item);
  }

  return [...byUser.values()].map((merged) => ({
    user: merged.user,
    duration: merged.duration,
    equipment: [...merged.equipment].sort(),
  }));
}

How it works

The tests it has to pass

This is the part most explanations leave out. "It works" is not a claim you can check by reading. These are the 4 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy2 cases · grouping and totals

combines each user, keeping users in first-seen order

const merged = mergeData([
  { user: 2, duration: 30, equipment: ["bike"] },
  { user: 1, duration: 20, equipment: ["bench"] },
  { user: 2, duration: 15, equipment: ["barbell", "bike"] },
]);
expectEqual(merged.map((session) => session.user), [2, 1]);
expectEqual(merged[0].duration, 45);

totals duration and collects unique equipment for a user

const merged = mergeData([
  { user: 4, duration: 20, equipment: ["bike"] },
  { user: 4, duration: 25, equipment: ["bike", "bench"] },
]);
expectEqual(merged.length, 1);
expectEqual(merged[0].duration, 45);
expectEqual([...merged[0].equipment].sort(), ["bench", "bike"]);
Hard2 cases · ordering and full merges

returns unique equipment in alphabetical order

const merged = mergeData([
  { user: 3, duration: 10, equipment: [] },
  { user: 3, duration: 5, equipment: ["barbell"] },
  { user: 3, duration: 15, equipment: ["barbell", "ab-roller"] },
]);
expectEqual(merged, [{ user: 3, duration: 30, equipment: ["ab-roller", "barbell"] }]);

merges every visit for a user across the whole list

const merged = mergeData([
  { user: 1, duration: 10, equipment: ["bike"] },
  { user: 2, duration: 5, equipment: ["rope"] },
  { user: 1, duration: 20, equipment: ["bench"] },
  { user: 2, duration: 15, equipment: ["rope", "bike"] },
  { user: 1, duration: 30, equipment: ["bike", "ab-roller"] },
]);
expectEqual(merged, [
  { user: 1, duration: 60, equipment: ["ab-roller", "bench", "bike"] },
  { user: 2, duration: 20, equipment: ["bike", "rope"] },
]);

Edge cases to watch out for

Using a plain object for the groups

Object keys that look like integers are enumerated in ascending numeric order regardless of insertion, so grouping by a numeric id silently re-sorts your output. A Map keeps insertion order for every key type.

Deduplicating with includes inside a loop

It works and is quadratic. A Set makes membership constant time and expresses the intent directly.

Mutating the input records

Pushing into session.equipment to accumulate modifies the caller's data. Build fresh accumulators instead.

Follow up questions

How would you group by more than one field?

Build a composite key and use that as the Map key: `${row.user}|${row.date}`, with a delimiter that can’t appear in the values. If it can, use a Map of Maps instead, or a stable serialisation of the key fields. Composite string keys are simpler and fine until a value contains your delimiter.

Why use a Map instead of a plain object for grouping?

Two reasons. Integer-like keys on a plain object are enumerated in ascending numeric order, so your group order is silently changed. And a key like "constructor" collides with an inherited property. A Map has neither problem.

What is Object.groupBy?

A newer built-in that groups an array by the result of a callback, returning an object of arrays. It handles the grouping half of this problem in one call, and Map.groupBy returns a Map instead. You still have to merge each group yourself.

How do you collect unique values while grouping?

Accumulate into a Set rather than an array, then spread it into an array at the end. Membership is constant time, duplicates are impossible by construction, and you can sort the result if the output order matters.

Practise this with the tests running

Practice Pad runs these 4 cases against your own solution, in a real editor, on your Mac. Twenty-four questions, and an optional AI panel that explains why a test failed.

Download for macOS Free. Requires macOS 12 or later on Apple Silicon.

See all 24 questions