Add configurable no-prefer penalty to solver

This commit is contained in:
Ian Gulliver
2026-02-15 21:57:53 -08:00
parent 758d0708ec
commit af338114c6
4 changed files with 47 additions and 10 deletions

48
main.go
View File

@@ -240,13 +240,13 @@ func handleListTrips(db *sql.DB) http.HandlerFunc {
return
}
rows, err := db.Query(`
SELECT t.id, t.name, t.room_size, t.prefer_not_multiple, COALESCE(
SELECT t.id, t.name, t.room_size, t.prefer_not_multiple, t.no_prefer_cost, COALESCE(
json_agg(json_build_object('id', ta.id, 'email', ta.email)) FILTER (WHERE ta.id IS NOT NULL),
'[]'
)
FROM trips t
LEFT JOIN trip_admins ta ON ta.trip_id = t.id
GROUP BY t.id, t.name, t.room_size, t.prefer_not_multiple
GROUP BY t.id, t.name, t.room_size, t.prefer_not_multiple, t.no_prefer_cost
ORDER BY t.id`)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -263,6 +263,7 @@ func handleListTrips(db *sql.DB) http.HandlerFunc {
Name string `json:"name"`
RoomSize int `json:"room_size"`
PreferNotMultiple int `json:"prefer_not_multiple"`
NoPreferCost int `json:"no_prefer_cost"`
Admins []tripAdmin `json:"admins"`
}
@@ -270,7 +271,7 @@ func handleListTrips(db *sql.DB) http.HandlerFunc {
for rows.Next() {
var t trip
var adminsJSON string
if err := rows.Scan(&t.ID, &t.Name, &t.RoomSize, &t.PreferNotMultiple, &adminsJSON); err != nil {
if err := rows.Scan(&t.ID, &t.Name, &t.RoomSize, &t.PreferNotMultiple, &t.NoPreferCost, &adminsJSON); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
@@ -389,14 +390,14 @@ func handleGetTrip(db *sql.DB) http.HandlerFunc {
return
}
var name string
var roomSize, preferNotMultiple int
err := db.QueryRow("SELECT name, room_size, prefer_not_multiple FROM trips WHERE id = $1", tripID).Scan(&name, &roomSize, &preferNotMultiple)
var roomSize, preferNotMultiple, noPreferCost int
err := db.QueryRow("SELECT name, room_size, prefer_not_multiple, no_prefer_cost FROM trips WHERE id = $1", tripID).Scan(&name, &roomSize, &preferNotMultiple, &noPreferCost)
if err != nil {
http.Error(w, "trip not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"id": tripID, "name": name, "room_size": roomSize, "prefer_not_multiple": preferNotMultiple})
json.NewEncoder(w).Encode(map[string]any{"id": tripID, "name": name, "room_size": roomSize, "prefer_not_multiple": preferNotMultiple, "no_prefer_cost": noPreferCost})
}
}
@@ -564,6 +565,7 @@ func handleUpdateTrip(db *sql.DB) http.HandlerFunc {
var body struct {
RoomSize *int `json:"room_size"`
PreferNotMultiple *int `json:"prefer_not_multiple"`
NoPreferCost *int `json:"no_prefer_cost"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
@@ -581,6 +583,12 @@ func handleUpdateTrip(db *sql.DB) http.HandlerFunc {
return
}
}
if body.NoPreferCost != nil {
if *body.NoPreferCost < 0 {
http.Error(w, "no_prefer_cost must be at least 0", http.StatusBadRequest)
return
}
}
if body.RoomSize != nil {
if _, err := db.Exec("UPDATE trips SET room_size = $1 WHERE id = $2", *body.RoomSize, tripID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
@@ -593,6 +601,12 @@ func handleUpdateTrip(db *sql.DB) http.HandlerFunc {
return
}
}
if body.NoPreferCost != nil {
if _, err := db.Exec("UPDATE trips SET no_prefer_cost = $1 WHERE id = $2", *body.NoPreferCost, tripID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
w.WriteHeader(http.StatusNoContent)
}
}
@@ -729,8 +743,8 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
return
}
var roomSize, pnMultiple int
err := db.QueryRow("SELECT room_size, prefer_not_multiple FROM trips WHERE id = $1", tripID).Scan(&roomSize, &pnMultiple)
var roomSize, pnMultiple, npCost int
err := db.QueryRow("SELECT room_size, prefer_not_multiple, no_prefer_cost FROM trips WHERE id = $1", tripID).Scan(&roomSize, &pnMultiple, &npCost)
if err != nil {
http.Error(w, "trip not found", http.StatusNotFound)
return
@@ -863,18 +877,34 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
groups[root] = append(groups[root], i)
}
hasPrefer := make([]bool, n)
for pk, kind := range overalls {
if kind == "prefer" {
hasPrefer[idx[pk.a]] = true
}
}
score := func(assignment []int) int {
s := 0
gotPrefer := make([]bool, n)
for pk, kind := range overalls {
ai, bi := idx[pk.a], idx[pk.b]
sameRoom := assignment[ai] == assignment[bi]
switch kind {
case "prefer":
if sameRoom { s++ }
if sameRoom {
s++
gotPrefer[ai] = true
}
case "prefer_not":
if sameRoom { s -= pnMultiple }
}
}
for i := 0; i < n; i++ {
if hasPrefer[i] && !gotPrefer[i] {
s -= npCost
}
}
return s
}

View File

@@ -12,7 +12,8 @@ CREATE TABLE IF NOT EXISTS trips (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
room_size INTEGER NOT NULL DEFAULT 2,
prefer_not_multiple INTEGER NOT NULL DEFAULT 5
prefer_not_multiple INTEGER NOT NULL DEFAULT 5,
no_prefer_cost INTEGER NOT NULL DEFAULT 10
);
CREATE TABLE IF NOT EXISTS trip_admins (

View File

@@ -87,6 +87,7 @@
<div id="trip-settings">
<label>Students per room: <input id="room-size" type="number" min="1"></label>
<label>Prefer Not cost: <input id="pn-multiple" type="number" min="1"></label>
<label>No Prefer cost: <input id="np-cost" type="number" min="0"></label>
</div>
<div id="conflicts"></div>
<div id="mismatches"></div>

View File

@@ -17,6 +17,7 @@ try {
document.getElementById('trip-name').textContent = trip.name;
document.getElementById('room-size').value = trip.room_size;
document.getElementById('pn-multiple').value = trip.prefer_not_multiple;
document.getElementById('np-cost').value = trip.no_prefer_cost;
document.getElementById('main').style.display = 'block';
document.getElementById('logout-btn').addEventListener('click', logout);
document.getElementById('room-size').addEventListener('change', async () => {
@@ -27,6 +28,10 @@ document.getElementById('pn-multiple').addEventListener('change', async () => {
const val = parseInt(document.getElementById('pn-multiple').value);
if (val >= 1) await api('PATCH', '/api/trips/' + tripID, { prefer_not_multiple: val });
});
document.getElementById('np-cost').addEventListener('change', async () => {
const val = parseInt(document.getElementById('np-cost').value);
if (val >= 0) await api('PATCH', '/api/trips/' + tripID, { no_prefer_cost: val });
});
let lastOveralls = {};