I thought a "random pairing" tool would be a half-hour job. Shuffle a list, take names two at a time, done.
The annoying part only showed up when I tried to make it usable for recurring things like coffee chats, book clubs, and gift exchanges. "Random" is easy. "Random, but please stop giving me the same pairings as last month" is where the implementation stops being trivial.
The input cleanup is doing more than it looks like
The first thing the component does is normalize the pasted list into a deduplicated array:
const namesList = computed(() => {
const tokens = rawNames.value
.split(/[\n,,;;]+/g)
.map((s) => s.trim())
.filter(Boolean);
return [...new Set(tokens)];
});
I like this because it fixes the boring real-world mess up front. People paste one name per line, or comma-separated, or full-width Chinese punctuation, and the tool accepts all of it. Trimming plus filter(Boolean) also prevents blank lines from turning into fake participants.
The more opinionated part is new Set(tokens). That means exact duplicate names are silently collapsed before any pairing happens. For a lot of office or classroom lists that's the right default, because duplicates are usually copy-paste mistakes. But it also means two different people both named Alex would be treated as one person unless the user distinguishes them manually.
The pairing logic avoids the classic "one person left over" bug
The shuffle itself is the standard Fisher-Yates pattern:
function shuffle(arr) {
const a = [...arr];
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
That part is conventional. The more interesting bit is what happens after the shuffle:
function buildPairingUnits(shuffled) {
const units = [];
const n = shuffled.length;
let i = 0;
let idx = 1;
while (i < n) {
if (n - i === 3) {
units.push({ id: idx++, members: shuffled.slice(i, i + 3), isTrio: true });
i += 3;
} else {
units.push({ id: idx++, members: shuffled.slice(i, i + 2), isTrio: false });
i += 2;
}
}
return units;
}
Instead of producing pairs until one unlucky person is stranded, it watches for the "exactly 3 left" case and turns that into a trio. That's a small rule, but it's the difference between a usable recurring tool and a toy demo. Nobody gets dropped, and the UI can explicitly label the result as a three-person unit.
Grouping by size is really balancing by remainder
The grouping mode has a subtle implementation detail I didn't expect at first. Even when the user picks "group by size", the code doesn't just keep slicing fixed chunks and dump leftovers into a tiny final group:
if (groupSizeMode.value === "byCount") {
const k = Math.max(1, Math.min(n, Math.floor(groupCountInput.value) || 1));
const base = Math.floor(n / k);
const remainder = n % k;
sizes = Array.from({ length: k }, (_, i) => base + (i < remainder ? 1 : 0));
} else {
const s = Math.max(1, Math.floor(groupSizeInput.value) || 1);
const k = Math.max(1, Math.round(n / s) || Math.ceil(n / s));
const base = Math.floor(n / k);
const remainder = n % k;
sizes = Array.from({ length: k }, (_, i) => base + (i < remainder ? 1 : 0));
}
The key idea is that both modes end up distributing the remainder across groups as evenly as possible. So if you have 10 people and ask for groups of 3, you don't get 3,3,3,1; you get something closer to 4,3,3. That feels much fairer in practice, especially for workshops or discussion groups where a lonely one-person leftover would be obviously wrong.
"Avoid repeats" is implemented as a search problem, not a hard rule
The smartest part of the tool is that it doesn't store only exact past groups. It breaks every result into pairwise co-occurrences:
function pairKey(a, b) {
return [a, b].sort().join("___");
}
function coOccurrencePairsOfUnits(units) {
const keys = [];
for (const u of units) {
const members = u.members;
for (let a = 0; a < members.length; a++) {
for (let b = a + 1; b < members.length; b++) {
keys.push(pairKey(members[a], members[b]));
}
}
}
return keys;
}
That design matters a lot. In grouping mode, the history is still tracking "who has already been together," not just "was this exact four-person group repeated." That's a much better definition of fairness for recurring rotations.
Then, instead of pretending repeats can always be avoided, the component tries up to 300 random draws and keeps the one with the fewest historical conflicts:
const MAX_ATTEMPTS = 300;
let best = null;
for (let i = 0; i < MAX_ATTEMPTS; i++) {
const attempt = generateAttempt();
if (!best || attempt.conflicts < best.conflicts) best = attempt;
if (attempt.conflicts === 0) break;
}
I like this tradeoff. It's honest about the math: with a small fixed roster, eventually you run out of unseen pairings. So the goal becomes "minimize repeats" rather than "guarantee perfection." For a client-side tool, 300 attempts is also cheap enough to feel instant.
The gotchas are real
There are a few limitations here that are worth saying out loud:
- History is stored in
localStorage, so it's tied to one browser on one device. Clear browser data or switch laptops, and the pairing memory is gone. -
saveHistoryStore()intentionally fails silently if storage is blocked or full. That's good for not breaking the UI, but it also means users may think history was saved when the browser refused it. - The "best of 300 attempts" approach is a heuristic, not a proof of optimality. It usually finds a low-conflict result, but it isn't solving the pairing problem exhaustively.
- Exact duplicate names are deduplicated. That's nice for accidental repeats, but not for two distinct humans with the same displayed name.
I turned that into a small free tool here: Fair Random Pairing & Grouping Generator.
Available in other languages
- 公平隨機配對/分組產生器 — 繁體中文
- 公平随机配对/分组生成器 — 简体中文
- Fair Random Pairing & Grouping Generator — English
- 公平ランダムペア・グループ分けツール — 日本語
- 공정한 랜덤 짝짓기·조 나누기 생성기 — 한국어
- Générateur de paires/groupes aléatoires équitables — Français
- Генератор случайных пар и групп — Русский
- Fairer Zufalls-Paarungs- & Gruppengenerator — Deutsch
- Generator Pasangan/Grup Acak yang Adil — Bahasa Indonesia
- Generador de Parejas/Grupos Aleatorios Justos — Español
- Công cụ Ghép Đôi/Chia Nhóm Ngẫu Nhiên Công Bằng — Tiếng Việt
- ตัวสุ่มจับคู่/แบ่งกลุ่มอย่างเป็นธรรม — ไทย
- Generator Sprawiedliwych Losowych Par/Grup — Polski
- Geçmişi Hatırlayan Adil Rastgele Eşleştirme/Gruplama Aracı — Türkçe
- Generatore di Coppie/Gruppi Casuali Equi — Italiano
- Gerador de Pares/Grupos Aleatórios Justos — Português
- Eerlijke Willekeurige Koppel-/Groepsgenerator — Nederlands
- Генератор випадкових пар і груп — Українська
Top comments (0)