From 540f93ec67e28381987f58eb11778c912f6e17a2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 10 Jan 2026 03:14:57 +0000 Subject: [PATCH] Add "Day Max Classes" strategy for scheduling - Backend: Updated `ScheduleRanker.score_schedule` to penalize schedules that exceed the daily class limit on selected days. - Frontend: Added UI controls for enabling the strategy, selecting the limit (0, 2, 4, 6), and choosing applicable days (with Select All/Invert buttons). - Strategy is implemented as a soft constraint (scoring penalty) rather than a hard filter. --- backend/ranker.py | 30 ++++++++++++++++++++++++++++++ static/app.js | 19 +++++++++++++++++-- static/index.html | 32 +++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 3 deletions(-) diff --git a/backend/ranker.py b/backend/ranker.py index a1bc8b1..66a9c37 100644 --- a/backend/ranker.py +++ b/backend/ranker.py @@ -109,4 +109,34 @@ def score_schedule(schedule, preferences): overload += (count - limit) score -= overload * 5.0 # Heavy penalty + # 5. Day Max Limit (Specific Days) + if preferences.get('day_max_limit_enabled'): + limit = preferences.get('day_max_limit_value', 4) + target_days = preferences.get('day_max_limit_days', []) # List of bools, idx 0=Mon + + penalty = 0 + # Ensure target_days has 7 elements + if len(target_days) < 7: + target_days = target_days + [False] * (7 - len(target_days)) + + for w in range(1, 26): + mask = full_bitmap[w] + if mask == 0: continue + + for d in range(7): + # Check if this day is selected for limiting + if not target_days[d]: + continue + + day_bits = (mask >> (d * 13)) & 0x1FFF + count = bin(day_bits).count('1') + + if count > limit: + # Penalty calculation + # If limit is 0 (day off), any class is bad. + diff = count - limit + penalty += diff * 50.0 # Very heavy penalty + + score -= penalty + return score diff --git a/static/app.js b/static/app.js index 52c6bd3..80f63a4 100644 --- a/static/app.js +++ b/static/app.js @@ -11,7 +11,10 @@ createApp({ avoid_early_morning: false, avoid_weekend: false, compactness: 'none', - max_daily_load: 0 + max_daily_load: 0, + day_max_limit_enabled: false, + day_max_limit_value: 4, + day_max_limit_days: [true, true, true, true, true, true, true] }); const filterText = ref(''); @@ -89,6 +92,18 @@ createApp({ visible.forEach(c => c.checked = !allChecked); }; + const toggleAllDays = (select) => { + for(let i=0; i<7; i++) { + preferences.day_max_limit_days[i] = select; + } + }; + + const invertDays = () => { + for(let i=0; i<7; i++) { + preferences.day_max_limit_days[i] = !preferences.day_max_limit_days[i]; + } + }; + // --- Import Logic --- const openImportModal = () => { @@ -332,7 +347,7 @@ createApp({ filterText, hasSearched, filteredSearchResults, doSearch, createGroup, getGroupName, getActiveCount, toggleCandidate, removeGroup, generateSchedules, getCell, downloadImage, saveSession, newSession, toastRef, - toggleSelectAll, + toggleSelectAll, toggleAllDays, invertDays, showImportModal, importText, isImporting, importStatus, importParams, openImportModal, closeImportModal, startBatchImport }; diff --git a/static/index.html b/static/index.html index 151174f..02f3303 100644 --- a/static/index.html +++ b/static/index.html @@ -178,7 +178,37 @@