From 3fd28ccc679466f51f58bc156e684dbb806e5ffa Mon Sep 17 00:00:00 2001 From: Justin Yao Du Date: Sun, 5 Nov 2023 14:55:50 -0800 Subject: [PATCH] Try to avoid pairing the same users again (#16) --- src/models/GroupModel.ts | 4 ++- src/services/group.ts | 59 +++++++++++++++++++++++++++++++++------- 2 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/models/GroupModel.ts b/src/models/GroupModel.ts index 0ce3381..de44764 100644 --- a/src/models/GroupModel.ts +++ b/src/models/GroupModel.ts @@ -85,11 +85,13 @@ const GroupSchema = new Schema({ }, }); -const indexes: { [K in keyof Group]?: 1 }[] = [ +const indexes: { [K in keyof Group]?: 1 | -1 }[] = [ // Used to determine what scheduled messages should be sent. { round: 1, initialMessageTimestamp: 1, status: 1 }, { round: 1, reminderMessageTimestamp: 1, status: 1 }, { round: 1, finalMessageTimestamp: 1, status: 1 }, + // Used to find the most recent groups in a particular channel. + { channel: 1, initialMessageTimestamp: -1 }, ]; indexes.forEach((index) => GroupSchema.index(index)); diff --git a/src/services/group.ts b/src/services/group.ts index 560f44d..7cb5af6 100644 --- a/src/services/group.ts +++ b/src/services/group.ts @@ -2,7 +2,7 @@ import { App } from "@slack/bolt"; import mongoose from "mongoose"; import env from "../env"; -import { GroupModel } from "../models/GroupModel"; +import { Group, GroupModel } from "../models/GroupModel"; import { RoundDocument } from "../models/RoundModel"; import { Result } from "../util/result"; @@ -29,6 +29,12 @@ async function getUsersToMatch( return Result.ok(filtered); } +async function getPreviousPairings(channel: string): Promise { + return GroupModel.find({ channel: channel }) + .sort({ initialMessageTimestamp: -1 }) + .limit(50); +} + /** * @returns A shuffled shallow copy of the input array. */ @@ -51,9 +57,44 @@ function shuffled(values: T[]): T[] { * Pair users randomly, including a group of three if there is an odd number, or * a group of one if there is only one user. */ -function makeGroups(users: string[]): string[][] { +function makeGroups(users: string[], prevGroups: Group[]): string[][] { + if (users.length === 0) { + return []; + } else if (users.length <= 3) { + return [users]; + } + users = shuffled(users); + // Attempt to avoid pairing users who have been paired previously. + + const prevPairs = new Set(); + for (const group of prevGroups) { + for (let i = 0; i < group.userIds.length; i++) { + for (let j = 0; j < group.userIds.length; j++) { + if (i != j) { + prevPairs.add(group.userIds[i] + group.userIds[j]); + } + } + } + } + + for (let iteration = 0; iteration < 20; iteration++) { + // Iterate over all pairs. There might be one extra person at the end, who + // becomes part of a group of three, but we ignore them for simplicity. + for (let i = 0; i + 1 < users.length; i += 2) { + if (prevPairs.has(users[i] + users[i + 1])) { + // Users have been paired before - swap one of them with someone else. + const src = i + Math.floor(Math.random() * 2); + const dest = Math.floor(Math.random() * users.length); + + const temp = users[src]; + users[src] = users[dest]; + users[dest] = temp; + } + } + } + const groups: string[][] = []; // Make as many pairs as possible. @@ -65,13 +106,8 @@ function makeGroups(users: string[]): string[][] { // Is there an unpaired person? if (i < users.length) { - if (groups.length === 0) { - // Only one person - put them in a group by themself. - groups.push([users[i]]); - } else { - // Add the person to the first group. - groups[0].push(users[i]); - } + // Add the person to the first group. + groups[0].push(users[i]); } return groups; @@ -90,7 +126,10 @@ async function createGroups( return Result.err(`could not get users to match: ${usersResult.error}`); } - const groups = makeGroups(usersResult.value); + const groups = makeGroups( + usersResult.value, + await getPreviousPairings(round.channel), + ); await mongoose.connection.transaction(async () => { await GroupModel.insertMany(