v1.1.0: Add cancelled course, duplicate prevention, and credit bar ticks
- Mark "Managing Growing Companies" as cancelled with visual indicator and solver exclusion - Prevent selecting duplicate courses across elective sets (e.g., same course in Spring and Summer) - Add 2.5-credit interval tick marks to specialization progress bars - Bump version to 1.1.0 with date display in UI header
This commit is contained in:
@@ -30,13 +30,14 @@ function computeCeiling(
|
||||
otherOpenSetIds: string[],
|
||||
ranking: string[],
|
||||
mode: OptimizationMode,
|
||||
excludedCourseIds?: Set<string>,
|
||||
): { count: number; specs: string[] } {
|
||||
const fn = mode === 'maximize-count' ? maximizeCount : priorityOrder;
|
||||
|
||||
if (otherOpenSetIds.length === 0) {
|
||||
// No other open sets — just solve with this choice added
|
||||
const selected = [...basePinnedCourses, chosenCourseId];
|
||||
const result = fn(selected, ranking, []);
|
||||
const result = fn(selected, ranking, [], excludedCourseIds);
|
||||
return { count: result.achieved.length, specs: result.achieved };
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ function computeCeiling(
|
||||
|
||||
if (setIndex >= otherOpenSetIds.length) {
|
||||
const selected = [...basePinnedCourses, chosenCourseId, ...accumulated];
|
||||
const result = fn(selected, ranking, []);
|
||||
const result = fn(selected, ranking, [], excludedCourseIds);
|
||||
if (result.achieved.length > bestCount) {
|
||||
bestCount = result.achieved.length;
|
||||
bestSpecs = result.achieved;
|
||||
@@ -61,6 +62,7 @@ function computeCeiling(
|
||||
const setId = otherOpenSetIds[setIndex];
|
||||
const courses = coursesBySet[setId];
|
||||
for (const course of courses) {
|
||||
if (excludedCourseIds?.has(course.id)) continue;
|
||||
enumerate(setIndex + 1, [...accumulated, course.id]);
|
||||
if (bestCount >= 3) return;
|
||||
}
|
||||
@@ -91,6 +93,7 @@ export function analyzeDecisionTree(
|
||||
ranking: string[],
|
||||
mode: OptimizationMode,
|
||||
onSetComplete?: (analysis: SetAnalysis) => void,
|
||||
excludedCourseIds?: Set<string>,
|
||||
): SetAnalysis[] {
|
||||
if (openSetIds.length > MAX_OPEN_SETS_FOR_ENUMERATION) {
|
||||
// Fallback: return empty analyses (caller uses upper bounds instead)
|
||||
@@ -107,21 +110,24 @@ export function analyzeDecisionTree(
|
||||
const otherOpenSets = openSetIds.filter((id) => id !== setId);
|
||||
const courses = coursesBySet[setId];
|
||||
|
||||
const choices: ChoiceOutcome[] = courses.map((course) => {
|
||||
const ceiling = computeCeiling(
|
||||
pinnedCourseIds,
|
||||
course.id,
|
||||
otherOpenSets,
|
||||
ranking,
|
||||
mode,
|
||||
);
|
||||
return {
|
||||
courseId: course.id,
|
||||
courseName: course.name,
|
||||
ceilingCount: ceiling.count,
|
||||
ceilingSpecs: ceiling.specs,
|
||||
};
|
||||
});
|
||||
const choices: ChoiceOutcome[] = courses
|
||||
.filter((course) => !excludedCourseIds?.has(course.id))
|
||||
.map((course) => {
|
||||
const ceiling = computeCeiling(
|
||||
pinnedCourseIds,
|
||||
course.id,
|
||||
otherOpenSets,
|
||||
ranking,
|
||||
mode,
|
||||
excludedCourseIds,
|
||||
);
|
||||
return {
|
||||
courseId: course.id,
|
||||
courseName: course.name,
|
||||
ceilingCount: ceiling.count,
|
||||
ceilingSpecs: ceiling.specs,
|
||||
};
|
||||
});
|
||||
|
||||
const impact = variance(choices.map((c) => c.ceilingCount));
|
||||
const analysis: SetAnalysis = { setId, setName: set.name, impact, choices };
|
||||
|
||||
@@ -107,6 +107,7 @@ export function checkFeasibility(
|
||||
export function preFilterCandidates(
|
||||
selectedCourseIds: string[],
|
||||
openSetIds: string[],
|
||||
excludedCourseIds?: Set<string>,
|
||||
): string[] {
|
||||
const selectedSet = new Set(selectedCourseIds);
|
||||
const openSetSet = new Set(openSetIds);
|
||||
@@ -128,6 +129,7 @@ export function preFilterCandidates(
|
||||
let potential = 0;
|
||||
const countedSets = new Set<string>();
|
||||
for (const e of entries) {
|
||||
if (excludedCourseIds?.has(e.courseId)) continue;
|
||||
const setId = setIdByCourse[e.courseId];
|
||||
if (selectedSet.has(e.courseId)) {
|
||||
if (!countedSets.has(setId)) {
|
||||
@@ -165,6 +167,7 @@ export function enumerateS2Choices(selectedCourseIds: string[]): (string | null)
|
||||
export function computeUpperBounds(
|
||||
selectedCourseIds: string[],
|
||||
openSetIds: string[],
|
||||
excludedCourseIds?: Set<string>,
|
||||
): Record<string, number> {
|
||||
const selectedSet = new Set(selectedCourseIds);
|
||||
const openSetSet = new Set(openSetIds);
|
||||
@@ -175,6 +178,7 @@ export function computeUpperBounds(
|
||||
let potential = 0;
|
||||
const countedSets = new Set<string>();
|
||||
for (const e of entries) {
|
||||
if (excludedCourseIds?.has(e.courseId)) continue;
|
||||
const setId = setIdByCourse[e.courseId];
|
||||
if (selectedSet.has(e.courseId)) {
|
||||
if (!countedSets.has(setId)) {
|
||||
|
||||
@@ -56,8 +56,9 @@ export function maximizeCount(
|
||||
selectedCourseIds: string[],
|
||||
ranking: string[],
|
||||
openSetIds: string[],
|
||||
excludedCourseIds?: Set<string>,
|
||||
): { achieved: string[]; allocations: Record<string, Record<string, number>> } {
|
||||
const candidates = preFilterCandidates(selectedCourseIds, openSetIds);
|
||||
const candidates = preFilterCandidates(selectedCourseIds, openSetIds, excludedCourseIds);
|
||||
|
||||
// Only check specs that can be achieved from selected courses alone (not open sets)
|
||||
// Filter to candidates that have qualifying selected courses
|
||||
@@ -108,8 +109,9 @@ export function priorityOrder(
|
||||
selectedCourseIds: string[],
|
||||
ranking: string[],
|
||||
openSetIds: string[],
|
||||
excludedCourseIds?: Set<string>,
|
||||
): { achieved: string[]; allocations: Record<string, Record<string, number>> } {
|
||||
const candidates = new Set(preFilterCandidates(selectedCourseIds, openSetIds));
|
||||
const candidates = new Set(preFilterCandidates(selectedCourseIds, openSetIds, excludedCourseIds));
|
||||
|
||||
// Only consider specs that have qualifying selected courses
|
||||
const withSelectedCourses = new Set(
|
||||
@@ -145,11 +147,12 @@ export function determineStatuses(
|
||||
selectedCourseIds: string[],
|
||||
openSetIds: string[],
|
||||
achieved: string[],
|
||||
excludedCourseIds?: Set<string>,
|
||||
): Record<string, SpecStatus> {
|
||||
const achievedSet = new Set(achieved);
|
||||
const selectedSet = new Set(selectedCourseIds);
|
||||
const openSetSet = new Set(openSetIds);
|
||||
const upperBounds = computeUpperBounds(selectedCourseIds, openSetIds);
|
||||
const upperBounds = computeUpperBounds(selectedCourseIds, openSetIds, excludedCourseIds);
|
||||
const statuses: Record<string, SpecStatus> = {};
|
||||
|
||||
for (const spec of SPECIALIZATIONS) {
|
||||
@@ -189,11 +192,12 @@ export function optimize(
|
||||
ranking: string[],
|
||||
openSetIds: string[],
|
||||
mode: 'maximize-count' | 'priority-order',
|
||||
excludedCourseIds?: Set<string>,
|
||||
): AllocationResult {
|
||||
const fn = mode === 'maximize-count' ? maximizeCount : priorityOrder;
|
||||
const { achieved, allocations } = fn(selectedCourseIds, ranking, openSetIds);
|
||||
const statuses = determineStatuses(selectedCourseIds, openSetIds, achieved);
|
||||
const upperBounds = computeUpperBounds(selectedCourseIds, openSetIds);
|
||||
const { achieved, allocations } = fn(selectedCourseIds, ranking, openSetIds, excludedCourseIds);
|
||||
const statuses = determineStatuses(selectedCourseIds, openSetIds, achieved, excludedCourseIds);
|
||||
const upperBounds = computeUpperBounds(selectedCourseIds, openSetIds, excludedCourseIds);
|
||||
|
||||
return { achieved, allocations, statuses, upperBounds };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user