getGradeRanking method
Fetches grade ranking data for all semesters.
Returns a list of GradeRankingDto ordered from most recent to oldest, each containing ranking positions at class, group, and department levels.
Implementation
Future<List<GradeRankingDto>> getGradeRanking() async {
final response = await _studentQueryDio.get('QryRank.jsp');
final document = parse(response.data);
final table = document.querySelector('table');
if (table == null) return [];
final semesterPattern = RegExp(r'(\d+)\s*-\s*(\d+)');
final results = <GradeRankingDto>[];
SemesterDto? currentSemester;
var currentEntries = <GradeRankingEntryDto>[];
// Rows are either: 8 cells (semester start + data), 7 cells (continuation),
// or other (header/notice — skip).
// Semester cell uses rowspan="3" to span its 3 ranking type rows.
for (final row in table.querySelectorAll('tr')) {
final cells = row.querySelectorAll('td').toList();
int dataStart;
if (cells.length == 8) {
// New semester with ranking data
if (currentSemester != null && currentEntries.isNotEmpty) {
results.add((semester: currentSemester, entries: currentEntries));
currentEntries = [];
}
// Cell contains "113 - 2<br>2025 - Spring" — use first text node
final semesterText = cells[0].nodes
.where((node) => node.nodeType == Node.TEXT_NODE)
.firstOrNull
?.text;
final match = semesterPattern.firstMatch(semesterText ?? '');
if (match == null) continue;
currentSemester = (
year: int.parse(match.group(1)!),
term: int.parse(match.group(2)!),
);
dataStart = 1;
} else if (cells.length == 7) {
dataStart = 0;
} else {
continue;
}
// cells[dataStart]: ranking type, +1/+2: semester rank/total,
// +3: semester percentage (skip), +4/+5: grand total rank/total,
// +6: grand total percentage (skip)
final type = _parseRankingType(cells[dataStart].text);
if (type == null) continue;
final semesterRank = int.tryParse(cells[dataStart + 1].text.trim());
final semesterTotal = int.tryParse(cells[dataStart + 2].text.trim());
final grandTotalRank = int.tryParse(cells[dataStart + 4].text.trim());
final grandTotalTotal = int.tryParse(cells[dataStart + 5].text.trim());
if (semesterRank == null ||
semesterTotal == null ||
grandTotalRank == null ||
grandTotalTotal == null) {
continue;
}
currentEntries.add((
type: type,
semesterRank: semesterRank,
semesterTotal: semesterTotal,
grandTotalRank: grandTotalRank,
grandTotalTotal: grandTotalTotal,
));
}
if (currentSemester != null && currentEntries.isNotEmpty) {
results.add((semester: currentSemester, entries: currentEntries));
}
return results;
}