(function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.ISMIStools = Object.assign(root.ISMIStools || {}, factory()); } })(typeof self !== 'undefined' ? self : this, function () { 'use strict'; function cleanText(s) { return String(s == null ? '' : s) .replace(/<[^>]+>/g, ' ') .replace(/ /gi, ' ') .replace(/\s+/g, ' ') .trim(); } function extractRows(html) { const rows = []; const trRe = /]*>([\s\S]*?)<\/tr>/gi; let trMatch; while ((trMatch = trRe.exec(html)) !== null) { const rowHtml = trMatch[1]; if (/]*>([\s\S]*?)<\/td>/gi; let tdMatch; while ((tdMatch = tdRe.exec(rowHtml)) !== null) { tds.push(cleanText(tdMatch[1])); } rows.push(tds); } return rows; } function parseStudyLoadTable(html) { return extractRows(html) .filter((tds) => tds.length >= 6) .map((tds) => ({ group: tds[0], courseCode: tds[1], schedule: tds[2], faculty: tds[3], units: tds[4], status: tds[5] })); } function parseProfilePage(html) { const result = {}; const h3Match = /]*>([\s\S]*?)<\/h3>/i.exec(html); const bMatch = (h3Match && /([\s\S]*?)<\/b>/i.exec(h3Match[1])) || /([\s\S]*?)<\/b>/i.exec(html); if (bMatch) result.program = cleanText(bMatch[1]); const smallMatch = /([\s\S]*?)<\/small>/i.exec(html); if (smallMatch) { const lines = smallMatch[1].split(//i).map(cleanText).filter(Boolean); result.semester = lines[lines.length - 1] || ''; } const kvRe = /]*>([\s\S]*?)<\/td>\s*]*class=["'][^"']*\bbold\b[^"']*["'][^>]*>([\s\S]*?)<\/td>/gi; let m; while ((m = kvRe.exec(html)) !== null) { const key = cleanText(m[1]); const val = cleanText(m[2]); if (/full name/i.test(key)) result.fullName = val; else if (/student id/i.test(key)) result.studentId = val; else if (/year level/i.test(key)) result.yearLevel = val; else if (/student type/i.test(key)) result.studentType = val; } return result; } function parseOfferedCourses(html) { return extractRows(html) .filter((tds) => tds.length >= 2) .map((tds) => ({ courseCode: tds[0].replace(/\s*-\s*Group\s*\d+$/i, '').trim(), description: tds[1] })); } // ---- Year standing helpers ---- // Parse the View Grades page into an ordered list of academic terms: // [{ semester: 'FIRST'|'SECOND'|'SUMMER', year: 2024 }, ...] // Terms appear in chronological order in the page. Regular semesters appear // as "FIRST SEMESTER - YYYY"; summer appears as "SUMMER - YYYY". function parseGradeHistory(html) { const history = []; const re = /\b(FIRST|SECOND)\s+SEMESTER\s*-\s*(\d{4})\b|\bSUMMER\s*-\s*(\d{4})\b/gi; let m; while ((m = re.exec(html)) !== null) { if (m[1]) { history.push({ semester: m[1].toUpperCase(), year: parseInt(m[2], 10) }); } else { history.push({ semester: 'SUMMER', year: parseInt(m[3], 10) }); } } return history; } // 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); const yearsBefore = distinctYears.filter((y) => y < year).length; const level = Math.min(yearsBefore + 1, distinctYears.length); return Number.isFinite(level) && level >= 1 ? level : null; } // Parse the Student Prospectus into per-year unit totals. Each table is // preceded by a "Year Level: N" heading. Returns [{ year, units }]. function parseProspectusYearUnits(html) { const segs = String(html || '').split(/(Year Level:\s*\d)/i); const yearUnits = {}; let cur = null; for (let i = 0; i < segs.length; i++) { const s = segs[i]; const ym = /Year Level:\s*(\d+)/i.exec(s); if (ym) { cur = parseInt(ym[1], 10); continue; } if (cur == null) continue; const tm = /]*>([\s\S]*?)<\/table>/i.exec(s); if (!tm) continue; const rows = extractRows(tm[1]); let units = 0; for (const r of rows) { if (r.length >= 3 && /^[A-Z]/.test(r[0]) && /[\d.]+/.test(r[2])) { const u = parseFloat(String(r[2]).replace(/[^\d.]/g, '')); if (Number.isFinite(u)) units += u; } } yearUnits[cur] = (yearUnits[cur] || 0) + units; cur = null; // only the immediately-following table belongs to this heading } const years = Object.keys(yearUnits).map(Number).sort((a, b) => a - b); return years.map((y) => ({ year: y, units: yearUnits[y] })); } // Parse the Lacking Subjects page for total remaining units. function parseLackingSubjectsUnits(html) { return parseLackingSubjects(html).units; } function parseLackingSubjects(html) { const rows = extractRows(String(html || '')); let units = 0; const courses = []; for (const r of rows) { if (r.length >= 3 && /^[A-Z]/.test(r[0]) && /[\d.]+/.test(r[2])) { const u = parseFloat(String(r[2]).replace(/[^\d.]/g, '')); if (Number.isFinite(u)) { units += u; courses.push({ courseCode: r[0], description: r[1], units: String(u) }); } } } return { units: units.toFixed(2), courses }; } // Year standing from units: finished = totalProspectusUnits - lackingUnits; // standing = first N in [1..maxYear] where finished < cumulativeUnits(N); // if finished passes every threshold, standing = maxYear (program length). // yearUnits is the array [{year, units}] from parseProspectusYearUnits. function computeYearStandingFromUnits(yearUnits, lackingUnits) { const yrs = (yearUnits || []).slice().sort((a, b) => a.year - b.year); if (!yrs.length) return null; const total = yrs.reduce((s, y) => s + y.units, 0); const finished = total - (parseFloat(lackingUnits) || 0); if (finished < 0) return yrs[0].year; let cum = 0; for (const y of yrs) { cum += y.units; if (finished < cum) return y.year; } return yrs[yrs.length - 1].year; // passed everything -> final level } function filterStudyLoadCourses(courses) { return (courses || []).filter( (c) => String(c.status || '').trim().toUpperCase() !== 'DISSOLVED' ); } function sumUnits(courses) { return (courses || []) .reduce((sum, c) => sum + (parseFloat(c.units) || 0), 0) .toFixed(2); } return { parseStudyLoadTable, parseProfilePage, parseOfferedCourses, filterStudyLoadCourses, sumUnits, parseGradeHistory, yearLevelForSemester, parseProspectusYearUnits, parseLackingSubjects, parseLackingSubjectsUnits, computeYearStandingFromUnits }; });