diff --git a/HFUTCourseSimulation/Kernel/Arranger.cs b/HFUTCourseSimulation/Kernel/Arranger.cs index c27361c..d0e24e8 100644 --- a/HFUTCourseSimulation/Kernel/Arranger.cs +++ b/HFUTCourseSimulation/Kernel/Arranger.cs @@ -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 { - /// - /// 一个标识符,用于指代学期中指定周的指定星期的指定节次 - /// - internal class ArrangerSpot : IEquatable { - public ArrangerSpot(int week, int day, int index) { - this.week = week; - this.day = day; - this.index = index; - } + ///// + ///// 一个标识符,用于指代学期中指定周的指定星期的指定节次 + ///// + //internal class ArrangerSpot : IEquatable { + // public ArrangerSpot(int week, int day, int index) { + // this.week = week; + // this.day = day; + // this.index = index; + // } - /// - /// 教学周 - /// - public int week; - /// - /// 星期几 - /// - public int day; - /// - /// 节次 - /// - public int index; + // /// + // /// 教学周 + // /// + // public int week; + // /// + // /// 星期几 + // /// + // public int day; + // /// + // /// 节次 + // /// + // 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.Default.Equals(left, right); - } + // public static bool operator ==(ArrangerSpot left, ArrangerSpot right) { + // return EqualityComparer.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); + // } + //} /// /// 课程安排器。 /// 负责将课程安排到每周之中,并提示用户错误等各种信息。 /// - public class Arranger { - public Arranger(Kernel.Data.Storage.Semester semester) { - this.semester = semester; - arrangeMap = new Dictionary(); - reporter = new Reporter(); - } - - private Kernel.Data.Storage.Semester semester; - private Dictionary arrangeMap; - private Reporter reporter; - - /// - /// 获取汇报器用于查看安排日志 - /// - /// - public Reporter GetReporter() { - return reporter; - } + public static class Arranger { /// /// 开始安排课程 /// /// 安排好的,用于呈现的课程。如果安排失败,则为null,查看汇报器来了解具体差错 - 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 验证用户课表输入 + /// /// 检查用户输入的学期数据是否正确 /// - /// 如果检查无误,返回检查后的结果,否则返回null - private Semester CheckSemester() { + /// 如果检查无误则返回检查好的结构,否则返回null + 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(); + var courses = new List(); foreach (var course in semester.Courses) { // 检查课程(实际上只有安排需要检查) reporter.Info($"正在检查课程:{course.Name}"); - var schedules = new List(); + var schedules = new List(); 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表示检查失败,否则返回检查好的值。 /// - 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中的三个字段Week,Day和Index的格式。 /// 各类参数含义与上面函数相同,只不过是作用到每一项上。 /// - private IEnumerable CheckIntegerCollection(string s, int min, int max, string field_name, string field_require) { + private static IEnumerable 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 安排课程 + /// /// 安排课程 /// - /// 安排无误返回true,否则返回false - private bool ArrangeCourses() { - // Check it first - var rv = CheckSemester(); - if (rv is null) return false; + /// 返回安排好的课程表 + 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 总结课程为方便渲染的结构 + /// /// 将安排好的课程总结为方便渲染的结果 /// - /// - private Kernel.Data.Presentation.Semester ConcludeCourses() { - // Arrange it first - var rv = ArrangeCourses(); - if (!rv) return null; + /// 总结好的可用于渲染的结果 + private static Data.Presentation.Semester ConcludeCourses(Data.Built.Semester semester, int[,,] arrangeMap, Reporter reporter) { + var dateCursor = semester.startDate; - return null; + var weeks = new List(); + for (var week = 1; week <= semester.weekCount; ++week) { + var days = new List(); + for (var day = 1; day <= 7; ++day) { + // 获取日期的字符串形式,并自增天数 + var dateString = dateCursor.ToString("MM/dd"); + dateCursor = dateCursor.AddDays(1); + + // 获取这一日的课程 + var lessons = new List(); + 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 + } } diff --git a/HFUTCourseSimulation/Kernel/Data/Built.cs b/HFUTCourseSimulation/Kernel/Data/Built.cs index e363849..5be9113 100644 --- a/HFUTCourseSimulation/Kernel/Data/Built.cs +++ b/HFUTCourseSimulation/Kernel/Data/Built.cs @@ -6,6 +6,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +/// +/// 安排课程时使用的课表数据。 +/// 仅在课表安排器中作为中间结构使用,其本质上为经过验证的用户输入的课表数据。 +/// namespace HFUTCourseSimulation.Kernel.Data.Built { public class Semester { diff --git a/HFUTCourseSimulation/Kernel/Data/Presentation.cs b/HFUTCourseSimulation/Kernel/Data/Presentation.cs index 69d1d9c..bdf74be 100644 --- a/HFUTCourseSimulation/Kernel/Data/Presentation.cs +++ b/HFUTCourseSimulation/Kernel/Data/Presentation.cs @@ -5,6 +5,10 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +/// +/// 用于最终呈现的排课完毕的课表数据。 +/// 方便模拟器和图片输出接口使用的课表数据,是用户输入课表的最终“编译”后的形式。 +/// namespace HFUTCourseSimulation.Kernel.Data.Presentation { /// @@ -30,42 +34,37 @@ namespace HFUTCourseSimulation.Kernel.Data.Presentation { } public class Semester { - /// - /// 学期开始日期。 - /// 该类保证该日期一定是星期一,且没有时间数据。 - /// - public readonly DateTime startDate; /// /// 教学周个数。 /// 该类保证该数值与weeks中存储的数据个数相同。 /// - public readonly int weekCount; + public int weekCount; /// /// 每天课程的节次数。 /// 该类保证该数值总是大于等于1。 /// - public readonly int indexCount; + public int indexCount; /// /// 早餐插入在第几节次后。 /// 该类保证该数值总是位于0至indexCount之间(含首尾)。 /// - public readonly int breakfastAt; + public int breakfastAt; /// /// 午餐插入在第几节次后。 /// 该类保证该数值总是位于0至indexCount之间(含首尾), /// 且总是大于等于breakfastAt。 /// - public readonly int lunchAt; + public int lunchAt; /// /// 晚餐插入在第几节次后。 /// 该类保证该数值总是位于0至indexCount之间(含首尾), /// 且总是大于等于lunchAt。 /// - public readonly int dinnerAt; + public int dinnerAt; /// /// 每周课程数据。 /// - public readonly ImmutableList weeks; + public ImmutableList 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项。 /// - public readonly ImmutableList days; + public ImmutableList days; } public class Day { + /// + /// 这一天的日期的字符串形式 + /// + public string date; /// /// 这一天的所有课程。 /// - public readonly ImmutableList lessons; + public ImmutableList lessons; } public class Lesson { /// /// 课程的名称。 /// - public readonly string name; + public string name; /// /// 课程的说明,例如教室位置,教师姓名等。 /// 该值可以包含换行。 /// - public readonly string description; + public string description; /// /// 课程的颜色 /// - public readonly Util.ColorPair color; + public Util.ColorPair color; /// /// 课程的起始节次。 /// 该类保证该值位于1到indexCount之间(含首尾)。 /// - public readonly int startIndex; + public int startIndex; /// - /// 课程的结束节次。 - /// 该类保证该值位于1到indexCount之间(含首尾), - /// 且大于等于startIndex。 + /// 课程的节次跨度。 + /// 该类保证该值总大于0,且加上startIndex后不会大于节次数。 /// - public readonly int endIndex; + public int indexSpan; } diff --git a/HFUTCourseSimulation/Kernel/Data/Storage.cs b/HFUTCourseSimulation/Kernel/Data/Storage.cs index 1e67692..8811f69 100644 --- a/HFUTCourseSimulation/Kernel/Data/Storage.cs +++ b/HFUTCourseSimulation/Kernel/Data/Storage.cs @@ -8,6 +8,10 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Media; +/// +/// 用于保存到文件的课表数据。 +/// 同时也是程序中作为转换核心的数据结构,各种课表数据之间的转换以该结构为核心。 +/// namespace HFUTCourseSimulation.Kernel.Data.Storage { /// diff --git a/HFUTCourseSimulation/Kernel/Data/Ui.cs b/HFUTCourseSimulation/Kernel/Data/Ui.cs index 87b0352..ef2b02d 100644 --- a/HFUTCourseSimulation/Kernel/Data/Ui.cs +++ b/HFUTCourseSimulation/Kernel/Data/Ui.cs @@ -7,6 +7,10 @@ using System.Text; using System.Threading.Tasks; using System.Windows.Media; +/// +/// 用于用户界面呈现的课表数据。 +/// 该命名空间的类针对WPF的UI实现了特殊的接口,以正确地和用户交互。 +/// namespace HFUTCourseSimulation.Kernel.Data.Ui { public class Semester : INotifyPropertyChanged {