Filter an array of objects

Apply each supplied option as its own filter, one after another, skipping any that were not provided. The design decision that matters is order: when records are merged before filtering, the filters see combined totals, which produces a materially different result from filtering first.

Easy to hard array filtering objects

The question

The open-ended one. There’s no single correct answer, which is the point: the interviewer wants to see how you structure code when the requirements arrive as a bag of optional flags. The question that earns the most credit is one you ask them: should merging happen before or after filtering?

The solution

Each filter is independent and skipped when its option is absent, so adding a new criterion means adding one block and nothing else.

function selectData(sessions, options = {}) {
  // Merge first when asked, so the filters see combined totals rather than
  // individual visits. Never mutate the input.
  let rows = options.merge ? mergeSessions(sessions) : sessions.map((session) => ({ ...session }));

  if (options.user !== undefined) {
    rows = rows.filter((row) => row.user === options.user);
  }
  if (options.minDuration !== undefined) {
    rows = rows.filter((row) => row.duration >= options.minDuration);
  }
  if (options.equipment) {
    rows = rows.filter((row) => options.equipment.some((item) => row.equipment.includes(item)));
  }

  return rows;
}

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

  for (const session of sessions) {
    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 5 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy3 cases · the individual filters

filters sessions by user

const sessions = [
  { user: 7, duration: 150, equipment: ["dumbbell"] },
  { user: 1, duration: 40, equipment: ["bench"] },
  { user: 7, duration: 100, equipment: ["bike"] },
];
expectEqual(selectData(sessions, { user: 7 }), [
  { user: 7, duration: 150, equipment: ["dumbbell"] },
  { user: 7, duration: 100, equipment: ["bike"] },
]);

filters sessions by minimum duration

const sessions = [
  { user: 7, duration: 150, equipment: ["dumbbell"] },
  { user: 1, duration: 40, equipment: ["bench"] },
];
expectEqual(selectData(sessions, { minDuration: 100 }), [
  { user: 7, duration: 150, equipment: ["dumbbell"] },
]);

keeps sessions that use any requested equipment

const sessions = [
  { user: 7, duration: 100, equipment: ["bike"] },
  { user: 1, duration: 40, equipment: ["bench"] },
  { user: 2, duration: 200, equipment: ["bike"] },
];
expectEqual(selectData(sessions, { equipment: ["bike"] }), [
  { user: 7, duration: 100, equipment: ["bike"] },
  { user: 2, duration: 200, equipment: ["bike"] },
]);
Hard2 cases · merge ordering and immutability

merges users before filtering, combining durations and equipment

const sessions = [
  { user: 7, duration: 150, equipment: ["dumbbell"] },
  { user: 1, duration: 40, equipment: ["bench"] },
  { user: 7, duration: 100, equipment: ["bike"] },
  { user: 2, duration: 200, equipment: ["bike"] },
];
const merged = selectData(sessions, { merge: true, minDuration: 200 });
const byUser = [...merged].sort((a, b) => a.user - b.user);
expectEqual(byUser, [
  { user: 2, duration: 200, equipment: ["bike"] },
  { user: 7, duration: 250, equipment: ["bike", "dumbbell"] },
]);

leaves the original sessions unchanged

const sessions = [
  { user: 7, duration: 150, equipment: ["dumbbell"] },
  { user: 7, duration: 100, equipment: ["bike"] },
];
selectData(sessions, { merge: true });
expectEqual(sessions.map((session) => session.duration), [150, 100]);

Edge cases to watch out for

Using truthiness to detect supplied options

if (options.user) ignores a user id of 0, and if (options.minDuration) ignores a minimum of 0. Compare against undefined instead.

Filtering before merging without saying so

Both orders are defensible and they produce different results. Picking one silently is the actual mistake; stating the choice and its consequence is what the interviewer is listening for.

Returning the caller's objects

filter returns a new array of the same object references, so a later mutation of a result also mutates the input. Copying the records keeps the boundary clean.

Nesting the conditions into one predicate

A single filter with four ANDed conditions and undefined guards inside it works, and becomes unreadable at the fifth criterion. Separate passes cost a little performance and keep each rule isolated.

If asked to scale it, build an array of predicate functions from the options and apply rows.filter((row) => predicates.every((p) => p(row))). That’s one pass instead of several, and new criteria become one entry in a table rather than another block of code.

Follow up questions

How would you add sorting and pagination?

Keep them as separate stages after filtering, in that order: filter, then sort, then slice. Sorting before filtering wastes work on rows about to be discarded, and paginating before sorting returns the wrong page entirely. Copy before sorting, since sort mutates.

Should you merge before or after filtering?

It depends what the filter means, and it changes the answer. Filtering after merging tests a user's combined total; filtering before tests individual records and merges only what survived. Neither is wrong, and choosing without saying which you chose is.

Why check against undefined instead of truthiness?

Because 0, "" and false are all legitimate filter values that a truthiness check treats as absent. A minimum duration of zero or a user id of zero would be silently ignored.

Practise this with the tests running

Practice Pad runs these 5 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