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

View File

@@ -12,7 +12,8 @@ CREATE TABLE IF NOT EXISTS trips (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
room_size INTEGER NOT NULL DEFAULT 2, 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 ( CREATE TABLE IF NOT EXISTS trip_admins (

View File

@@ -87,6 +87,7 @@
<div id="trip-settings"> <div id="trip-settings">
<label>Students per room: <input id="room-size" type="number" min="1"></label> <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>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>
<div id="conflicts"></div> <div id="conflicts"></div>
<div id="mismatches"></div> <div id="mismatches"></div>

View File

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