feat: parse undergraduate timetable activity data

This commit is contained in:
Codex
2026-10-14 23:32:27 +08:00
parent 141436f545
commit 17e4903399
5 changed files with 548 additions and 0 deletions
@@ -0,0 +1,68 @@
<input name="semester.id" value="523"><table id="manualArrangeCourseTable"><tr><th>节次</th><th>星期一</th><th>星期二</th><th>星期三</th><th>星期四</th><th>星期五</th><th>星期六</th><th>星期日</th></tr></table><table id="gridFixture"><tr><th>序号</th><th>课程代码</th><th>课程名称</th><th>课程类别</th><th>学分</th><th>课程序号</th><th>教师</th><th>职称</th><th>教材信息</th><th>选课备注</th></tr><tr><td>1</td><td>C1</td><td>课程1</td><td>专业课</td><td>3</td><td>C1.01</td><td>教师1</td><td></td><td></td><td>上课时间:19:00-22:00</td></tr><tr><td>2</td><td>C2</td><td>课程2</td><td>专业课</td><td>3</td><td>C2.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>3</td><td>C3</td><td>课程3</td><td>专业课</td><td>3</td><td>C3.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>4</td><td>C4</td><td>课程4</td><td>专业课</td><td>3</td><td>C4.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>5</td><td>C5</td><td>课程5</td><td>专业课</td><td>3</td><td>C5.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>6</td><td>C6</td><td>课程6</td><td>专业课</td><td>3</td><td>C6.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>7</td><td>C7</td><td>课程7</td><td>专业课</td><td>3</td><td>C7.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>8</td><td>C8</td><td>课程8</td><td>专业课</td><td>3</td><td>C8.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>9</td><td>C9</td><td>课程9</td><td>专业课</td><td>3</td><td>C9.01</td><td>教师1</td><td></td><td></td><td></td></tr><tr><td>10</td><td>C10</td><td>课程10</td><td>专业课</td><td>3</td><td>C10.01</td><td>教师1</td><td></td><td></td><td></td></tr></table><script>var table0=new CourseTable(2026,84);var unitCount = 12;
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室1","00000000000001000000000000000000000000000000000000000");
index = 2*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
index = 2*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室1","00000000001101000000000000000000000000000000000000000");
index = 4*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
index = 4*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室2","01110011110010000000000000000000000000000000000000000");
index = 4*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
index = 4*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T1","教师1","COURSE1","课程1(C1.01)","R1","教室2","01111111111110000000000000000000000000000000000000000");
index = 2*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
index = 2*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T2","教师2","COURSE2","课程2(C2.01)","R2","教室3","01111000000000000000000000000000000000000000000000000");
index = 1*unitCount+8; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+9; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+10; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+11; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T2","教师2","COURSE2","课程2(C2.01)","R2","教室4","00000111100000000000000000000000000000000000000000000");
index = 1*unitCount+8; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+9; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+10; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+11; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T3","教师3","COURSE3","课程3(C3.01)","R3","教室5","01111011111110000000000000000000000000000000000000000");
index = 3*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
index = 3*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T3","教师3","COURSE3","课程3(C3.01)","R3","教室5","01111111111111000000000000000000000000000000000000000");
index = 1*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T4","教师4","COURSE4","课程4(C4.01)","R4","教室6","01111011111000000000000000000000000000000000000000000");
index = 3*unitCount+0; table0.activities[index][table0.activities[index].length]=activity;
index = 3*unitCount+1; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T4","教师4","COURSE4","课程4(C4.01)","R4","教室6","01111111111100000000000000000000000000000000000000000");
index = 1*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T5","教师5","COURSE5","课程5(C5.01)","R5","教室7","01111111111111111110000000000000000000000000000000000");
index = 0*unitCount+8; table0.activities[index][table0.activities[index].length]=activity;
index = 0*unitCount+9; table0.activities[index][table0.activities[index].length]=activity;
index = 0*unitCount+10; table0.activities[index][table0.activities[index].length]=activity;
index = 0*unitCount+11; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T6","教师6、教师7","COURSE6","课程6(C6.01)","R6","教室8","01111111111110000000000000000000000000000000000000000");
index = 2*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
index = 2*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T6","教师6、教师7","COURSE6","课程6(C6.01)","R6","教室8","01111111111110000000000000000000000000000000000000000");
index = 0*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
index = 0*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T7","教师8、教师9、教师10、教师7、教师11、教师12、教师13、教师14、教师15","COURSE7","课程7(C7.01)","R7","教室9","00001011111111111000000000000000000000000000000000000");
index = 3*unitCount+6; table0.activities[index][table0.activities[index].length]=activity;
index = 3*unitCount+7; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T8","教师16","COURSE8","课程8(C8.01)","R8","教室10","01111101111110000000000000000000000000000000000000000");
index = 1*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+6; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+7; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T8","教师16","COURSE8","课程8(C8.01)","R8","停课","00000010000000000000000000000000000000000000000000000");
index = 1*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+6; table0.activities[index][table0.activities[index].length]=activity;
index = 1*unitCount+7; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T9","教师17","COURSE9","课程9(C9.01)","R9","教室12","00000000000000010000000000000000000000000000000000000");
index = 6*unitCount+2; table0.activities[index][table0.activities[index].length]=activity;
index = 6*unitCount+3; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T10","教师18","COURSE10","课程10(C10.01)","R10","教室13","01111111111110000000000000000000000000000000000000000");
index = 0*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
index = 0*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;
activity = new TaskActivity("T10","教师18","COURSE10","课程10(C10.01)","R10","教室13","01111111111110000000000000000000000000000000000000000");
index = 2*unitCount+4; table0.activities[index][table0.activities[index].length]=activity;
index = 2*unitCount+5; table0.activities[index][table0.activities[index].length]=activity;</script>
@@ -0,0 +1,144 @@
import 'dart:io';
import 'dart:convert';
import 'package:html/parser.dart' as html;
import 'package:hepan_everyday/features/school/undergraduate/activity_data.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hepan_everyday/features/school/undergraduate/timetable_adapter.dart';
import 'package:hepan_everyday/data/schedule_file_import.dart';
import 'package:hepan_everyday/domain/school_identity.dart';
void main() {
final uri = Uri.parse(
'https://eams.uestc.edu.cn/eams/courseTableForStd.action',
);
String fixture() =>
File('test/fixtures/school/undergraduate-timetable.html')
.readAsStringSync();
test('undergraduate activity data preserves teachers rooms weeks and period twelve', () {
final draft = UndergraduateTimetableAdapter.extract(fixture(), uri);
expect(draft.schoolTerm!.id, 523);
expect(draft.schoolTerm!.school, SupportedSchool.uestcUndergraduate);
final activities = parseActivityData(
html.parse(fixture()).querySelector('script')!.text,
unitCount: 12,
);
final expected = <String>{};
for (final a in activities.where((a) => !a.cancelled)) {
for (final index in a.indices) {
final cancelled = activities
.where(
(c) =>
c.cancelled &&
c.courseCode == a.courseCode &&
c.indices.contains(index),
)
.expand((c) => c.weeks)
.toSet();
for (final week in a.weeks.difference(cancelled)) {
expected.add(
jsonEncode([
a.courseCode.split('.').first,
a.teacher,
a.room,
index,
week,
]),
);
}
}
}
final actual = {
for (final c in draft.courses)
for (var p = c.startPeriod; p <= c.endPeriod; p++)
for (final w in c.weeks)
jsonEncode([
c.courseCode,
c.teacher,
c.room,
(c.weekday - 1) * 12 + p - 1,
w,
]),
};
expect(
actual,
expected,
reason: 'continuous grouping must preserve every original active index and week',
);
expect(draft.courses, hasLength(18));
expect(draft.courses.map((c) => c.courseCode).toSet(), hasLength(10));
expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3));
expect(draft.courses.any((c) => c.room == '停课'), isFalse);
expect(draft.courses.any((c) => c.teacher.contains('教师9')), isTrue);
expect(draft.courses.any((c) => c.notes.contains('19:00-22:00')), isTrue);
expect(ScheduleFileImport.parse(fixture()).schoolTerm, draft.schoolTerm);
expect(
UndergraduateTimetableAdapter.extract(
fixture(),
uri,
).courses.map((c) => c.id),
draft.courses.map((c) => c.id),
);
});
test(
'invalid or incomplete activity data never becomes an empty timetable',
() {
for (final html in [
'<html>登录</html>',
fixture().replaceAll('unitCount = 12', 'unitCount = 11'),
fixture().replaceFirst(
RegExp(r'index = \d+\s*\*\s*unitCount\s*\+\s*\d+'),
'index = unknown()',
),
fixture().replaceFirst('value="523"', 'value="504"'),
fixture().replaceFirst('"教师1"', '""'),
]) {
expect(
() => UndergraduateTimetableAdapter.extract(html, uri),
throwsFormatException,
);
}
expect(
() => UndergraduateTimetableAdapter.extract(
fixture(),
Uri.parse('https://idas.uestc.edu.cn/'),
),
throwsFormatException,
);
},
);
test('explicit empty pages require recognized table and actual term', () {
final empty =
'<input name="semester.id" value="523"><table id="manualArrangeCourseTable"><tr><td>暂无课程</td></tr></table>';
expect(
UndergraduateTimetableAdapter.extract(empty, uri).explicitlyEmpty,
isTrue,
);
expect(
() => UndergraduateTimetableAdapter.extract(
empty.replaceAll('暂无课程', ''),
uri,
),
throwsFormatException,
);
});
final original =
Platform.environment['UESTC_UNDERGRADUATE_TIMETABLE_FIXTURE'];
if (original != null)
test(
'actual supplied undergraduate HTML has complete normalized arrangements',
() {
final draft = UndergraduateTimetableAdapter.extract(
File(original).readAsStringSync(),
uri,
);
expect(draft.courses, hasLength(18));
expect(draft.courses.map((c) => c.courseCode).toSet(), hasLength(10));
expect(draft.courses.where((c) => c.endPeriod == 12), hasLength(3));
expect(
draft.courses.every((c) => c.teacher.isNotEmpty && c.room.isNotEmpty),
isTrue,
);
},
);
}