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.
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
Mappreserves insertion order, so groups come out in first-seen order with no sorting and no separate index to track.- A
Setper group deduplicates as you go, which is why noincludescheck is needed inside the loop. - Accumulators are built, then converted. Keeping the Set internal and turning it into a sorted array at the end keeps the hot loop cheap.
- Nothing is mutated. New objects are created throughout, so the input array and its records are untouched.
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.
✓ 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"]);
✓ 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.