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,139 @@
class UndergraduateActivity {
UndergraduateActivity(
this.teacher,
this.name,
this.courseCode,
this.room,
this.indices,
this.weeks,
);
final String teacher, name, courseCode, room;
final Set<int> indices, weeks;
bool get cancelled => room == '停课';
}
List<UndergraduateActivity> parseActivityData(
String script, {
required int unitCount,
}) {
if (unitCount != 12) throw const FormatException('本科节次结构无效');
final matches = RegExp(
r'new\s+TaskActivity\s*\((.*?)\)\s*;',
dotAll: true,
).allMatches(script).toList();
final result = <UndergraduateActivity>[];
for (var i = 0; i < matches.length; i++) {
final m = matches[i], args = _strings(m[1]!);
if (args.length != 7) throw const FormatException('课程活动字段不完整');
final suffix = RegExp(r'\(([A-Za-z0-9_.-]+)\)$').firstMatch(args[3]);
if (suffix == null || args[1].trim().isEmpty || args[5].trim().isEmpty)
throw const FormatException('教师、教室或课程序号缺失');
final bitmap = args[6];
if (!RegExp(r'^0[01]+$').hasMatch(bitmap) ||
bitmap.length > 64 ||
(bitmap.length > 31 && bitmap.substring(31).contains('1')))
throw const FormatException('上课周次超出1–30周');
final weeks = {
for (var w = 1; w < bitmap.length && w <= 30; w++)
if (bitmap[w] == '1') w,
};
if (weeks.isEmpty) throw const FormatException('课程周次为空');
var end = i + 1 < matches.length ? matches[i + 1].start : script.length;
final finish = script.indexOf('table0.marshalTable', m.end);
if (finish >= 0 && finish < end) end = finish;
final body = script.substring(m.end, end);
final indices = <int>{};
final assignments = RegExp(r'\bindex\s*=\s*([^;]+);')
.allMatches(body)
.toList();
final appends = RegExp(
r'table\d+\.activities\[index\]\s*\[[^;]+?\]\s*=\s*activity\s*;',
).allMatches(body).length;
if (assignments.isEmpty || assignments.length != appends)
throw const FormatException('课程节次赋值不完整');
for (final assignment in assignments) {
final expression = RegExp(r'^\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)\s*$')
.firstMatch(assignment[1]!);
if (expression == null) throw const FormatException('不支持的节次表达式');
final day = int.parse(expression[1]!), period = int.parse(expression[2]!);
if (day > 6 || period >= unitCount)
throw const FormatException('课程星期或节次无效');
indices.add(day * unitCount + period);
}
result.add(
UndergraduateActivity(
args[1].trim(),
args[3].substring(0, suffix.start).trim(),
suffix[1]!,
args[5].trim(),
indices,
weeks,
),
);
}
return result;
}
/// Reads only quoted JS literals; expressions and calls are never evaluated.
List<String> _strings(String input) {
final out = <String>[];
var p = 0;
void spaces() {
while (p < input.length && RegExp(r'\s').hasMatch(input[p])) {
p++;
}
}
while (true) {
spaces();
if (p >= input.length) break;
final quote = input[p++];
if (quote != '"' && quote != "'") throw const FormatException('活动参数必须为字符串');
final b = StringBuffer();
var closed = false;
while (p < input.length) {
var c = input[p++];
if (c == quote) {
closed = true;
break;
}
if (c == '\\') {
if (p >= input.length) throw const FormatException('字符串转义不完整');
c = input[p++];
const escapes = {
'n': '\n',
'r': '\r',
't': '\t',
'b': '\b',
'f': '\f',
'v': '\u000b',
'0': '\u0000',
};
if (c == 'u' || c == 'x') {
final count = c == 'u' ? 4 : 2;
if (p + count > input.length) throw const FormatException('字符串转义无效');
final hex = input.substring(p, p + count);
if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(hex))
throw const FormatException('字符串转义无效');
b.writeCharCode(int.parse(hex, radix: 16));
p += count;
} else if (escapes.containsKey(c)) {
b.write(escapes[c]);
} else if (const ['"', "'", '\\', '/'].contains(c)) {
b.write(c);
} else {
throw const FormatException('不支持的字符串转义');
}
} else {
if (c == '\n' || c == '\r') throw const FormatException('字符串未闭合');
b.write(c);
}
}
if (!closed) throw const FormatException('字符串未闭合');
out.add(b.toString());
spaces();
if (p == input.length) break;
if (input[p++] != ',') throw const FormatException('活动字段分隔无效');
}
return out;
}