Skip to content

Instantly share code, notes, and snippets.

@NicolasZanotti
Created December 3, 2021 13:37
Show Gist options
  • Save NicolasZanotti/e171df0dfd836584081cb9a4ad4bed8e to your computer and use it in GitHub Desktop.
Save NicolasZanotti/e171df0dfd836584081cb9a4ad4bed8e to your computer and use it in GitHub Desktop.
A script to assign people to each other at random
import randomlyAssignPeopleToEachOther, { filterExclusions, filterOwnName, filterSelections, orderByPossibleSelections } from "./random-assignment.ts";
const describe = (name:string, fn:Function) => console.log(`${name}:`) + fn();
const test = (name:string, fn:Function) => console.log(`\t• ${name}:`, fn());
describe(
'Filtering data of type Person.',
() => {
test(
'It does not select the same person as giver and receiver.',
() => filterOwnName([{ name: "A" }], { name: "A" }).toString() === ""
);
test(
'It excludes one person if specified.',
() => {
const people = [{ name: "A", exclusions: ["B"] }, { name: "B" }];
const results = filterExclusions(people, people[0]);
return (
results.length === 1 &&
results[0].name === "A"
);
}
)
test(
'It excludes two people if specified.',
() => {
const people = [{ name: "A", exclusions: ["B", "C"] }, { name: "B" }, { name: "C" }];
const results = filterExclusions(people, people[0]);
return (
results.length === 1 &&
results[0].name === "A"
);
}
)
test(
'It does not exclude anybody if nobody is specified.',
() => {
const people = [{ name: "A" }, { name: "B" }];
const results = filterExclusions(people, people[0]);
return (
results.length === 2 &&
results[0].name === "A" &&
results[1].name === "B"
);
}
);
test(
'It does not include previous selections, including the own name.',
() => {
const previousSelections = ["B"];
const people = [{ name: "A" }, { name: "B" }, { name: "C" }];
const results = filterSelections(people, people[0], previousSelections);
return (
results.length === 1 &&
results[0].name === "C"
);
}
);
}
);
describe(
'Ordering data of type Person.',
() => {
test(
'It orders people by available possible selections (e.g. Person "A" has four people to select from). Most limited to least limited.',
() => {
const people = [{ name: "A" }, { name: "B" }, { name: "C", exclusions: ["A"] }, { name: "D", exclusions: ["A", "B"] }];
const result = orderByPossibleSelections(people);
return (
result.length === people.length &&
result[0].name === "D" &&
result[1].name === "C" &&
result[2].name === "A" &&
result[3].name === "B"
);
}
);
test(
'It takes people into account that are excluded but not participating.',
() => {
const people = [{ name: "A" }, { name: "B" }, { name: "C", exclusions: ["A", "NOT_IN_ARRAY", "B"] }];
const result = orderByPossibleSelections(people);
return (
result.length === people.length &&
result[0]?.exclusions?.length === 2 &&
result[0]?.exclusions[0] === "A" &&
result[0]?.exclusions[1] === "B"
);
}
);
}
)
describe(
'Assigning people.',
() => {
test(
'It assigns people in order if no random assignment is set.',
() => {
const people = [{ name: "A" }, { name: "B" }, { name: "C" }, { name: "D" }];
const result = randomlyAssignPeopleToEachOther(people, length => 0);
return (
result.length === people.length &&
result[0].name === "A" &&
result[1].name === "B" &&
result[2].name === "C" &&
result[3].name === "D"
);
}
);
}
)
/**
* This script assigns people to each other at random.
* It is possible to exclude people to ensure they are not assigned.
*
* A use-case is to randomly assign people to each other for Secret Santa.
* The exclusions would be for people that already are giving each other presents.
*
* @example
* const assignment = randomlyAssignPeopleToEachOther([
* { name: "Tintin" },
* { name: "Haddock", exclusions: ["Bianca"] },
* { name: "Thomson" },
* { name: "Calculus" },
* { name: "Nestor", exclusions: ["Haddock"] },
* { name: "Bianca" },
* ]);
* assignment.forEach(p => console.log(`${p.name} is assigned to ${p.selection}`));
*/
export type Person = {
name: string;
selection?: string;
exclusions?: string[];
};
export type RandomIndex = (length: number) => number;
export function filterOwnName(people: Person[], person: Person): Person[] {
return people.filter(p => p.name !== person.name);
}
export function filterExclusions(people: Person[], person: Person): Person[] {
return people.filter(p => !person.exclusions?.includes(p.name));
}
export function filterSelections(people: Person[], person: Person, selections: string[]): Person[] {
return people.filter(p => p.name !== person.name && !selections.includes(p.name));
}
export function orderByPossibleSelections(people: Person[]) {
const participants = people.map(p => p.name);
const withoutUnknownSelections = people.map(p => {
return {
name: p.name,
exclusions: p.exclusions?.filter(excludedPerson => participants.includes(excludedPerson))
} as Person;
});
return withoutUnknownSelections.sort(
(a, b) => (b.exclusions?.length ?? 0) - (a.exclusions?.length ?? 0)
);
}
export default function randomlyAssignPeopleToEachOther(
people: Person[],
randomIndex: RandomIndex = length => Math.floor(Math.random() * length),
tries: number = 5
) {
function assign(triesCount:number):Person[] {
const results = orderByPossibleSelections(people);
const selections: string[] = [];
for (let person of results) {
const withoutOwnName = filterOwnName(people, person);
const withoutExclusions = filterExclusions(withoutOwnName, person);
if (withoutExclusions.length === 0) throw new Error(`Cannot select anybody for ${person.name}. Check if too many people are excluded from their selection.`);
const withoutSelections = filterSelections(withoutExclusions, person, selections);
if (withoutSelections.length === 0) {
if (triesCount === 0) throw new Error(`Nobody left to assign to ${person.name} after ${tries}`);
return assign(triesCount - 1);
} else {
const i = randomIndex(withoutSelections.length);
if (i < 0 || i > withoutSelections.length) throw new Error(`The random index function is out of bounds with ${i}. Check your randomIndex function.`);
person.selection = withoutSelections[i]?.name;
selections.push(person.selection);
}
}
return results;
}
return assign(tries);
}
@NicolasZanotti
Copy link
Author

NicolasZanotti commented Nov 26, 2023

This script can be used for organizing a Secret Santa gift exchange. Also called Wichteln in German speaking countries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment