import Foundation /// One time slot of a day: parallel lessons of subgroups share it. struct Slot: Hashable, Identifiable { var date: CivilDate var start: ClockTime var end: ClockTime var lessons: [Lesson] var id: String { date.iso + "|" + start.description } /// "13:40 → 15:05" var timeLabel: String { "\(start) → \(end)" } } func subjectKey(_ subject: String) -> String { subject.trimmed.lowercased() } /// End of a lesson: its own end, else the bell table's, else 80 minutes after the start. func lessonEnd(_ lesson: Lesson, _ t: Timetable) -> ClockTime { if let end = lesson.end.flatMap(ClockTime.init) { return end } if let bell = t.bells.first(where: { $0.start == lesson.start }), let end = ClockTime(bell.end) { return end } return (ClockTime(lesson.start) ?? ClockTime(minutes: 0)).adding(minutes: 80) } extension Timetable { func day(_ date: CivilDate) -> Day? { days.first { $0.date == date.iso } } func week(of date: CivilDate) -> Week? { day(date.monday)?.week ?? day(date)?.week } var dates: [CivilDate] { days.compactMap { CivilDate($0.date) } } /// Lessons of a date after the student's subgroup filter and the lessons they hid. func lessons(on date: CivilDate, subgroup: String?, hidden: Set) -> [Lesson] { (day(date)?.lessons ?? []) .filter { subgroup == nil || $0.subgroup == nil || $0.subgroup == subgroup } .filter { !hidden.contains($0.hideKey) } } /// "Занятия в Полоцке" on the date for the student's subgroup, only on a day they have lessons. func places(on date: CivilDate, subgroup: String?, hidden: Set) -> [DayPlace] { guard let day = day(date), !day.places.isEmpty, !lessons(on: date, subgroup: subgroup, hidden: hidden).isEmpty else { return [] } return day.places.forSubgroup(subgroup) } func slots(on date: CivilDate, subgroup: String?, hidden: Set) -> [Slot] { let lessons = lessons(on: date, subgroup: subgroup, hidden: hidden) var order: [String] = [] var groups: [String: [Lesson]] = [:] for l in lessons { if groups[l.start] == nil { order.append(l.start) } groups[l.start, default: []].append(l) } return order.compactMap { start in guard let begin = ClockTime(start), let items = groups[start] else { return nil } return Slot(date: date, start: begin, end: lessonEnd(items[0], self), lessons: items) } .sorted { $0.start < $1.start } } /// Palette slot of every subject: alphabetical, so a subject keeps its colour across refreshes. var subjectSlots: [String: Int] { let subjects = Set((days.flatMap(\.lessons) + weekly.flatMap(\.lessons)).map { subjectKey($0.subject) }).sorted() return Dictionary(uniqueKeysWithValues: subjects.enumerated().map { ($1, $0) }) } } private let shortWords: Set = ["и", "в", "во", "на", "по", "с", "со", "к", "о", "об", "для", "из", "от"] /// "Экономика предприятия в машиностроении" → "ЭПМ"; short names stay as they are. func abbreviate(_ subject: String) -> String { let name = (subject.split(separator: ":").first.map(String.init) ?? subject).trimmed if name.count <= 10 { return name } let letters = name.split(whereSeparator: { " -(),".contains($0) }) .filter { !$0.isEmpty && !shortWords.contains($0.lowercased()) && ($0.first?.isLetter ?? false) } .map { String($0.first!).uppercased() } .joined() return letters.count >= 2 ? String(letters.prefix(4)) : String(name.prefix(8)) } /// Faculty names as the student knows them. private let faculties = [ "Финансово-экономический факультет": "ФЭФ", "Факультет информационных технологий": "ФИТ", "Факультет компьютерных наук и электроники": "ФКНЭ", "Гуманитарный факультет": "ГФ", "Инженерно-строительный факультет": "ИСФ", "Механико-технологический факультет": "МТФ", "Юридический факультет": "ЮФ", ] func facultyShort(_ name: String) -> String? { name.isEmpty ? nil : (faculties[name] ?? name) } /// What a widget or reminder shows: today's slots, or the next study day's once today is over. struct Upcoming { var date: CivilDate var slots: [Slot] } func upcoming(_ t: Timetable, settings: UserSettings, now: Date = Date()) -> Upcoming? { let today = CivilDate.today(now) let clock = ClockTime.now(now) let todays = t.slots(on: today, subgroup: settings.subgroup, hidden: settings.hidden).filter { $0.end > clock } if !todays.isEmpty { return Upcoming(date: today, slots: todays) } for step in 1...7 { let d = today.adding(days: step) let slots = t.slots(on: d, subgroup: settings.subgroup, hidden: settings.hidden) if !slots.isEmpty { return Upcoming(date: d, slots: slots) } } return nil }