refactor: finish arranger

This commit is contained in:
2025-09-07 16:19:11 +08:00
parent 9fb9359c11
commit 2982583048
5 changed files with 218 additions and 130 deletions

View File

@ -1,140 +1,129 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using HFUTCourseSimulation.Kernel.Data.Built;
using HFUTCourseSimulation.Util;
namespace HFUTCourseSimulation.Kernel {
/// <summary>
/// 一个标识符,用于指代学期中指定周的指定星期的指定节次
/// </summary>
internal class ArrangerSpot : IEquatable<ArrangerSpot> {
public ArrangerSpot(int week, int day, int index) {
this.week = week;
this.day = day;
this.index = index;
}
///// <summary>
///// 一个标识符,用于指代学期中指定周的指定星期的指定节次
///// </summary>
//internal class ArrangerSpot : IEquatable<ArrangerSpot> {
// public ArrangerSpot(int week, int day, int index) {
// this.week = week;
// this.day = day;
// this.index = index;
// }
/// <summary>
/// 教学周
/// </summary>
public int week;
/// <summary>
/// 星期几
/// </summary>
public int day;
/// <summary>
/// 节次
/// </summary>
public int index;
// /// <summary>
// /// 教学周
// /// </summary>
// public int week;
// /// <summary>
// /// 星期几
// /// </summary>
// public int day;
// /// <summary>
// /// 节次
// /// </summary>
// public int index;
public override bool Equals(object obj) {
return Equals(obj as ArrangerSpot);
}
// public override bool Equals(object obj) {
// return Equals(obj as ArrangerSpot);
// }
public bool Equals(ArrangerSpot other) {
return !(other is null) &&
week == other.week &&
day == other.day &&
index == other.index;
}
// public bool Equals(ArrangerSpot other) {
// return !(other is null) &&
// week == other.week &&
// day == other.day &&
// index == other.index;
// }
public override int GetHashCode() {
int hashCode = -206993699;
hashCode = hashCode * -1521134295 + week.GetHashCode();
hashCode = hashCode * -1521134295 + day.GetHashCode();
hashCode = hashCode * -1521134295 + index.GetHashCode();
return hashCode;
}
// public override int GetHashCode() {
// int hashCode = -206993699;
// hashCode = hashCode * -1521134295 + week.GetHashCode();
// hashCode = hashCode * -1521134295 + day.GetHashCode();
// hashCode = hashCode * -1521134295 + index.GetHashCode();
// return hashCode;
// }
public static bool operator ==(ArrangerSpot left, ArrangerSpot right) {
return EqualityComparer<ArrangerSpot>.Default.Equals(left, right);
}
// public static bool operator ==(ArrangerSpot left, ArrangerSpot right) {
// return EqualityComparer<ArrangerSpot>.Default.Equals(left, right);
// }
public static bool operator !=(ArrangerSpot left, ArrangerSpot right) {
return !(left == right);
}
}
// public static bool operator !=(ArrangerSpot left, ArrangerSpot right) {
// return !(left == right);
// }
//}
/// <summary>
/// 课程安排器。
/// 负责将课程安排到每周之中,并提示用户错误等各种信息。
/// </summary>
public class Arranger {
public Arranger(Kernel.Data.Storage.Semester semester) {
this.semester = semester;
arrangeMap = new Dictionary<ArrangerSpot, int>();
reporter = new Reporter();
}
private Kernel.Data.Storage.Semester semester;
private Dictionary<ArrangerSpot, int> arrangeMap;
private Reporter reporter;
/// <summary>
/// 获取汇报器用于查看安排日志
/// </summary>
/// <returns></returns>
public Reporter GetReporter() {
return reporter;
}
public static class Arranger {
/// <summary>
/// 开始安排课程
/// </summary>
/// <returns>安排好的用于呈现的课程。如果安排失败则为null查看汇报器来了解具体差错</returns>
public Kernel.Data.Presentation.Semester Arrange() {
reporter.Clear();
arrangeMap.Clear();
return ConcludeCourses();
public static Data.Presentation.Semester Arrange(Data.Storage.Semester semester, Reporter reporter) {
var builtSemester = CheckSemester(semester, reporter);
if (builtSemester is null) return null;
var arrangeMap = ArrangeCourses(builtSemester, reporter);
var renderResult = ConcludeCourses(builtSemester, arrangeMap, reporter);
return renderResult;
}
#region
/// <summary>
/// 检查用户输入的学期数据是否正确
/// </summary>
/// <returns>如果检查无误返回检查的结否则返回null</returns>
private Semester CheckSemester() {
/// <returns>如果检查无误返回检查的结否则返回null</returns>
private static Data.Built.Semester CheckSemester(Data.Storage.Semester semester, Reporter reporter) {
// 检查Semester
reporter.Info($"正在检查学期");
if (semester.StartDate.DayOfWeek != DayOfWeek.Monday) {
reporter.Error($"起始日期{semester.StartDate:yyyy-MM-dd}不是星期一");
return null;
}
var rv = CheckInteger(semester.WeekCount, 1, 100, "周数", "0 < N");
var rv = CheckInteger(reporter, semester.WeekCount, 1, 100, "周数", "0 < N");
if (rv is null) return null;
int weekCount = rv.Value;
rv = CheckInteger(semester.IndexCount, 1, 100, "节次数", "0 < N");
rv = CheckInteger(reporter, semester.IndexCount, 1, 100, "节次数", "0 < N");
if (rv is null) return null;
int indexCount = rv.Value;
rv = CheckInteger(semester.BreakfastAt, 0, indexCount, "早餐时间点", "0 <= N <= 节次数");
rv = CheckInteger(reporter, semester.BreakfastAt, 0, indexCount, "早餐时间点", "0 <= N <= 节次数");
if (rv is null) return null;
int breakfastAt = rv.Value;
rv = CheckInteger(semester.LunchAt, breakfastAt, indexCount, "午餐时间点", "早餐时间点 <= N <= 节次数");
rv = CheckInteger(reporter, semester.LunchAt, breakfastAt, indexCount, "午餐时间点", "早餐时间点 <= N <= 节次数");
if (rv is null) return null;
int lunchAt = rv.Value;
rv = CheckInteger(semester.DinnerAt, lunchAt, indexCount, "晚餐时间点", "午餐时间点 <= N <= 节次数");
rv = CheckInteger(reporter, semester.DinnerAt, lunchAt, indexCount, "晚餐时间点", "午餐时间点 <= N <= 节次数");
if (rv is null) return null;
int dinnerAt = rv.Value;
var courses = new List<Course>();
var courses = new List<Data.Built.Course>();
foreach (var course in semester.Courses) {
// 检查课程(实际上只有安排需要检查)
reporter.Info($"正在检查课程:{course.Name}");
var schedules = new List<Schedule>();
var schedules = new List<Data.Built.Schedule>();
foreach (var schedule in course.Schedules) {
// 检查安排
var week = CheckIntegerCollection(schedule.Week, 1, weekCount, "周", "1 <= N <= 周数");
var day = CheckIntegerCollection(schedule.Day, 1, 7, "星期", "1 <= N <= 7");
var index = CheckIntegerCollection(schedule.Index, 1, indexCount, "节次", "1 <= N <= 节次数");
var week = CheckIntegerCollection(reporter, schedule.Week, 1, weekCount, "周", "1 <= N <= 周数");
var day = CheckIntegerCollection(reporter, schedule.Day, 1, 7, "星期", "1 <= N <= 7");
var index = CheckIntegerCollection(reporter, schedule.Index, 1, indexCount, "节次", "1 <= N <= 节次数");
// 如果格式均正确,就添加这个安排
if (!(week is null || day is null || index is null)) {
schedules.Add(new Schedule() {
schedules.Add(new Data.Built.Schedule() {
week = week,
day = day,
index = index
@ -143,7 +132,7 @@ namespace HFUTCourseSimulation.Kernel {
}
// OK插入课程
courses.Add(new Course() {
courses.Add(new Data.Built.Course() {
name = course.Name,
description = course.Description,
color = ColorTrans.ToStandardColorPair(course.Color),
@ -152,7 +141,7 @@ namespace HFUTCourseSimulation.Kernel {
}
// OK无误返回结果
return new Semester() {
return new Data.Built.Semester() {
startDate = semester.StartDate,
weekCount = weekCount,
indexCount = indexCount,
@ -168,8 +157,8 @@ namespace HFUTCourseSimulation.Kernel {
/// field_name是字段的名称field_require是字段的要求均用于用户提示。
/// 返回null表示检查失败否则返回检查好的值。
/// </summary>
private int? CheckInteger(string s, int min, int max, string field_name, string field_require) {
if (int.TryParse(semester.WeekCount, out int rv)) {
private static int? CheckInteger(Reporter reporter, string s, int min, int max, string field_name, string field_require) {
if (int.TryParse(s, out int rv)) {
if (rv < min || rv > max) {
reporter.Error($"{field_name}的值“{s}”超出范围。要求:{field_require}");
return null;
@ -192,7 +181,7 @@ namespace HFUTCourseSimulation.Kernel {
/// 检查整数集合的辅助函数即检查Schedule中的三个字段WeekDay和Index的格式。
/// 各类参数含义与上面函数相同,只不过是作用到每一项上。
/// </summary>
private IEnumerable<int> CheckIntegerCollection(string s, int min, int max, string field_name, string field_require) {
private static IEnumerable<int> CheckIntegerCollection(Reporter reporter, string s, int min, int max, string field_name, string field_require) {
// 两种整数集合所特有的字符常量,用于分割和分析
const char COMMA = ',';
const char DASH = '-';
@ -215,7 +204,7 @@ namespace HFUTCourseSimulation.Kernel {
split_char = COMMA;
break;
case IntegerCollectionKind.Range:
split_char = DASH;
split_char = DASH;
break;
default:
return null;
@ -240,63 +229,148 @@ namespace HFUTCourseSimulation.Kernel {
// 按整数集合类型进行收尾检查并返回
switch (kind) {
case IntegerCollectionKind.Individual:
return new IndividualInt(ints);
return new Data.Built.IndividualInt(ints);
case IntegerCollectionKind.Range:
if (ints.Count != 2) {
reporter.Error($"{field_name}的值“{s}”不是有效的整数范围表达式");
return null;
} else {
return new RangedInt(ints[0], ints[1]);
return new Data.Built.RangedInt(ints[0], ints[1]);
}
default:
return null;
}
}
#endregion
#region
/// <summary>
/// 安排课程
/// </summary>
/// <returns>安排无误返回true否则返回false</returns>
private bool ArrangeCourses() {
// Check it first
var rv = CheckSemester();
if (rv is null) return false;
/// <returns>返回安排好的课程表</returns>
private static int[,,] ArrangeCourses(Data.Built.Semester semester, Reporter reporter) {
// 创建课程表并预填充所有项为-1。
var arrangeMap = new int[semester.weekCount, 7, semester.indexCount];
for (var week = 0; week < semester.weekCount; ++week) {
for (var day = 0; day < 7; ++day) {
for (var index = 0; index < semester.indexCount; ++index) {
arrangeMap[week, day, index] = -1;
}
}
}
// 遍历所有课程安排开始排课
bool okey = true;
for (int course_index = 0; course_index < rv.courses.Count; ++course_index) {
var course = rv.courses[course_index];
// 排课冲突并不是严重错误,因此不需要强制退出
for (int course_index = 0; course_index < semester.courses.Count; ++course_index) {
var course = semester.courses[course_index];
foreach (var schedule in course.schedules) {
foreach (var week in schedule.week) {
foreach (var day in schedule.day) {
foreach (var index in schedule.index) {
var spot = new ArrangerSpot(week, day, index);
if (arrangeMap.TryGetValue(spot, out int occupied_course_index)) {
var occupied_course = rv.courses[occupied_course_index];
reporter.Error($"课程冲突:无法将{course.name}安排到周{week},星期{day},第{index}节。因为此处已被{occupied_course.name}占据");
okey = false;
// 获取课程占用情况并分析
var occupied_course_index = arrangeMap[week - 1, day - 1, index - 1];
if (occupied_course_index < 0) {
// 没有占用的课程,可以放心安排
arrangeMap[week - 1, day - 1, index - 1] = course_index;
} else {
arrangeMap.Add(spot, course_index);
// 发现课程占用汇报占用的课程并记录到flag
var occupied_course = semester.courses[occupied_course_index];
reporter.Error($"课程冲突:无法将{course.name}安排到周{week},星期{day},第{index}节。因为此处已被{occupied_course.name}占据");
}
}
}
}
}
}
return okey;
// 返回结果
return arrangeMap;
}
#endregion
#region 便
/// <summary>
/// 将安排好的课程总结为方便渲染的结果
/// </summary>
/// <returns></returns>
private Kernel.Data.Presentation.Semester ConcludeCourses() {
// Arrange it first
var rv = ArrangeCourses();
if (!rv) return null;
/// <returns>总结好的可用于渲染的结果</returns>
private static Data.Presentation.Semester ConcludeCourses(Data.Built.Semester semester, int[,,] arrangeMap, Reporter reporter) {
var dateCursor = semester.startDate;
return null;
var weeks = new List<Data.Presentation.Week>();
for (var week = 1; week <= semester.weekCount; ++week) {
var days = new List<Data.Presentation.Day>();
for (var day = 1; day <= 7; ++day) {
// 获取日期的字符串形式,并自增天数
var dateString = dateCursor.ToString("MM/dd");
dateCursor = dateCursor.AddDays(1);
// 获取这一日的课程
var lessons = new List<Data.Presentation.Lesson>();
Data.Presentation.Lesson current_lesson = null;
int previous_course_index = -1;
for (var index = 1; index <= semester.indexCount; ++index) {
// 比较上一个课程和当前课程的区别
var this_course_index = arrangeMap[week - 1, day - 1, index - 1];
if (this_course_index != previous_course_index) {
// 课程发生了变化,我们需要根据这种变化做出操作。
// 如果有上一任课程我们就把它push到列表中。
if (previous_course_index >= 0) {
lessons.Add(current_lesson);
current_lesson = null;
}
// 然后设置当前课程
var this_course = semester.courses[this_course_index];
current_lesson = new Data.Presentation.Lesson() {
name = this_course.name,
description = this_course.description,
color = this_course.color,
startIndex = index,
indexSpan = 1
};
} else {
// 课程未曾发生改变,如果当前有课程,我们只需要单纯地自增其节次跨度
if (previous_course_index >= 0) {
current_lesson.indexSpan++;
}
}
// 更新上一个课程的index
previous_course_index = this_course_index;
}
// 收尾处理最后一个课程
if (previous_course_index >= 0) {
lessons.Add(current_lesson);
current_lesson = null;
}
// 将这一日添加到这周中
days.Add(new Data.Presentation.Day() {
date = dateString,
lessons = lessons.ToImmutableList()
});
}
// 将这一星期添加教学周中
weeks.Add(new Data.Presentation.Week() {
days = days.ToImmutableList()
});
}
// 以教学周构建并返回学期
return new Data.Presentation.Semester() {
weekCount = semester.weekCount,
indexCount = semester.indexCount,
breakfastAt = semester.breakfastAt,
lunchAt = semester.lunchAt,
dinnerAt = semester.dinnerAt,
weeks = weeks.ToImmutableList()
};
}
#endregion
}
}

View File

@ -6,6 +6,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// 安排课程时使用的课表数据。
/// 仅在课表安排器中作为中间结构使用,其本质上为经过验证的用户输入的课表数据。
/// </summary>
namespace HFUTCourseSimulation.Kernel.Data.Built {
public class Semester {

View File

@ -5,6 +5,10 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// 用于最终呈现的排课完毕的课表数据。
/// 方便模拟器和图片输出接口使用的课表数据,是用户输入课表的最终“编译”后的形式。
/// </summary>
namespace HFUTCourseSimulation.Kernel.Data.Presentation {
/// <summary>
@ -30,42 +34,37 @@ namespace HFUTCourseSimulation.Kernel.Data.Presentation {
}
public class Semester {
/// <summary>
/// 学期开始日期。
/// 该类保证该日期一定是星期一,且没有时间数据。
/// </summary>
public readonly DateTime startDate;
/// <summary>
/// 教学周个数。
/// 该类保证该数值与weeks中存储的数据个数相同。
/// </summary>
public readonly int weekCount;
public int weekCount;
/// <summary>
/// 每天课程的节次数。
/// 该类保证该数值总是大于等于1。
/// </summary>
public readonly int indexCount;
public int indexCount;
/// <summary>
/// 早餐插入在第几节次后。
/// 该类保证该数值总是位于0至indexCount之间含首尾
/// </summary>
public readonly int breakfastAt;
public int breakfastAt;
/// <summary>
/// 午餐插入在第几节次后。
/// 该类保证该数值总是位于0至indexCount之间含首尾
/// 且总是大于等于breakfastAt。
/// </summary>
public readonly int lunchAt;
public int lunchAt;
/// <summary>
/// 晚餐插入在第几节次后。
/// 该类保证该数值总是位于0至indexCount之间含首尾
/// 且总是大于等于lunchAt。
/// </summary>
public readonly int dinnerAt;
public int dinnerAt;
/// <summary>
/// 每周课程数据。
/// </summary>
public readonly ImmutableList<Week> weeks;
public ImmutableList<Week> weeks;
public IndexKind GetIndexKind(int index) {
if (index <= 0) throw new ArgumentException("index out of range");
@ -82,41 +81,44 @@ namespace HFUTCourseSimulation.Kernel.Data.Presentation {
/// 每周七天的数据。
/// 该类保证该字段总包含7项。
/// </summary>
public readonly ImmutableList<Day> days;
public ImmutableList<Day> days;
}
public class Day {
/// <summary>
/// 这一天的日期的字符串形式
/// </summary>
public string date;
/// <summary>
/// 这一天的所有课程。
/// </summary>
public readonly ImmutableList<Lesson> lessons;
public ImmutableList<Lesson> lessons;
}
public class Lesson {
/// <summary>
/// 课程的名称。
/// </summary>
public readonly string name;
public string name;
/// <summary>
/// 课程的说明,例如教室位置,教师姓名等。
/// 该值可以包含换行。
/// </summary>
public readonly string description;
public string description;
/// <summary>
/// 课程的颜色
/// </summary>
public readonly Util.ColorPair color;
public Util.ColorPair color;
/// <summary>
/// 课程的起始节次。
/// 该类保证该值位于1到indexCount之间含首尾
/// </summary>
public readonly int startIndex;
public int startIndex;
/// <summary>
/// 课程的结束节次。
/// 该类保证该值位于1到indexCount之间含首尾
/// 且大于等于startIndex。
/// 课程的节次跨度
/// 该类保证该值总大于0且加上startIndex后不会大于节次数。
/// </summary>
public readonly int endIndex;
public int indexSpan;
}

View File

@ -8,6 +8,10 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
/// <summary>
/// 用于保存到文件的课表数据。
/// 同时也是程序中作为转换核心的数据结构,各种课表数据之间的转换以该结构为核心。
/// </summary>
namespace HFUTCourseSimulation.Kernel.Data.Storage {
/// <summary>

View File

@ -7,6 +7,10 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
/// <summary>
/// 用于用户界面呈现的课表数据。
/// 该命名空间的类针对WPF的UI实现了特殊的接口以正确地和用户交互。
/// </summary>
namespace HFUTCourseSimulation.Kernel.Data.Ui {
public class Semester : INotifyPropertyChanged {