workshop-library/library.go

234 lines
6.2 KiB
Go
Raw Normal View History

2025-02-12 19:56:11 +01:00
package main
import (
"fmt"
"sync"
"time"
"log/slog"
)
// Participant struct
type Participant struct {
ID int
Preferences []int
AssignedTo int
RejectedFrom map[int]bool
mu sync.Mutex
}
// Workshop struct
type Workshop struct {
ID int
Capacity int
Participants []int
mu sync.Mutex
}
// Concurrent Stable Matching Algorithm
func StableMatch(participants []Participant, workshops []Workshop) {
var wg sync.WaitGroup
numWorkers := 10
tasks := make(chan *Participant, len(participants))
worker := func() {
defer wg.Done()
for p := range tasks {
assignParticipant(p, &workshops, participants)
}
}
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker()
}
for i := range participants {
tasks <- &participants[i]
}
close(tasks)
wg.Wait()
}
// Assign a participant to a workshop
func assignParticipant(p *Participant, workshops *[]Workshop, participants []Participant) {
p.mu.Lock()
defer p.mu.Unlock()
assigned := false
for _, workshopID := range p.Preferences {
if p.RejectedFrom[workshopID] {
continue
}
if workshopID-1 < 0 || workshopID-1 >= len(*workshops) {
continue // Prevent index out of range
}
w := &(*workshops)[workshopID-1]
w.mu.Lock()
if len(w.Participants) < w.Capacity {
p.AssignedTo = workshopID
w.Participants = append(w.Participants, p.ID)
w.mu.Unlock()
assigned = true
break
} else {
weakestID, weakestIndex := findWeakestParticipant(w.Participants, participants, workshopID)
if weakestIndex != -1 && isPreferred(p.ID, weakestID, workshopID, participants) {
replaceParticipant(weakestID, p, w, weakestIndex, participants)
w.mu.Unlock()
assigned = true
break
} else {
p.RejectedFrom[workshopID] = true
}
}
w.mu.Unlock()
}
if !assigned {
for i := range *workshops {
w := &(*workshops)[i]
w.mu.Lock()
if len(w.Participants) < w.Capacity {
p.AssignedTo = w.ID
w.Participants = append(w.Participants, p.ID)
w.mu.Unlock()
slog.Info("Participant is assigned to Workshop as fallback\n", slog.Int("ParticipantId", p.ID), slog.Int("WorkshopId", w.ID))
assigned = true
break
}
w.mu.Unlock()
}
}
if !assigned {
slog.Info("❌ Participant could not be assigned\n", slog.Int("ParticipantId", p.ID))
p.AssignedTo = -1
}
}
// Replace weakest participant in a workshop
func replaceParticipant(weakestID int, newP *Participant, w *Workshop, weakestIndex int, participants []Participant) {
for i, id := range w.Participants {
if id == weakestID {
w.Participants = append(w.Participants[:i], w.Participants[i+1:]...)
break
}
}
w.Participants[weakestIndex] = newP.ID
newP.AssignedTo = w.ID
participants[weakestID-1].AssignedTo = -1
participants[weakestID-1].RejectedFrom[w.ID] = true
}
// Find the weakest participant in a workshop
func findWeakestParticipant(participantIDs []int, participants []Participant, workshopID int) (int, int) {
weakestID := -1
weakestIndex := -1
lowestRank := 4
for i, id := range participantIDs {
if id-1 < 0 || id-1 >= len(participants) {
continue
}
for rank, pref := range participants[id-1].Preferences {
if pref == workshopID && rank < lowestRank {
lowestRank = rank
weakestID = id
weakestIndex = i
}
}
}
return weakestID, weakestIndex
}
// Check if participant A is preferred over B for a workshop
func isPreferred(aID, bID, workshopID int, participants []Participant) bool {
if aID-1 < 0 || aID-1 >= len(participants) || bID-1 < 0 || bID-1 >= len(participants) {
return false
}
aRank, bRank := 4, 4
for rank, pref := range participants[aID-1].Preferences {
if pref == workshopID {
aRank = rank
}
}
for rank, pref := range participants[bID-1].Preferences {
if pref == workshopID {
bRank = rank
}
}
return aRank < bRank
}
// Print final assignments
func PrintAssignments(participants []Participant, workshops []Workshop) {
fmt.Println("\nFinal Assignments:")
for _, p := range participants {
fmt.Printf("Participant %d → Workshop %d\n", p.ID, p.AssignedTo)
}
fmt.Println("\nWorkshops and their Participants:")
for _, w := range workshops {
fmt.Printf("Workshop %d: %v\n", w.ID, w.Participants)
}
}
// Print statistics
func PrintStatistics(participants []Participant, workshops []Workshop, duration time.Duration) {
totalParticipants := len(participants)
totalWorkshops := len(workshops)
assignedCount := 0
unassignedCount := 0
firstChoiceCount := 0
secondChoiceCount := 0
thirdChoiceCount := 0
fallbackCount := 0
// Count participant assignment stats
for _, p := range participants {
if p.AssignedTo != -1 {
assignedCount++
// Check which preference was assigned
if p.AssignedTo == p.Preferences[0] {
firstChoiceCount++
} else if p.AssignedTo == p.Preferences[1] {
secondChoiceCount++
} else if p.AssignedTo == p.Preferences[2] {
thirdChoiceCount++
} else {
fallbackCount++
}
} else {
unassignedCount++
}
}
// Calculate workshop utilization
fmt.Println("\nWorkshop Utilization:")
for _, w := range workshops {
usage := float64(len(w.Participants)) / float64(w.Capacity) * 100
fmt.Printf("Workshop %d: %d/%d participants (%.2f%% full)\n", w.ID, len(w.Participants), w.Capacity, usage)
}
// Print summary statistics
fmt.Println("\n🔹 Matching Statistics:")
fmt.Printf("⏳ Runtime: %d ms\n", duration.Milliseconds())
fmt.Printf("📌 Total Participants: %d\n", totalParticipants)
fmt.Printf("🏫 Total Workshops: %d\n", totalWorkshops)
fmt.Printf("✅ Assigned Participants: %d (%.2f%%)\n", assignedCount, float64(assignedCount)/float64(totalParticipants)*100)
fmt.Printf("❌ Unassigned Participants: %d (%.2f%%)\n", unassignedCount, float64(unassignedCount)/float64(totalParticipants)*100)
fmt.Printf("🥇 First Choice Assigned: %d (%.2f%%)\n", firstChoiceCount, float64(firstChoiceCount)/float64(totalParticipants)*100)
fmt.Printf("🥈 Second Choice Assigned: %d (%.2f%%)\n", secondChoiceCount, float64(secondChoiceCount)/float64(totalParticipants)*100)
fmt.Printf("🥉 Third Choice Assigned: %d (%.2f%%)\n", thirdChoiceCount, float64(thirdChoiceCount)/float64(totalParticipants)*100)
fmt.Printf("⚠️ Assigned via Fallback: %d (%.2f%%)\n", fallbackCount, float64(fallbackCount)/float64(totalParticipants)*100)
}