Format a list with commas 'and'

Join all but the last item with commas, then append the last with "and". Handle the empty and single-item cases first, because both break the general rule. Options such as sorted and unique are applied to a copy before formatting so the input is never modified.

Easy array methods string formatting

The question

A gentle question that rewards care. Every candidate writes the two-item and three-item cases correctly; the ones who stand out handle zero and one, and notice that sort() mutates the array it’s called on.

The solution

The copy on the first line is doing real work: sort() sorts in place, so without it the caller's array would be reordered.

function listFormat(items, options = {}) {
  let values = [...items];

  if (options.unique) values = [...new Set(values)];
  if (options.sorted) values = values.sort();

  if (values.length === 0) return "";
  if (values.length === 1) return String(values[0]);

  const last = values[values.length - 1];
  const rest = values.slice(0, -1);
  return rest.join(", ") + " and " + last;
}

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 3 cases Practice Pad runs against your solution, and the implementation above passes all of them.

Easy3 cases · joining and options

joins names with commas and a final "and"

expectEqual(listFormat(["Ada", "Grace", "Linus"]), "Ada, Grace and Linus");
expectEqual(listFormat(["Ada", "Linus"]), "Ada and Linus");

applies the sorted option on its own

expectEqual(listFormat(["Linus", "Ada", "Grace"], { sorted: true }), "Ada, Grace and Linus");

applies the unique option on its own

expectEqual(listFormat(["Ada", "Linus", "Ada"], { unique: true }), "Ada and Linus");

Edge cases to watch out for

Letting sort() mutate the caller's array

Calling items.sort() reorders the array that was passed in. Copying first with a spread costs one line and prevents an action-at-a-distance bug that’s genuinely hard to track down.

Forgetting the zero and one item cases

The general rule produces " and Ada" for a single item and " and undefined" for none. Both need handling before the main path.

Joining then replacing the last comma

Building the whole string and then using lastIndexOf(",") to swap in "and" breaks the moment an item itself contains a comma. Slice the array instead.

The built-in exists. new Intl.ListFormat("en", { style: "long", type: "conjunction" }).format(items) does this, and handles the Oxford comma and every other locale's conventions correctly. Implementing it by hand is the exercise; knowing the built-in is the professional answer.

Follow up questions

How do you handle "or" instead of "and"?

Make the conjunction a parameter rather than hard-coding it. That’s exactly what Intl.ListFormat does with its type option: "conjunction" for and, "disjunction" for or, "unit" for a plain list with no final word at all.

What is Intl.ListFormat?

A built-in that formats lists according to locale conventions, including whether to use a serial comma and what the local equivalent of "and" or "or" is. new Intl.ListFormat("en").format(["a","b","c"]) gives "a, b, and c".

Why copy the array before sorting?

Because Array.prototype.sort sorts in place and returns the same array. Sorting the argument directly reorders the caller's data as a side effect, which is exactly the kind of bug that’s hard to trace back. toSorted() is the newer non-mutating form.

Practise this with the tests running

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