From 90e4107bf0eebc93da535c90228730c93f1c9315 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Thu, 20 Aug 2026 08:54:55 +0800 Subject: [PATCH] fix(year-level): prefer units-passed standing over calendar-year history count The grade-history method counted distinct academic years, which over-advanced standing for students who repeated/failed a year (e.g. showing Year 5 when the student is genuinely Year 4, since a partial 2025 year didn't advance them). Reorder the hybrid so the UNITS math (total prospectus units minus still-lacking units = units actually passed) is primary, and View Grades history is only a fallback when units data is unavailable. --- content.js | 23 ++++++++++++++--------- parsers.js | 13 +++++++------ 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/content.js b/content.js index f3a444c..520130a 100644 --- a/content.js +++ b/content.js @@ -179,15 +179,10 @@ const yearNum = parseInt(year, 10); - // 1. View Grades history matching. - const gradesHtml = await fetchText('/ViewGrades'); - if (gradesHtml) { - const history = ISMIStools.parseGradeHistory(gradesHtml); - const level = ISMIStools.yearLevelForSemester(history, yearNum); - if (level != null) return String(level); - } - - // 2. Units math: prospectus total minus lacking units. + // 1. UNITS MATH — primary. Standing from units actually passed: + // total prospectus units minus still-lacking units. This reflects real + // earned units and is correct even for students who repeated or failed + // a year (the naive grade-history count would over-advance them). const [prospectusHtml, lackingHtml] = await Promise.all([ fetchText('/prospectus/StudentProspectus'), fetchText('/Extras/ViewLackingSubjectsStudent1') @@ -199,6 +194,16 @@ if (level != null) return String(level); } + // 2. GRADE HISTORY — fallback only when the units path is unavailable. + // It counts distinct academic years, so it is only an approximation for + // the printed semester and is not used when units data is present. + const gradesHtml = await fetchText('/ViewGrades'); + if (gradesHtml) { + const history = ISMIStools.parseGradeHistory(gradesHtml); + const level = ISMIStools.yearLevelForSemester(history, yearNum); + if (level != null) return String(level); + } + // 3. Profile-scraped value as a last resort. if (header.yearLevel) return String(header.yearLevel); return null; diff --git a/parsers.js b/parsers.js index 5e303bf..cc7c1c2 100644 --- a/parsers.js +++ b/parsers.js @@ -101,12 +101,13 @@ return history; } - // Standing for the study load printed at (semester, year): it is the number - // of distinct academic years strictly before that term's year + 1. SUMMER of - // year Y is the same standing as the rest of year Y (the student has just - // finished the preceding level during Y's regular semesters). Clamped to at - // most the number of distinct years in history (a student cannot be past the - // final level recorded). + // Standing for a study load printed in `year` from the View Grades history. + // It is the number of distinct academic years strictly before that year + 1 + // (FIRST/SECOND/SUMMER of a year share the same standing), clamped to at most + // the number of distinct years in history. NOTE: this is only an + // approximation for the printed semester — it counts calendar years present, + // not units passed — so a repeated/failed year can over-advance. It is used + // only as a fallback when the units-based standing is unavailable. function yearLevelForSemester(history, year) { if (!history || !history.length || !Number.isFinite(year)) return null; const distinctYears = [...new Set(history.map((t) => t.year))].sort((a, b) => a - b);