feat: add validated semester and lesson calculations

This commit is contained in:
Codex
2026-10-12 23:13:02 +08:00
parent 36d18f029f
commit ae8e347e49
36 changed files with 1522 additions and 1 deletions
@@ -0,0 +1,117 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:timezone/data/latest.dart' as tzdata;
import 'package:hepan_everyday/domain/schedule_engine.dart';
Course course({
String id = 'math',
int day = 3,
int first = 1,
int last = 1,
Set<int> weeks = const {1},
}) => Course(
id: id,
name: '矩阵论',
teacher: '示例教师',
room: '示例教室',
weekday: day,
startPeriod: first,
endPeriod: last,
weeks: weeks,
source: CourseSource.file,
);
void main() {
setUpAll(tzdata.initializeTimeZones);
final term = Semester(startDate: DateTime(2026, 9, 9), totalWeeks: 5);
test(
'all eleven lessons produce the specified Shanghai start and end times',
() {
const starts = [
510,
560,
620,
670,
870,
920,
980,
1030,
1170,
1220,
1270,
];
const ends = [555, 605, 665, 715, 915, 965, 1025, 1075, 1215, 1265, 1315];
for (var p = 1; p <= 11; p++) {
final result = ScheduleEngine.occurrences([
course(first: p, last: p),
], term);
expect(result, hasLength(1));
expect(
result.single.start.hour * 60 + result.single.start.minute,
starts[p - 1],
);
expect(
result.single.end.hour * 60 + result.single.end.minute,
ends[p - 1],
);
}
},
);
test('term beginning Wednesday suppresses earlier Monday', () {
expect(
ScheduleEngine.occurrences([
course(day: 1, weeks: {1, 2}),
], term).map((e) => e.start.day).toList(),
[14],
);
expect(ScheduleEngine.weekNumber(term, DateTime(2026, 9, 9)), 1);
expect(ScheduleEngine.weekNumber(term, DateTime(2026, 9, 14)), 2);
expect(ScheduleEngine.weekNumber(term, DateTime(2026, 10, 12)), isNull);
});
test('odd and nonconsecutive weeks create only matching dates', () {
expect(
ScheduleEngine.occurrences([
course(weeks: {1, 3, 5}),
], term).map((e) => '${e.start.month}/${e.start.day}').toList(),
['9/9', '9/23', '10/7'],
);
});
test('Sunday and consecutive periods use one correct occurrence', () {
final events = ScheduleEngine.occurrences([
course(day: 7, first: 2, last: 3),
], term);
expect(events, hasLength(1));
expect(events.single.start.day, 13);
expect(events.single.start.hour, 9);
expect(events.single.start.minute, 20);
expect(events.single.end.hour, 11);
expect(events.single.end.minute, 5);
});
test(
'school lesson time has a fixed UTC instant regardless of device zone',
() {
final events = ScheduleEngine.occurrences([course()], term);
expect(events, hasLength(1));
expect(events.single.start.toUtc(), DateTime.utc(2026, 9, 9, 0, 30));
},
);
test('overlap keeps simultaneous courses but excludes adjacent courses', () {
final events = ScheduleEngine.occurrences([
course(id: 'a', first: 1, last: 2),
course(id: 'b', first: 2, last: 3),
course(id: 'c', first: 4, last: 4),
], term);
expect(events, hasLength(3));
expect(ScheduleEngine.overlaps(events[0], events[1]), isTrue);
expect(ScheduleEngine.overlaps(events[1], events[2]), isFalse);
});
test('invalid lesson, weekday and week values are rejected', () {
expect(() => course(first: 0), throwsArgumentError);
expect(() => course(last: 12), throwsArgumentError);
expect(() => course(day: 8), throwsArgumentError);
expect(() => course(weeks: {0}), throwsArgumentError);
expect(
() => Semester(startDate: DateTime(2026), totalWeeks: 31),
throwsArgumentError,
);
});
}