diff --git a/extension/requirements.test.js b/extension/requirements.test.js index a0a2f1e..0b0447f 100644 --- a/extension/requirements.test.js +++ b/extension/requirements.test.js @@ -4,7 +4,7 @@ const assert = require('assert'); Object.assign(globalThis, require('./src/curriculum.js'), require('./src/scheduler.js')); const { UPLB_CATALOG } = require('./src/catalog.js'); -const { amisTermToAbs, gradeResult, removalKey, applyRemovals, amisCourses, plannerCourseList, remainingRequirements, gwaOutlook } = require('./src/requirements.js'); +const { amisTermToAbs, gradeResult, removalKey, applyRemovals, surplusElectives, amisCourses, plannerCourseList, remainingRequirements, gwaOutlook } = require('./src/requirements.js'); assert.strictEqual(amisTermToAbs(1251), 2025 * 3); assert.strictEqual(amisTermToAbs('1232'), 2023 * 3 + 1); @@ -144,3 +144,17 @@ console.log('requirements.test.js: all assertions passed'); assert.strictEqual(remainingRequirements(econ, []).electives.doneUnits, 0, 'no electives done with nothing taken'); console.log('requirements.test.js: HIST 1 for KAS 1 passed'); } + +// Honors count only the required electives, the earliest first. +{ + const prog = UPLB_PROGRAMS.BSCS; + const list = plannerCourseList(prog, 'sp', UPLB_CATALOG); + const feSlots = list.filter(c => /^FE\b/.test(c.code)).length; + const extra = Array.from({ length: feSlots + 2 }, (_, i) => ({ code: `SOC ${100 + i}`, title: 'Elective', units: 3, grade: '1.00', result: 'passed', termId: String(1231 + i) })); + const named = { code: 'CMSC 12', title: 'Foundations of Computer Science', units: 3, grade: '1.00', result: 'passed', termId: '1231' }; + const surplus = surplusElectives(list, [named, ...extra]); + assert.deepStrictEqual(surplus.map(r => r.code), extra.slice(-2).map(r => r.code), 'the two latest electives are the extra ones'); + assert.deepStrictEqual(surplusElectives(list, [named, ...extra.slice(0, feSlots)]), [], 'no surplus when electives fit'); + assert.deepStrictEqual(surplusElectives(list.filter(c => c.genericRequirement !== 'elective'), extra), [], 'no elective slots, no guess'); +} +console.log('requirements.test.js: surplus electives passed'); diff --git a/extension/src/popup.html b/extension/src/popup.html index 42df02d..0138231 100644 --- a/extension/src/popup.html +++ b/extension/src/popup.html @@ -55,6 +55,7 @@
Not affiliated with UP. Unofficial estimate. INC/DRP may not reflect correctly. Always verify with the OUR.
+ diff --git a/extension/src/popup.js b/extension/src/popup.js index 4d2501c..41556f5 100644 --- a/extension/src/popup.js +++ b/extension/src/popup.js @@ -622,6 +622,8 @@ function displayRemaining() { const trackInfoEl = $('trackInfo'); const curriculum = getCurrentCurriculum(); + $('honorsGwa').replaceChildren(); + if (window.gradesData) displayHonorStatus(window.gradesData.gwa, window.gradesData.totalUnits); const quality = getProgramDataQuality(curriculum.code); showNotice($('whatifQuality'), !quality.confident && { tone: 'warn', icon: 'alert', title: 'Treat these numbers as a rough guide', body: quality.reasons.join(' '), @@ -666,6 +668,7 @@ function displayRemaining() { renderSpecializationCourses(new Set(passedRows.map(r => r.code))); const left = remainingRequirements(courses, passedRows, { substitutions, overrides: customCourseStatus }); window.requirementsLeft = left; + displayHonorsGwa(surplusElectives(courses, passedRows, substitutions), allCourses); // Substitution pickers, sorted by code const requiredCodesSet = new Set((curriculum.requiredCodes || []).map(c => c.toUpperCase().trim())); @@ -710,6 +713,19 @@ function displayRemaining() { renderWhatIf(); } +// Extra electives stay in the GWA but not in the one for Latin honors, so the +// honor badge follows the honors GWA and the card says what it left out. +function displayHonorsGwa(surplus, allCourses) { + if (!surplus.length) return; + const skip = new Set(surplus.map(r => removalKey(r.termId, r.code))); + const { gwa, totalUnits } = calculateGWA(allCourses.filter(c => !skip.has(removalKey(c.termId, c.courseCode))), excludedCourses); + displayHonorStatus(gwa, totalUnits); + $('honorsGwa').replaceChildren(stat('GWA for Latin honors', gwa.toFixed(4), { + variant: 'row', + note: `Counts only the electives your program requires, the earliest first. Leaves out ${surplus.map(r => r.code).join(', ')}.`, + })); +} + // A 4.00 gets one removal exam. Picking its result recomputes everything as // if AMIS already showed 3.00 (pass) or 5.00 (fail). function removalPicker(course) { diff --git a/extension/src/requirements.js b/extension/src/requirements.js index cc7538b..c3c6186 100644 --- a/extension/src/requirements.js +++ b/extension/src/requirements.js @@ -113,6 +113,18 @@ function fillRequirementSlots(courses, rows, substitutions = {}) { return fill; } +// Latin honors count only as many electives as the program requires, the +// earliest first (UPLB catalog, Graduation with Honors). Returns the passed +// rows that fill no slot, which the honors GWA leaves out. A program with no +// elective slots returns none, since its data cannot tell extra from missing. +function surplusElectives(courses, passedRows, substitutions = {}) { + if (!courses.some(c => c.genericRequirement === 'elective')) return []; + const used = new Set(fillRequirementSlots(courses, passedRows, substitutions).values()); + const named = new Set(courses.filter(c => !c.genericRequirement).map(c => normalizeCourseCode(c.code))); + return passedRows.filter(r => !used.has(r) && !named.has(normalizeCourseCode(r.code)) && + !isNonGwaCourseCode(r.code) && r.units > 0); +} + // The planner's course list: checklist rows for the track and specialization // plus GE, HK, NSTP and free elective slots, with catalog units, offerings and // prerequisites. @@ -169,5 +181,5 @@ function gwaOutlook(gwa, units, left, target) { } if (typeof module !== 'undefined' && module.exports) { - module.exports = { gradeResult, amisTermToAbs, removalKey, applyRemovals, amisCourses, plannerCourseList, fillRequirementSlots, remainingRequirements, gwaOutlook, LATIN_HONORS }; + module.exports = { gradeResult, amisTermToAbs, removalKey, applyRemovals, amisCourses, plannerCourseList, fillRequirementSlots, surplusElectives, remainingRequirements, gwaOutlook, LATIN_HONORS }; }