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.
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 input is copied immediately, because
sort()mutates. This is the single most common bug in this question. - Unique runs before sorted, so deduplication can’t reintroduce ordering surprises.
- Zero and one items are special-cased, since there’s no separator to place in either.
- Everything but the last joins with ", " and the last is appended after " and ", which is the whole formatting rule.
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.
✓ 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.
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.