Files
TPSchedule/mobile/lib/features/school/undergraduate/activity_data.dart
T

178 lines
5.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class UndergraduateActivityException extends FormatException {
const UndergraduateActivityException(
this.code,
String message,
this.activityNumber,
) : super(message);
final String code;
final int activityNumber;
}
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();
if (RegExp(r'\bnew\s+TaskActivity\s*\(').allMatches(script).length !=
matches.length) {
throw const FormatException('课程活动声明不完整');
}
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) {
throw UndergraduateActivityException(
'course_sequence_invalid',
'课程序号格式无法识别,请附带网页反馈',
i + 1,
);
}
if (args[1].trim().isEmpty) {
throw UndergraduateActivityException(
'teacher_missing',
'课程活动未提供教师信息,请附带网页反馈',
i + 1,
);
}
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 appendPattern = RegExp(
r'table\d+\.activities\[index\]\s*\[[^;]+?\]\s*=\s*activity\s*;',
);
final appends = appendPattern.allMatches(body).length;
final unconsumed = body
.replaceAll(appendPattern, '')
.replaceAll(RegExp(r'\bindex\s*=\s*[^;]+;'), '');
if (RegExp(r'\btable\d+\.activities\s*\[|\bindex\s*=')
.hasMatch(unconsumed)) {
throw const FormatException('存在无法识别的课程节次赋值');
}
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().isEmpty ? '教室待定' : 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;
}