Support multiple room sizes via room_groups table
This commit is contained in:
@@ -15,10 +15,15 @@ import (
|
|||||||
"rooms/solver"
|
"rooms/solver"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type roomGroupData struct {
|
||||||
|
Size int `json:"size"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
type tripData struct {
|
type tripData struct {
|
||||||
RoomSize int `json:"room_size"`
|
PreferNotMultiple int `json:"prefer_not_multiple"`
|
||||||
PreferNotMultiple int `json:"prefer_not_multiple"`
|
NoPreferCost int `json:"no_prefer_cost"`
|
||||||
NoPreferCost int `json:"no_prefer_cost"`
|
RoomGroups []roomGroupData `json:"room_groups"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type studentData struct {
|
type studentData struct {
|
||||||
@@ -189,7 +194,18 @@ func main() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Students: %d, Room size: %d, Constraints: %d\n", n, trip.RoomSize, len(constraints))
|
var roomSizes []int
|
||||||
|
for _, rg := range trip.RoomGroups {
|
||||||
|
for range rg.Count {
|
||||||
|
roomSizes = append(roomSizes, rg.Size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(roomSizes) == 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "no room_groups in trip data\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Students: %d, Room sizes: %v, Constraints: %d\n", n, roomSizes, len(constraints))
|
||||||
fmt.Printf("Prefer Not multiple: %d, No Prefer cost: %d\n", trip.PreferNotMultiple, trip.NoPreferCost)
|
fmt.Printf("Prefer Not multiple: %d, No Prefer cost: %d\n", trip.PreferNotMultiple, trip.NoPreferCost)
|
||||||
fmt.Printf("Runs per config: %d\n\n", *runs)
|
fmt.Printf("Runs per config: %d\n\n", *runs)
|
||||||
|
|
||||||
@@ -207,7 +223,7 @@ func main() {
|
|||||||
for run := range *runs {
|
for run := range *runs {
|
||||||
rng := rand.New(rand.NewSource(int64(run * 31337)))
|
rng := rand.New(rand.NewSource(int64(run * 31337)))
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
sols := solver.SolveFast(n, trip.RoomSize, trip.PreferNotMultiple, trip.NoPreferCost, constraints, params, rng)
|
sols := solver.SolveFast(n, roomSizes, trip.PreferNotMultiple, trip.NoPreferCost, constraints, params, rng)
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
if len(sols) > 0 {
|
if len(sols) > 0 {
|
||||||
var assignments [][]int
|
var assignments [][]int
|
||||||
|
|||||||
1
drop.sql
1
drop.sql
@@ -1,6 +1,7 @@
|
|||||||
DROP TABLE IF EXISTS roommate_constraints;
|
DROP TABLE IF EXISTS roommate_constraints;
|
||||||
DROP TABLE IF EXISTS parents;
|
DROP TABLE IF EXISTS parents;
|
||||||
DROP TABLE IF EXISTS students;
|
DROP TABLE IF EXISTS students;
|
||||||
|
DROP TABLE IF EXISTS room_groups;
|
||||||
DROP TABLE IF EXISTS trip_admins;
|
DROP TABLE IF EXISTS trip_admins;
|
||||||
DROP TABLE IF EXISTS trips;
|
DROP TABLE IF EXISTS trips;
|
||||||
DROP TYPE IF EXISTS constraint_level;
|
DROP TYPE IF EXISTS constraint_level;
|
||||||
|
|||||||
158
main.go
158
main.go
@@ -87,6 +87,9 @@ func main() {
|
|||||||
http.HandleFunc("GET /api/trips/{tripID}/constraints", handleListConstraints(db))
|
http.HandleFunc("GET /api/trips/{tripID}/constraints", handleListConstraints(db))
|
||||||
http.HandleFunc("POST /api/trips/{tripID}/constraints", handleCreateConstraint(db))
|
http.HandleFunc("POST /api/trips/{tripID}/constraints", handleCreateConstraint(db))
|
||||||
http.HandleFunc("DELETE /api/trips/{tripID}/constraints/{constraintID}", handleDeleteConstraint(db))
|
http.HandleFunc("DELETE /api/trips/{tripID}/constraints/{constraintID}", handleDeleteConstraint(db))
|
||||||
|
http.HandleFunc("GET /api/trips/{tripID}/room-groups", handleListRoomGroups(db))
|
||||||
|
http.HandleFunc("POST /api/trips/{tripID}/room-groups", handleCreateRoomGroup(db))
|
||||||
|
http.HandleFunc("DELETE /api/trips/{tripID}/room-groups/{groupID}", handleDeleteRoomGroup(db))
|
||||||
http.HandleFunc("POST /api/trips/{tripID}/solve", handleSolve(db))
|
http.HandleFunc("POST /api/trips/{tripID}/solve", handleSolve(db))
|
||||||
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := db.Ping(); err != nil {
|
if err := db.Ping(); err != nil {
|
||||||
@@ -295,13 +298,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, t.no_prefer_cost, COALESCE(
|
SELECT t.id, t.name, 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, t.no_prefer_cost
|
GROUP BY t.id, t.name, 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)
|
||||||
@@ -316,7 +319,6 @@ func handleListTrips(db *sql.DB) http.HandlerFunc {
|
|||||||
type trip struct {
|
type trip struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
RoomSize int `json:"room_size"`
|
|
||||||
PreferNotMultiple int `json:"prefer_not_multiple"`
|
PreferNotMultiple int `json:"prefer_not_multiple"`
|
||||||
NoPreferCost int `json:"no_prefer_cost"`
|
NoPreferCost int `json:"no_prefer_cost"`
|
||||||
Admins []tripAdmin `json:"admins"`
|
Admins []tripAdmin `json:"admins"`
|
||||||
@@ -326,7 +328,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, &t.NoPreferCost, &adminsJSON); err != nil {
|
if err := rows.Scan(&t.ID, &t.Name, &t.PreferNotMultiple, &t.NoPreferCost, &adminsJSON); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -470,14 +472,14 @@ func handleGetTrip(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var name string
|
var name string
|
||||||
var roomSize, preferNotMultiple, noPreferCost int
|
var 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)
|
err := db.QueryRow("SELECT name, prefer_not_multiple, no_prefer_cost FROM trips WHERE id = $1", tripID).Scan(&name, &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, "no_prefer_cost": noPreferCost})
|
json.NewEncoder(w).Encode(map[string]any{"id": tripID, "name": name, "prefer_not_multiple": preferNotMultiple, "no_prefer_cost": noPreferCost})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,7 +674,6 @@ func handleUpdateTrip(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
var body struct {
|
var body struct {
|
||||||
RoomSize *int `json:"room_size"`
|
|
||||||
PreferNotMultiple *int `json:"prefer_not_multiple"`
|
PreferNotMultiple *int `json:"prefer_not_multiple"`
|
||||||
NoPreferCost *int `json:"no_prefer_cost"`
|
NoPreferCost *int `json:"no_prefer_cost"`
|
||||||
}
|
}
|
||||||
@@ -680,12 +681,6 @@ func handleUpdateTrip(db *sql.DB) http.HandlerFunc {
|
|||||||
http.Error(w, "invalid request body", http.StatusBadRequest)
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if body.RoomSize != nil {
|
|
||||||
if *body.RoomSize < 1 {
|
|
||||||
http.Error(w, "room_size must be at least 1", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if body.PreferNotMultiple != nil {
|
if body.PreferNotMultiple != nil {
|
||||||
if *body.PreferNotMultiple < 1 {
|
if *body.PreferNotMultiple < 1 {
|
||||||
http.Error(w, "prefer_not_multiple must be at least 1", http.StatusBadRequest)
|
http.Error(w, "prefer_not_multiple must be at least 1", http.StatusBadRequest)
|
||||||
@@ -698,12 +693,6 @@ func handleUpdateTrip(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
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)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if body.PreferNotMultiple != nil {
|
if body.PreferNotMultiple != nil {
|
||||||
if _, err := db.Exec("UPDATE trips SET prefer_not_multiple = $1 WHERE id = $2", *body.PreferNotMultiple, tripID); err != nil {
|
if _, err := db.Exec("UPDATE trips SET prefer_not_multiple = $1 WHERE id = $2", *body.PreferNotMultiple, tripID); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
@@ -993,16 +982,18 @@ func handleListConstraints(db *sql.DB) http.HandlerFunc {
|
|||||||
hardConflicts = append(hardConflicts, chain)
|
hardConflicts = append(hardConflicts, chain)
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomSize int
|
var maxRoomSize int
|
||||||
db.QueryRow("SELECT room_size FROM trips WHERE id = $1", tripID).Scan(&roomSize)
|
db.QueryRow("SELECT COALESCE(MAX(size), 0) FROM room_groups WHERE trip_id = $1", tripID).Scan(&maxRoomSize)
|
||||||
mustGroups := map[int64][]string{}
|
mustGroups := map[int64][]string{}
|
||||||
for _, id := range studentIDs {
|
for _, id := range studentIDs {
|
||||||
root := ufFind(id)
|
root := ufFind(id)
|
||||||
mustGroups[root] = append(mustGroups[root], studentName[id])
|
mustGroups[root] = append(mustGroups[root], studentName[id])
|
||||||
}
|
}
|
||||||
for _, members := range mustGroups {
|
if maxRoomSize > 0 {
|
||||||
if len(members) > roomSize {
|
for _, members := range mustGroups {
|
||||||
oversizedGroups = append(oversizedGroups, members)
|
if len(members) > maxRoomSize {
|
||||||
|
oversizedGroups = append(oversizedGroups, members)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1119,6 +1110,93 @@ func handleDeleteConstraint(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleListRoomGroups(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, tripID, ok := requireTripAdmin(db, w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rows, err := db.Query("SELECT id, size, count FROM room_groups WHERE trip_id = $1 ORDER BY id", tripID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
type roomGroup struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
var groups []roomGroup
|
||||||
|
for rows.Next() {
|
||||||
|
var g roomGroup
|
||||||
|
if err := rows.Scan(&g.ID, &g.Size, &g.Count); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groups = append(groups, g)
|
||||||
|
}
|
||||||
|
if groups == nil {
|
||||||
|
groups = []roomGroup{}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(groups)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleCreateRoomGroup(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, tripID, ok := requireTripAdmin(db, w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Size int `json:"size"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
http.Error(w, "invalid request body", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Size < 1 || body.Count < 1 {
|
||||||
|
http.Error(w, "size and count must be at least 1", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var id int64
|
||||||
|
err := db.QueryRow("INSERT INTO room_groups (trip_id, size, count) VALUES ($1, $2, $3) RETURNING id", tripID, body.Size, body.Count).Scan(&id)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{"id": id, "size": body.Size, "count": body.Count})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDeleteRoomGroup(db *sql.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
_, tripID, ok := requireTripAdmin(db, w, r)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
groupID, err := strconv.ParseInt(r.PathValue("groupID"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "invalid group ID", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := db.Exec("DELETE FROM room_groups WHERE id = $1 AND trip_id = $2", groupID, tripID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if n, _ := result.RowsAffected(); n == 0 {
|
||||||
|
http.Error(w, "room group not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handleSolve(db *sql.DB) http.HandlerFunc {
|
func handleSolve(db *sql.DB) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
_, tripID, ok := requireTripAdmin(db, w, r)
|
_, tripID, ok := requireTripAdmin(db, w, r)
|
||||||
@@ -1126,13 +1204,35 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var roomSize, pnMultiple, npCost int
|
var 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)
|
err := db.QueryRow("SELECT prefer_not_multiple, no_prefer_cost FROM trips WHERE id = $1", tripID).Scan(&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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rgRows, err := db.Query("SELECT size, count FROM room_groups WHERE trip_id = $1 ORDER BY id", tripID)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rgRows.Close()
|
||||||
|
var roomSizes []int
|
||||||
|
for rgRows.Next() {
|
||||||
|
var size, count int
|
||||||
|
if err := rgRows.Scan(&size, &count); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for range count {
|
||||||
|
roomSizes = append(roomSizes, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(roomSizes) == 0 {
|
||||||
|
http.Error(w, "no room groups configured", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
rows, err := db.Query("SELECT id, name FROM students WHERE trip_id = $1 ORDER BY id", tripID)
|
rows, err := db.Query("SELECT id, name FROM students WHERE trip_id = $1 ORDER BY id", tripID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
@@ -1219,14 +1319,14 @@ func handleSolve(db *sql.DB) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rng := rand.New(rand.NewSource(42))
|
rng := rand.New(rand.NewSource(42))
|
||||||
solutions := solver.SolveFast(n, roomSize, pnMultiple, npCost, constraints, solver.DefaultParams, rng)
|
solutions := solver.SolveFast(n, roomSizes, pnMultiple, npCost, constraints, solver.DefaultParams, rng)
|
||||||
|
|
||||||
if solutions == nil {
|
if solutions == nil {
|
||||||
http.Error(w, "hard conflicts exist, resolve before solving", http.StatusBadRequest)
|
http.Error(w, "hard conflicts exist, resolve before solving", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
numRooms := (n + roomSize - 1) / roomSize
|
numRooms := len(roomSizes)
|
||||||
|
|
||||||
type roomMember struct {
|
type roomMember struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
|
|||||||
10
schema.sql
10
schema.sql
@@ -11,11 +11,19 @@ END $$;
|
|||||||
CREATE TABLE IF NOT EXISTS trips (
|
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,
|
|
||||||
prefer_not_multiple INTEGER NOT NULL DEFAULT 5,
|
prefer_not_multiple INTEGER NOT NULL DEFAULT 5,
|
||||||
no_prefer_cost INTEGER NOT NULL DEFAULT 10
|
no_prefer_cost INTEGER NOT NULL DEFAULT 10
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_groups (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
trip_id BIGINT NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||||
|
size INTEGER NOT NULL,
|
||||||
|
count INTEGER NOT NULL,
|
||||||
|
CHECK(size >= 1),
|
||||||
|
CHECK(count >= 1)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS trip_admins (
|
CREATE TABLE IF NOT EXISTS trip_admins (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
trip_id BIGINT NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
trip_id BIGINT NOT NULL REFERENCES trips(id) ON DELETE CASCADE,
|
||||||
|
|||||||
@@ -57,9 +57,9 @@ func normalizeKey(a []int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type solverState struct {
|
type solverState struct {
|
||||||
n int
|
n int
|
||||||
roomSize int
|
roomSizes []int
|
||||||
numRooms int
|
numRooms int
|
||||||
pnMultiple int
|
pnMultiple int
|
||||||
npCost int
|
npCost int
|
||||||
|
|
||||||
@@ -77,11 +77,11 @@ type solverState struct {
|
|||||||
mustApartFor [][]int
|
mustApartFor [][]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSolverState(n, roomSize, pnMultiple, npCost int, constraints []Constraint) *solverState {
|
func newSolverState(n int, roomSizes []int, pnMultiple, npCost int, constraints []Constraint) *solverState {
|
||||||
s := &solverState{
|
s := &solverState{
|
||||||
n: n,
|
n: n,
|
||||||
roomSize: roomSize,
|
roomSizes: roomSizes,
|
||||||
numRooms: (n + roomSize - 1) / roomSize,
|
numRooms: len(roomSizes),
|
||||||
pnMultiple: pnMultiple,
|
pnMultiple: pnMultiple,
|
||||||
npCost: npCost,
|
npCost: npCost,
|
||||||
constraints: constraints,
|
constraints: constraints,
|
||||||
@@ -235,7 +235,6 @@ func (s *solverState) feasibleForGroup(assignment []int, groupRoot int, room int
|
|||||||
|
|
||||||
func (s *solverState) fastHillClimb(assignment []int) int {
|
func (s *solverState) fastHillClimb(assignment []int) int {
|
||||||
n := s.n
|
n := s.n
|
||||||
roomSize := s.roomSize
|
|
||||||
numRooms := s.numRooms
|
numRooms := s.numRooms
|
||||||
|
|
||||||
roomCounts := make([]int, numRooms)
|
roomCounts := make([]int, numRooms)
|
||||||
@@ -386,7 +385,7 @@ func (s *solverState) fastHillClimb(assignment []int) int {
|
|||||||
if room == gRoom {
|
if room == gRoom {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if roomCounts[room]+len(grp) > roomSize {
|
if roomCounts[room]+len(grp) > s.roomSizes[room] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !s.feasibleForGroup(assignment, gRoot, room) {
|
if !s.feasibleForGroup(assignment, gRoot, room) {
|
||||||
@@ -410,7 +409,7 @@ func (s *solverState) fastHillClimb(assignment []int) int {
|
|||||||
}
|
}
|
||||||
newGRoom := roomCounts[gRoom] - len(grp) + len(grp2)
|
newGRoom := roomCounts[gRoom] - len(grp) + len(grp2)
|
||||||
newG2Room := roomCounts[g2Room] - len(grp2) + len(grp)
|
newG2Room := roomCounts[g2Room] - len(grp2) + len(grp)
|
||||||
if newGRoom > roomSize || newG2Room > roomSize {
|
if newGRoom > s.roomSizes[gRoom] || newG2Room > s.roomSizes[g2Room] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !s.feasibleForGroup(assignment, gRoot, g2Room) {
|
if !s.feasibleForGroup(assignment, gRoot, g2Room) {
|
||||||
@@ -456,9 +455,7 @@ func (s *solverState) fastHillClimb(assignment []int) int {
|
|||||||
|
|
||||||
func (s *solverState) initialPlacement(assignment []int) bool {
|
func (s *solverState) initialPlacement(assignment []int) bool {
|
||||||
roomCap := make([]int, s.numRooms)
|
roomCap := make([]int, s.numRooms)
|
||||||
for i := range roomCap {
|
copy(roomCap, s.roomSizes)
|
||||||
roomCap[i] = s.roomSize
|
|
||||||
}
|
|
||||||
|
|
||||||
var placeGroups func(gi int) bool
|
var placeGroups func(gi int) bool
|
||||||
placeGroups = func(gi int) bool {
|
placeGroups = func(gi int) bool {
|
||||||
@@ -517,9 +514,7 @@ func (s *solverState) initialPlacement(assignment []int) bool {
|
|||||||
|
|
||||||
func (s *solverState) randomPlacement(assignment []int, rng *rand.Rand) bool {
|
func (s *solverState) randomPlacement(assignment []int, rng *rand.Rand) bool {
|
||||||
roomCap := make([]int, s.numRooms)
|
roomCap := make([]int, s.numRooms)
|
||||||
for i := range roomCap {
|
copy(roomCap, s.roomSizes)
|
||||||
roomCap[i] = s.roomSize
|
|
||||||
}
|
|
||||||
perm := rng.Perm(len(s.groupList))
|
perm := rng.Perm(len(s.groupList))
|
||||||
for _, pi := range perm {
|
for _, pi := range perm {
|
||||||
grp := s.groupList[pi]
|
grp := s.groupList[pi]
|
||||||
@@ -597,12 +592,12 @@ func (t *solutionTracker) add(a []int, s int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SolveFast(n, roomSize, pnMultiple, npCost int, constraints []Constraint, params Params, rng *rand.Rand) []Solution {
|
func SolveFast(n int, roomSizes []int, pnMultiple, npCost int, constraints []Constraint, params Params, rng *rand.Rand) []Solution {
|
||||||
if n == 0 {
|
if n == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
st := newSolverState(n, roomSize, pnMultiple, npCost, constraints)
|
st := newSolverState(n, roomSizes, pnMultiple, npCost, constraints)
|
||||||
if st.hasHardConflict() {
|
if st.hasHardConflict() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -637,8 +632,8 @@ func SolveFast(n, roomSize, pnMultiple, npCost int, constraints []Constraint, pa
|
|||||||
for _, room := range a {
|
for _, room := range a {
|
||||||
rc[room]++
|
rc[room]++
|
||||||
}
|
}
|
||||||
for _, cnt := range rc {
|
for room, cnt := range rc {
|
||||||
if cnt > roomSize {
|
if cnt > st.roomSizes[room] {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -657,7 +652,7 @@ func SolveFast(n, roomSize, pnMultiple, npCost int, constraints []Constraint, pa
|
|||||||
if room == oldRoom {
|
if room == oldRoom {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if roomCount(assignment, room)+len(grp) > roomSize {
|
if roomCount(assignment, room)+len(grp) > st.roomSizes[room] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, m := range grp {
|
for _, m := range grp {
|
||||||
|
|||||||
@@ -95,7 +95,15 @@
|
|||||||
<h2 id="trip-name"></h2>
|
<h2 id="trip-name"></h2>
|
||||||
<div id="admin-view" style="display: none;">
|
<div id="admin-view" style="display: none;">
|
||||||
<div id="trip-settings">
|
<div id="trip-settings">
|
||||||
<label>Students per room: <input id="room-size" type="number" min="1"></label>
|
<div id="room-groups">
|
||||||
|
<div class="tags" id="room-group-tags"></div>
|
||||||
|
<div style="display: flex; gap: 0.3rem; align-items: center; flex-wrap: wrap;">
|
||||||
|
<wa-input id="new-rg-count" type="number" min="1" placeholder="Count" size="small" style="width: 5rem;"></wa-input>
|
||||||
|
<span>rooms of</span>
|
||||||
|
<wa-input id="new-rg-size" type="number" min="1" placeholder="Size" size="small" style="width: 5rem;"></wa-input>
|
||||||
|
<wa-button id="add-rg-btn" size="small">Add</wa-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<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>
|
<label>No Prefer cost: <input id="np-cost" type="number" min="0"></label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,13 +30,41 @@ if (me.role !== 'admin') {
|
|||||||
await (async () => {
|
await (async () => {
|
||||||
|
|
||||||
document.getElementById('admin-view').style.display = 'block';
|
document.getElementById('admin-view').style.display = 'block';
|
||||||
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('np-cost').value = trip.no_prefer_cost;
|
||||||
document.getElementById('room-size').addEventListener('change', async () => {
|
|
||||||
const size = parseInt(document.getElementById('room-size').value);
|
let roomGroups = [];
|
||||||
if (size >= 1) await api('PATCH', '/api/trips/' + tripID, { room_size: size });
|
|
||||||
|
async function loadRoomGroups() {
|
||||||
|
roomGroups = await api('GET', '/api/trips/' + tripID + '/room-groups');
|
||||||
|
const tags = document.getElementById('room-group-tags');
|
||||||
|
tags.innerHTML = '';
|
||||||
|
for (const rg of roomGroups) {
|
||||||
|
const tag = document.createElement('wa-tag');
|
||||||
|
tag.size = 'small';
|
||||||
|
tag.setAttribute('with-remove', '');
|
||||||
|
tag.textContent = rg.count + ' \u00d7 ' + rg.size + '-person';
|
||||||
|
tag.addEventListener('wa-remove', async () => {
|
||||||
|
await api('DELETE', '/api/trips/' + tripID + '/room-groups/' + rg.id);
|
||||||
|
loadRoomGroups();
|
||||||
|
});
|
||||||
|
tags.appendChild(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await loadRoomGroups();
|
||||||
|
|
||||||
|
document.getElementById('add-rg-btn').addEventListener('click', async () => {
|
||||||
|
const sizeInput = document.getElementById('new-rg-size');
|
||||||
|
const countInput = document.getElementById('new-rg-count');
|
||||||
|
const size = parseInt((sizeInput.value || '').trim());
|
||||||
|
const count = parseInt((countInput.value || '').trim());
|
||||||
|
if (!size || size < 1 || !count || count < 1) return;
|
||||||
|
await api('POST', '/api/trips/' + tripID + '/room-groups', { size, count });
|
||||||
|
sizeInput.value = '';
|
||||||
|
countInput.value = '';
|
||||||
|
loadRoomGroups();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('pn-multiple').addEventListener('change', async () => {
|
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 });
|
||||||
@@ -158,7 +186,8 @@ async function loadStudents() {
|
|||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'conflict-row';
|
div.className = 'conflict-row';
|
||||||
div.appendChild(kindSpan('must'));
|
div.appendChild(kindSpan('must'));
|
||||||
div.appendChild(document.createTextNode(' group too large (' + members.length + ' for room size ' + trip.room_size + '): ' + members.join(', ')));
|
const maxSize = roomGroups.length > 0 ? Math.max(...roomGroups.map(g => g.size)) : 0;
|
||||||
|
div.appendChild(document.createTextNode(' group too large (' + members.length + ' for max room size ' + maxSize + '): ' + members.join(', ')));
|
||||||
det.appendChild(div);
|
det.appendChild(div);
|
||||||
}
|
}
|
||||||
hardConflictsEl.appendChild(det);
|
hardConflictsEl.appendChild(det);
|
||||||
|
|||||||
Reference in New Issue
Block a user